SSD Nodes Learn 8GB RAM — $66/năm
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-08-01

Cài wg-easy WireGuard trên Docker Compose

Chạy wg-easy với Docker Compose: cấu hình port, NET_ADMIN, sysctl cần thiết và QR code để kết nối điện thoại. Lưu ý version 15 đổi cách cài đặt.

Bạn sẽ xây dựng gì

wg-easy là WireGuard có giao diện web và chạy trong một Docker container duy nhất. Nó quản lý WireGuard interface cho bạn và cung cấp UI trên trình duyệt để tạo client. Mỗi client bạn tạo sẽ có một file cấu hình và một QR code, nên điện thoại có thể tham gia VPN bằng cách hướng camera vào màn hình.

Tunnel sử dụng WireGuard thông thường. Kernel module xử lý việc chuyển tiếp packet, nên throughput giống như thiết lập thủ công. Lợi ích là bạn có thể quản lý vòng đời của client: thêm, vô hiệu hóa và xóa peer mà không cần chỉnh sửa file cấu hình qua SSH. Đổi lại, bạn mất quyền kiểm soát trực tiếp file cấu hình đó. Nội dung này được trình bày trong thiết lập WireGuard thủ công trên VPS.

Bạn cần một KVM VPS có địa chỉ IPv4 public, Docker Engine với Compose plugin và quyền truy cập root. Các dạng container virtualisation dùng chung kernel của host, như OpenVZ hoặc LXC, thường không thể load WireGuard module. Khi đó, container sẽ không thể đưa interface lên.

Version 15 chuyển phần cài đặt ra khỏi biến môi trường

Hầu hết hướng dẫn bạn tìm thấy được viết cho wg-easy 14. Ở phiên bản đó, bạn đặt WG_HOST thành địa chỉ máy chủ và PASSWORD_HASH thành bcrypt hash của mật khẩu admin, cả hai đều dưới dạng biến môi trường. Version 15 là bản viết lại. Ghi chú migration chính thức nêu rõ rằng v15 không sử dụng các biến môi trường giống v14, và hầu hết cấu hình đã được chuyển vào admin panel trong web UI.

Vì vậy, WG_HOSTPASSWORD_HASH không còn có tác dụng. Nếu bạn sao chép một compose file cũ, container sẽ khởi động, bỏ qua các dòng đó, rồi yêu cầu bạn tạo tài khoản admin trong trình duyệt. Đây không phải lỗi. Đây là quy trình thiết lập mới.

Tính đến tháng 7 năm 2026, major tag cần pin là 15. Hãy pin major version thay vì dùng latest, vì major upgrade sẽ thay đổi định dạng config trên đĩa và không thể rollback sạch sẽ.

Tệp compose

Tạo một thư mục cho stack rồi ghi tệp compose chính thức vào đó. Đây là tệp upstream, không thay đổi.

sudo mkdir -p /etc/docker/containers/wg-easy
sudo curl -o /etc/docker/containers/wg-easy/docker-compose.yml \
  https://raw.githubusercontent.com/wg-easy/wg-easy/master/docker-compose.yml

Nội dung có dạng như sau:

volumes:
  etc_wireguard:

services:
  wg-easy:
    image: ghcr.io/wg-easy/wg-easy:15
    container_name: wg-easy
    networks:
      wg:
        ipv4_address: 10.42.42.42
        ipv6_address: fdcc:ad94:bacf:61a3::2a
    volumes:
      - etc_wireguard:/etc/wireguard
      - /lib/modules:/lib/modules:ro
    ports:
      - "51820:51820/udp"
      - "51821:51821/tcp"
    restart: unless-stopped
    cap_add:
      - NET_ADMIN
      - SYS_MODULE
    sysctls:
      - net.ipv4.ip_forward=1
      - net.ipv4.conf.all.src_valid_mark=1
      - net.ipv6.conf.all.disable_ipv6=0
      - net.ipv6.conf.all.forwarding=1
      - net.ipv6.conf.default.forwarding=1

networks:
  wg:
    driver: bridge
    enable_ipv6: true
    ipam:
      driver: default
      config:
        - subnet: 10.42.42.0/24
        - subnet: fdcc:ad94:bacf:61a3::/64

etc_wireguard là named volume chứa server key và mọi client bạn tạo. Hãy sao lưu volume đó, nếu không việc rebuild sẽ xóa toàn bộ peer. Nếu muốn xem các tệp đó trên filesystem của host, hãy thay volume này bằng bind mount. Trước khi làm, hãy đọc sự khác biệt giữa bind mount và named volume, vì quyền truy cập hoạt động khác nhau.

Vì sao cần NET_ADMIN, SYS_MODULE và các sysctl

Mặc định, container không được phép tác động vào network stack. Mỗi dòng này loại bỏ một rào cản cụ thể.

