SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor

wg-easy Docker Compose 配置与 WireGuard 二维码

使用 Docker Compose 部署 wg-easy,了解 WireGuard 端口、NET_ADMIN、SYS_MODULE 和必需 sysctl;v15 已移除旧环境变量,并支持手机扫码导入配置。

构建内容

wg-easy 是带 Web 界面的 WireGuard,以单个 Docker 容器运行。它会代为管理 WireGuard 接口,并提供用于创建客户端的浏览器界面。每个创建的客户端都会获得一个配置文件和二维码,因此手机只需将摄像头对准屏幕即可加入 VPN。

隧道本身使用标准 WireGuard。内核模块负责转发数据包,因此吞吐量与手动配置相同。它提供的是客户端生命周期管理功能:无需通过 SSH 编辑配置文件,即可添加、禁用和删除对等端。代价是无法直接控制该配置文件;相关内容请参阅VPS 上的手动 WireGuard 配置。

您需要一台具有公网 IPv4 地址的 KVM VPS、安装 Docker Engine 和 Compose 插件的环境,以及 root 访问权限。共享宿主机内核的容器虚拟化环境(例如 OpenVZ 或 LXC)通常无法加载 WireGuard 模块,容器也就无法启动该接口。

第15版将设置移出环境变量

您看到的大多数指南都是为 wg-easy 14 编写的。在该版本中,您需要将 WG_HOST 设置为服务器地址,将 PASSWORD_HASH 设置为管理员密码的 bcrypt 哈希值,并将二者都配置为环境变量。第15版进行了重写。官方迁移说明明确指出,v15 不使用与 v14 相同的环境变量,其中大多数设置已移至 Web UI 中的管理面板。

因此,WG_HOST 和 PASSWORD_HASH 已不再生效。如果您复制旧版 compose 文件,容器会启动并忽略这些配置行,然后要求您在浏览器中创建管理员帐户。这不是错误,而是新的设置流程。

截至 2026 年 10 月,应固定使用的主要标签是 15。请固定主要版本,不要使用 latest,因为主要版本升级会更改磁盘上的配置格式,且无法正常回滚。迁移到第15版后,v14 使用的 JSON 文件已替换为 SQLite 数据库。

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::/64

etc_wireguard 是一个命名卷,用于保存服务器密钥以及通过 SQLite 数据库 wg-easy.db 创建的所有客户端。请备份该卷,否则重建会删除所有对等端。如果希望在主机文件系统中查看这些文件,可以将其替换为绑定挂载。但请先阅读绑定挂载与命名卷的区别,因为两者的权限行为不同。

为什么需要 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 只会重启已经存在的容器,因此你对 compose 文件所做的任何更改(包括网络和 sysctl 设置)都不会应用;而 up 会重新创建发生变化的部分。如果你希望主机重启后恢复该堆栈,restart: unless-stopped 已经涵盖此需求;compose 服务的启动行为说明了该策略能保证和不能保证的内容。

Web UI 监听 TCP 51821。首次访问时会显示设置页面,你可以在其中创建管理员账户,并确认客户端用于连接服务器的主机地址。该主机地址会写入每个客户端配置的 Endpoint 行,因此必须填写 VPS 的公网 IP 或 DNS 名称。如果填写错误,提供给手机的二维码会指向无法访问的地址,握手也永远无法完成。

还需要注意该端口的一点:除非设置 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 中的每台设备都可访问该网络,无需进行把你带离 UI 的逐个对端 AllowedIPs 编辑。如果你希望使用该仪表板和自动网状路由,但不想使用他人的协调服务器,在 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 挂载用于在主机尚未加载该模块时,让容器加载模块。在 sudo modprobe wireguard 已经成功执行的主机上,这项 capability 不会被使用。移除它是合理的加固措施,但无论是否移除,仍然需要 NET_ADMIN。

客户端可以连接,但无法访问互联网。问题是什么?

只有握手而没有流量,几乎总是转发配置导致的。确认 compose 文件中仍然包含 net.ipv4.ip_forward=1 和 net.ipv4.conf.all.src_valid_mark=1,因为手动编辑的副本经常会丢失这些配置。如果已启用转发,请检查客户端收到的 DNS 服务器。隧道将所有流量发送到 VPN,却指向客户端无法再访问的 DNS 服务器时,在浏览器中看起来就像连接失效。

如何备份客户端?

所有数据都存储在 etc_wireguard 命名卷中。在版本 15 中,这表示容器内 /etc/wireguard/wg-easy.db 处的 SQLite 数据库。v15 文档没有说明备份或导出功能,因此请直接备份该卷:运行 docker compose down,将卷内容复制到服务器之外的位置,然后使用 docker compose up -d 重新启动整个堆栈。请在任何升级前执行此操作。旧指南提到的 wg0.json 文件属于 wg-easy 14。它是 v14 备份按钮导出的文件;在 v15 中,它唯一的用途是在迁移旧安装时,作为设置向导中上传的文件。

wg-easy 可以部署在反向代理之后吗?

可以。将代理置于 TCP 51821 前端,在代理处终止 TLS,并在容器上设置 INSECURE=true,使其接受来自代理的明文 HTTP 请求。请直接发布 UDP 51820,因为 VPN 流量使用 UDP,不会经过 HTTP 代理。