wg-easy Docker Compose 部署与 WireGuard 配置
了解 wg-easy v15 的配置迁移、Docker Compose 端口、NET_ADMIN、SYS_MODULE 与 sysctl 参数,并通过二维码让手机快速加入 WireGuard VPN。
构建内容
wg-easy 是带 Web 界面的 WireGuard,以单个 Docker 容器运行。它会代您管理 WireGuard 接口,并提供用于创建客户端的浏览器界面。您创建的每个客户端都会获得一个配置文件和一个二维码,因此手机对准屏幕扫描二维码即可加入 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 月,应固定使用的 major tag 是 15。请固定 major 版本,不要使用 latest,因为 major 版本升级会改变磁盘上的配置格式,且无法正常回滚。
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 码会指向无法访问的地址,握手也永远无法完成。
还需要注意该端口的一点:除非设置 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 会分配下一个可用的隧道地址,并自动生成密钥对。每个客户端行都提供二维码和可下载的 .conf 文件。
在手机上安装官方 WireGuard 应用,选择通过二维码添加隧道,然后用摄像头扫描屏幕上的二维码。隧道会显示为你输入的名称。将其启用后,UI 中的客户端行会开始显示传输计数器和最近一次握手时间。手机接入隧道后,就能访问从未发布到互联网的服务。因此,无论身在何处,手机都可以持续向 自托管照片服务器 上传照片,而该服务器无需向公网开放任何端口。同样的方法也适用于媒体服务。重建为 90 年代录像店的 Jellyfin 媒体库 在酒店房间中浏览起来很方便,同时仍像在局域网中一样保持私有。告警也可以通过同一隧道反向发送:自托管的 ntfy 服务器 可以在备份任务失败的瞬间向手机推送消息,而无需响应来自公网的任何请求。
启用后仍不显示握手的客户端,实际上根本无法连接到服务器。问题通常出在 UDP 51820 上,可能是提供商防火墙阻止了该端口,也可能是配置中写入的端点地址有误。显示握手但无法正常访问互联网的客户端,问题通常出在转发或 DNS。
在桌面设备上,下载 .conf 文件,并将其导入 WireGuard 客户端,不要手动重新输入配置。该文件中的私钥只生成一次,也只显示一次。请像保护 SSH 私钥一样保护此文件。
何时应停止使用 UI
当您的对端主要是个人设备和手机时,wg-easy 是合适的工具。使用 UI 比编辑配置文件更快;手机丢失后,撤销其访问权限只需单击一次。
当您需要 UI 无法描述的配置时,就会遇到它的限制。站点到站点路由通常是第一个限制:对端的 AllowedIPs 覆盖整个远程子网,而不是单个地址。下一步通常是为每个对端配置路由规则的分流隧道,或使用配置生成工具生成配置文件。此时,手动编写配置并不更难,只是方式不同;WireGuard 基础指南展示了如何使用 wg0.conf 构建相同的隧道。如果您希望完全停止运行控制平面,WireGuard 与 Tailscale 的比较介绍了托管方案。是否值得这样取舍,取决于协调服务器实际能够访问哪些资源;在让它接入您的网络前,建议阅读 Tailscale 的信任模型。成本通常是下一个问题,而 Tailscale 免费套餐实际包含的内容表明,家庭用户或小型团队通常无需付费。超过免费套餐后,计费按用户而不是设备计算。这与您已经在支付费用的 VPS 的计费方式不同,因此在迁移团队前,应先查看 超出 Tailscale 免费套餐后的费用。您刚刚构建的完整隧道在 Tailscale 中有直接对应方案:将 VPS 发布为 Tailscale 出口节点,即可通过服务器使用相同的出站路由;区别在于,该路由是在管理控制台中批准的,而不是写入每个客户端配置。子网限制也有对应方案,因为从 VPS 发布整个私有网络可以将该网络提供给 tailnet 中的所有设备,无需逐个对端编辑 AllowedIPs,也就不必因此离开 UI。如果您需要该仪表板和自动网状路由,但不希望使用他人的协调服务器,在 VPS 上运行您自己的 NetBird 服务器可以将控制平面保留在您拥有的硬件上,但您需要自行配置 DNS 和 TLS;这些工作是 wg-easy 不要求您处理的。
如果上文中让您感到陌生的是 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 挂载用于在主机尚未加载 WireGuard 模块时,让容器加载该模块。如果主机上 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 文件内。Web UI 也提供备份按钮,可以导出相同的数据。在升级前,将该文件复制到服务器之外的位置。恢复时,在全新容器的设置步骤中上传该文件。
可以将 wg-easy 放在反向代理之后运行吗?
可以。将代理置于 TCP 51821 前面,并在代理处终止 TLS,然后在容器中设置 INSECURE=true,使其接受代理转发的明文 HTTP 请求。保持 UDP 51820 直接发布,因为 VPN 流量使用 UDP,不会经过 HTTP 代理。