NET_ADMIN cho phép container tạo interface wg0, gán địa chỉ cho interface đó và ghi các route. Nếu thiếu quyền này, container khởi động rồi dừng khi đưa interface lên, vì ip link add wg0 type wireguard trả về Operation not permitted.

SYS_MODULE cùng với mount /lib/modules chỉ đọc cho phép container load kernel module WireGuard nếu host chưa load module này. Module nằm trong host kernel, không nằm bên trong image. Vì vậy, thư mục trên host phải được hiển thị cho container. Trên kernel hiện đại, module thường đã được built-in. Bạn có thể xác nhận bằng sudo modprobe wireguard && echo ok trên host.

net.ipv4.ip_forward=1 khiến kernel forward các packet không gửi đến chính máy đó. Nếu thiếu, client kết nối, handshake thành công, nhưng mọi packet đi đến internet bị drop. Vì vậy, ping 1.1.1.1 timeout trong khi VPN vẫn hiển thị là đã kết nối.

net.ipv4.conf.all.src_valid_mark=1 là mục khiến nhiều người bất ngờ. WireGuard đánh dấu các packet đi ra của chính nó để chúng không bị route ngược vào tunnel. Reverse path filtering ở chế độ strict thấy packet có source address không khớp với route dự kiến và drop packet đó. Sysctl này yêu cầu kernel chấp nhận các packet đã được đánh dấu. Đây là yếu tố giúp full tunnel không tự làm gián đoạn chính nó.

Khởi động và tạo tài khoản quản trị

cd /etc/docker/containers/wg-easy
sudo docker compose up -d
sudo docker compose logs -f

Dùng docker compose updocker compose down, không dùng startstop. Upstream cảnh báo rằng start trên một container được tạo với các thiết lập khác có thể khiến network rơi vào trạng thái không nhất quán. Nếu muốn stack tự khởi động lại sau khi reboot, restart: unless-stopped đã xử lý việc đó. Hành vi khởi động của các service compose giải thích policy này đảm bảo và không đảm bảo điều gì.

Web UI lắng nghe trên TCP 51821. Trong lần truy cập đầu tiên, UI hiển thị trang thiết lập để bạn tạo tài khoản quản trị và xác nhận địa chỉ host mà client sẽ dùng để kết nối đến server. Địa chỉ host đó được ghi vào dòng Endpoint trong mọi client config, nên phải là public IP hoặc tên DNS của VPS. Nếu địa chỉ sai, QR code bạn đưa cho điện thoại sẽ trỏ đến một địa chỉ không thể truy cập và handshake sẽ không hoàn tất.

Còn một điểm cần lưu ý về port đó: wg-easy 15 từ chối HTTP thuần nếu bạn không đặt INSECURE=true. Truy cập qua HTTPS bằng certificate không được tin cậy, hoặc terminate TLS tại reverse proxy phía trước nó, đều được. Truy cập qua http:// với thiết lập mặc định thì không được.

Không công khai port UI trên Internet

File compose công khai port 51821 trên mọi interface. Đây là trang đăng nhập của một máy có thể định tuyến traffic của bạn, vì vậy không nên mở port này cho toàn Internet. Khi publish một port trong Docker, Docker ghi các rule vào chain DOCKER. Chain này được xử lý trước ufw, nên rule ufw deny không đóng được port đó. Đây là một bẫy đáng hiểu rõ, và vì sao các port được Docker publish bỏ qua ufw giải thích đầy đủ vấn đề này.

Cách sửa đơn giản là bind UI vào loopback rồi truy cập qua SSH tunnel:

    ports:
      - "51820:51820/udp"
      - "127.0.0.1:51821:51821/tcp"
    environment:
      - INSECURE=true

Sau đó, trên laptop của bạn:

ssh -L 51821:127.0.0.1:51821 youruser@your.server.address

Mở http://127.0.0.1:51821 trong trình duyệt trên laptop. Traffic được SSH mã hóa, port không phản hồi với bất kỳ máy nào khác, và INSECURE=true an toàn trong trường hợp này vì kết nối HTTP thuần không rời khỏi loopback interface.

Mở UDP 51820 và kiểm tra cả hai firewall

WireGuard cần UDP 51820 có thể truy cập từ Internet. Docker publish port này, nhưng nhiều nhà cung cấp đặt một network firewall riêng phía trước VPS mà Docker không biết đến. Hãy mở port ở cả hai nơi. Nếu bạn quản lý firewall của host bằng ufw, các rule ufw cơ bản cho VPS sẽ nhanh hơn so với tự viết nftables.

Kiểm tra xem container có thực sự đang listen hay không:

sudo ss -ulnp | grep 51820

Bạn phải thấy một socket UDP đang listen. Nếu dòng đó không có gì, container chưa đưa interface lên và sudo docker compose logs wg-easy sẽ cho biết lý do.

Tạo client và quét trên điện thoại

