wg-easy Docker Compose 部署与 WireGuard 配置
使用 Docker Compose 部署 wg-easy,了解 WireGuard 所需端口、NET_ADMIN、SYS_MODULE 与 sysctl 设置,避开 v15 环境变量变更,并通过 QR 码让手机快速接入。
您要构建的内容
wg-easy 是带 Web 界面的 WireGuard,以单个 Docker 容器运行。它会为您管理 WireGuard 接口,并提供用于创建客户端的浏览器界面。您创建的每个客户端都会获得一个配置文件和一个 QR 码,因此手机对准屏幕扫描即可加入 VPN。
隧道本身使用标准 WireGuard。内核模块负责传输数据包,因此吞吐量与手动配置相同。您获得的是客户端生命周期管理功能:无需通过 SSH 编辑配置文件,即可添加、禁用和删除对等端。代价是无法直接控制该配置文件。相关内容请参阅 VPS 上的手动 WireGuard 配置。
您需要一台具有公共 IPv4 地址的 KVM VPS、带 Compose 插件的 Docker Engine,以及 root 访问权限。OpenVZ 或 LXC 等共享主机内核的容器虚拟化环境通常无法加载 WireGuard 模块,因此容器将无法启动该接口。
版本 15 将设置移出环境变量
您找到的大多数指南都是为 wg-easy 14 编写的。当时,您需要将 WG_HOST 设置为服务器地址,将 PASSWORD_HASH 设置为管理员密码的 bcrypt 哈希值,并将二者都配置为环境变量。版本 15 是一次重写。官方迁移说明明确指出,v15 不使用与 v14 相同的环境变量,并且其中大多数设置已移至 Web UI 的管理面板。
因此,WG_HOST 和 PASSWORD_HASH 已不再生效。如果您复制旧的 compose 文件,容器会启动并忽略这些行,随后要求您在浏览器中创建管理员帐户。这不是错误,而是新的设置流程。
截至 2026 年 7 月,应固定使用的主要版本标签是 15。请固定主要版本,而不要使用 latest,因为主要版本升级会更改磁盘上的配置格式,且无法干净地回滚。
compose 文件
为该堆栈创建目录,并将官方 compose 文件写入其中。这是上游文件,内容未作修改。
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内容如下:
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::/64etc_wireguard 是一个命名卷,其中保存服务器密钥和您创建的所有客户端。请备份该卷,否则重建会丢失所有对等端。如果您希望在主机文件系统中查看这些文件,可以将其替换为绑定挂载。执行此操作前,请阅读绑定挂载与命名卷的区别,因为两者的权限行为不同。
为什么需要 NET_ADMIN、SYS_MODULE 和 sysctl
默认情况下,容器不能操作网络协议栈。以下每一行都会解除一个特定限制。
NET_ADMIN 允许容器创建 wg0 接口、为其分配地址并写入路由。没有它,容器会启动,然后在启用接口时退出,因为 ip link add wg0 type wireguard 返回 Operation not permitted。
SYS_MODULE 加上只读挂载 /lib/modules,允许容器在主机尚未加载 WireGuard 内核模块时加载该模块。该模块位于主机内核中,而不在镜像内,因此必须让容器能够访问主机目录。在现代内核中,该模块通常已内置。您可以在主机上使用 sudo modprobe wireguard && echo ok 进行确认。
net.ipv4.ip_forward=1 让内核转发目标不是本机的数据包。没有它,客户端可以连接,握手也会成功,但发往互联网的所有数据包都会被丢弃,因此 ping 1.1.1.1 会超时,而 VPN 看起来仍处于连接状态。
net.ipv4.conf.all.src_valid_mark=1 是最容易令人困惑的一项。WireGuard 会标记自身发出的数据包,使其不会被重新路由回隧道。严格的反向路径过滤会发现数据包的源地址与预期路由不匹配,然后将其丢弃。此 sysctl 告诉内核接受带标记的数据包,从而防止全隧道连接中断。
启动并创建管理员账户
cd /etc/docker/containers/wg-easy
sudo docker compose up -d
sudo docker compose logs -f使用 docker compose up 和 docker compose down,不要使用 start 和 stop。上游项目警告,在不同设置下创建的容器上运行 start 会使网络处于不一致状态。如果希望系统重启后恢复该堆栈,restart: unless-stopped 已经涵盖此需求;compose 服务的启动行为说明了该策略的作用范围和限制。
Web UI 监听 TCP 51821。首次访问时会显示设置页面,您可以在其中创建管理员账户,并确认客户端用于连接服务器的主机地址。该主机地址会写入每个客户端配置的 Endpoint 行,因此必须填写 VPS 的公网 IP 或 DNS 名称。如果填写错误,您提供给手机的 QR code 会指向无法访问的地址,握手也不会完成。
还需要注意该端口:除非设置 INSECURE=true,否则 wg-easy 15 会拒绝普通 HTTP。通过不受信任的证书使用 HTTPS 访问,或在其前方使用反向代理终止 TLS,均可正常工作。使用默认设置通过 http:// 访问则不可行。
不要将 UI 端口发布到互联网
compose 文件会在所有接口上发布 51821。这是一个可路由网络流量的主机的登录页面,不应对公网开放。在 Docker 中发布端口会将规则写入 DOCKER 链。该链的处理顺序早于 ufw,因此 ufw deny 规则无法关闭此端口。这个问题本身值得了解,为什么 Docker 发布的端口会忽略 ufw对此有完整说明。
最简单的修复方法是将 UI 绑定到 loopback,然后通过 SSH 隧道访问:
ports:
- "51820:51820/udp"
- "127.0.0.1:51821:51821/tcp"
environment:
- INSECURE=true然后在您的笔记本电脑上执行:
ssh -L 51821:127.0.0.1:51821 youruser@your.server.address在笔记本电脑的浏览器中打开 http://127.0.0.1:51821。流量由 SSH 加密,其他主机无法访问此端口。这里使用 INSECURE=true 是安全的,因为未加密的 HTTP 跳转不会离开 loopback 接口。
打开 UDP 51820,并检查两个防火墙
WireGuard 本身要求互联网可访问 UDP 51820。Docker 会发布该端口,但许多服务提供商会在 VPS 前面设置独立的网络防火墙,Docker 无法感知它。请在两个位置都打开该端口。如果使用 ufw 管理主机防火墙,VPS 的基本 ufw 规则比手动编写 nftables 更简单。
检查容器是否确实在监听:
sudo ss -ulnp | grep 51820您应该看到一个正在监听的 UDP 套接字。该行没有任何内容,表示容器未能启动接口,sudo docker compose logs wg-easy 会说明原因。
创建客户端并在手机上扫描
在 UI 中创建客户端,并为其设置一个便于日后识别的名称,例如所属设备的名称。wg-easy 会分配下一个可用的隧道地址,并自动生成密钥对。每个客户端行都提供 QR 码和可下载的 .conf 文件。
在手机上安装官方 WireGuard 应用,选择从 QR 码添加隧道,然后将摄像头对准屏幕上的 QR 码。隧道会显示为你输入的名称。启用隧道后,UI 中的客户端行会开始显示传输计数器和最近的握手时间。
启用客户端后仍未显示握手,说明客户端根本无法连接到服务器。问题通常出在 UDP 51820,可能是服务提供商的防火墙阻止了访问,也可能是配置中内置的 endpoint 地址有误。客户端如果显示握手但无法正常访问互联网,问题通常出在转发或 DNS。
在桌面设备上下载 .conf 文件,然后将其导入 WireGuard 客户端,不要手动重新输入配置。该文件中的私钥只生成一次,也只显示一次。应像保护 SSH 私钥一样保护此文件。
何时应超出 UI 的能力范围
当对端是个人设备和手机时,wg-easy 是合适的工具。使用 UI 比编辑配置文件更快。撤销丢失手机的访问权限只需单击一次。
当您需要实现 UI 未建模的功能时,就会遇到它的限制。站点到站点路由通常是首先遇到的限制:某个对端的 AllowedIPs 覆盖整个远程子网,而不是单个地址。接下来是带有每个对端路由规则的分流隧道,或者由配置预配工具生成的配置。此时,手动编写配置并不更难,只是方式不同。WireGuard 基础指南展示了如何使用 wg0.conf 构建相同的隧道。如果您希望完全停止运行控制平面,WireGuard 与 Tailscale 的比较介绍了托管方案。
如果上面的 compose 语法才是您不熟悉的部分,而不是 WireGuard,VPS 上的 Docker Compose 基础会介绍文件格式和日常命令。
FAQ
为什么 wg-easy 会忽略我的 WG_HOST 和 PASSWORD_HASH?
这些变量属于 wg-easy 14。版本 15 完全重写了,上游已将几乎所有配置移到 Web UI 的管理面板中。容器不会读取这两个变量,因此会正常启动,并在首次访问时要求您创建管理员帐户。在该设置页面中设置面向客户端的主机地址。
如果内核已经支持 WireGuard,还需要 SYS_MODULE 吗?
不需要。SYS_MODULE 和 /lib/modules 挂载用于在主机尚未加载该模块时,让容器加载模块。如果主机上的 sudo modprobe wireguard 已经成功,容器就不会使用此功能。删除它是合理的加固措施,但无论是否删除,仍然需要 NET_ADMIN。
客户端可以连接,但无法访问互联网。哪里有问题?
只有握手而没有流量,几乎总是转发配置导致的。确认 compose 文件中仍有 net.ipv4.ip_forward=1 和 net.ipv4.conf.all.src_valid_mark=1,因为手动编辑的副本经常会丢失这些配置。如果已启用转发,请检查客户端收到的 DNS 服务器。隧道将所有流量发送到 VPN,但指定的 DNS 服务器已无法访问时,在浏览器中看起来与连接失效完全相同。
如何备份客户端?
所有数据都存储在 etc_wireguard 命名卷中的 wg0.json 文件内。UI 也提供备份按钮,可导出相同的数据。在升级前,将该文件复制到服务器以外的位置。恢复时,在新容器的设置步骤中上传该文件。
可以在反向代理后运行 wg-easy 吗?
可以。将代理置于 TCP 51821 前方,并在那里终止 TLS;然后在容器上设置 INSECURE=true,使其接受代理发送的明文 HTTP 请求。直接发布 UDP 51820,因为 VPN 流量使用 UDP,不会经过 HTTP 代理。