Trong UI, tạo một client và đặt tên dễ nhận ra sau này, chẳng hạn theo thiết bị mà client đó thuộc về. wg-easy tự cấp địa chỉ tunnel còn trống tiếp theo và tạo key pair cho bạn. Mỗi dòng client có QR code và file .conf để tải xuống.

Cài ứng dụng WireGuard chính thức trên điện thoại. Chọn thêm tunnel từ QR code, rồi hướng camera vào QR code trên màn hình. Tunnel sẽ xuất hiện với tên bạn đã nhập. Bật tunnel. Dòng client trong UI sẽ bắt đầu hiển thị bộ đếm lưu lượng và thời điểm handshake gần nhất.

Nếu client không hiển thị handshake sau khi được bật, client hoàn toàn chưa kết nối được đến server. Nguyên nhân thường liên quan đến UDP 51820, có thể ở firewall của nhà cung cấp hoặc ở địa chỉ endpoint được ghi sẵn trong cấu hình. Nếu client có handshake nhưng Internet không hoạt động, vấn đề nằm ở forwarding hoặc DNS.

Trên máy tính, tải file .conf xuống và import file đó vào WireGuard client thay vì nhập lại cấu hình. Private key trong file được tạo một lần và chỉ hiển thị một lần. Hãy bảo vệ file này như cách bạn bảo vệ SSH private key.

Khi nào nên chuyển khỏi UI

wg-easy phù hợp khi các peer của bạn là người dùng và điện thoại. UI nhanh hơn việc sửa file cấu hình, và việc thu hồi quyền truy cập của điện thoại bị mất chỉ cần một lần nhấp.

Bạn sẽ chạm đến giới hạn khi cần một chức năng mà UI không hỗ trợ. Định tuyến site-to-site, trong đó AllowedIPs của một peer bao phủ toàn bộ subnet từ xa thay vì một địa chỉ duy nhất, thường là giới hạn đầu tiên. Tiếp theo là split tunnel với quy tắc định tuyến riêng cho từng peer hoặc cấu hình do công cụ provisioning tạo ra. Khi đó, thiết lập thủ công không khó hơn; nó chỉ khác cách làm. Hướng dẫn WireGuard cơ bản trình bày cách tạo cùng tunnel bằng wg0.conf. Nếu muốn ngừng chạy control plane hoàn toàn, WireGuard so với Tailscale trình bày tùy chọn managed.

Nếu phần syntax của compose ở trên mới là phần lạ với bạn, thay vì phần WireGuard, Kiến thức cơ bản về Docker Compose trên VPS giải thích format của file và các lệnh thường dùng.

FAQ

Vì sao wg-easy bỏ qua WG_HOST và PASSWORD_HASH?

Các biến này thuộc về wg-easy 14. Version 15 đã được viết lại, và upstream đã chuyển gần như toàn bộ cấu hình vào admin panel trong web UI. Container không đọc biến nào trong hai biến này. Vì vậy, container vẫn khởi động bình thường rồi yêu cầu bạn tạo tài khoản admin trong lần truy cập đầu tiên. Đặt địa chỉ host mà client sử dụng trên trang thiết lập đó.

Tôi có cần SYS_MODULE nếu kernel đã có WireGuard không?

Không. SYS_MODULE và mount /lib/modules được dùng để container có thể nạp module khi host chưa có module này. Trên host mà sudo modprobe wireguard đã chạy thành công, capability này không được sử dụng. Gỡ capability này là một bước hardening hợp lý. Dù có gỡ hay không, NET_ADMIN vẫn bắt buộc.

Client kết nối được nhưng không có internet. Lỗi nằm ở đâu?

Handshake thành công nhưng không có traffic gần như luôn liên quan đến forwarding. Xác nhận net.ipv4.ip_forward=1net.ipv4.conf.all.src_valid_mark=1 vẫn còn trong compose file, vì bản sao được chỉnh sửa thủ công thường làm mất các mục này. Nếu forwarding đã bật, hãy kiểm tra DNS server mà client nhận được. Một tunnel gửi toàn bộ traffic qua VPN nhưng trỏ đến DNS server không còn truy cập được sẽ biểu hiện trong browser giống hệt một kết nối bị hỏng.

Tôi sao lưu các client như thế nào?

Toàn bộ dữ liệu nằm trong named volume etc_wireguard, trong file wg0.json. UI cũng có nút backup để export cùng dữ liệu đó. Hãy copy file này ra ngoài server trước mọi lần upgrade. Khôi phục được thực hiện bằng cách upload file trong bước thiết lập trên một container mới.

Tôi có thể chạy wg-easy phía sau reverse proxy không?

Có. Đặt proxy phía trước TCP 51821, terminate TLS tại đó, rồi đặt INSECURE=true trên container để container chấp nhận hop HTTP không mã hóa từ proxy. Giữ UDP 51820 được publish trực tiếp, vì traffic VPN sử dụng UDP và không đi qua HTTP proxy.