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所讨论的内容。

你需要一台可管理的 Linux 主机,例如 KVM VPS;该主机还应具有公网 IP 地址或指向它的域名、x86_64 或 arm64 CPU、带 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 年 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 名称。如果填写错误,提供给手机的二维码会指向无法访问的地址,握手也永远不会完成。

还需要注意该端口的一点:wg-easy 不会自行提供 TLS,也不会拒绝纯 HTTP。通过 http:// 访问时,UI 仍会加载并显示警告横幅。默认配置实际改变的是会话 Cookie,因为该 Cookie 带有 Secure 标志。浏览器通过公网地址上的纯 HTTP 访问 UI 时会丢弃该 Cookie,因此登录状态无法保持。INSECURE=true 只会移除该标志,不会执行其他操作。在前面放置一个终止 TLS 的反向代理,即使使用自签名证书,也完全不需要修改 wg-easy。

不要将 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 只会从会话 Cookie 中移除 Secure 标志,使浏览器能够在这个纯 HTTP 地址上保留登录状态。这里是安全的,因为纯 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 私钥一样保护此文件。

何时需要超越界面

当你的对等端主要是人员和手机时,wg-easy 是合适的工具。其界面比编辑配置文件更快,撤销丢失的手机只需单击一次。

当你需要实现界面无法描述的配置时,就会遇到它的限制。站点到站点路由通常是第一个障碍:对等端的 AllowedIPs 不再覆盖单个地址,而是覆盖整个远程子网。接下来可能是带有每个对等端路由规则的拆分隧道,或由配置预配工具生成的配置。此时,手动配置并不更难,只是方式不同;纯 WireGuard 指南展示了如何从 wg0.conf 构建相同的隧道。如果你希望完全停止运行控制平面,WireGuard 与 Tailscale 的对比介绍了托管方案。是否值得这样取舍,取决于协调服务器实际能够访问什么资源。在将网络交给它之前,建议阅读Tailscale 的信任模型。费用通常是下一个问题;Tailscale 免费套餐实际包含的内容是:最多 6 名用户免费,但仅限非商业用途。因此,家庭用户无需付费,而小型企业团队无论人数多寡都不符合条件。超过这一范围后,计费按用户而不是设备计算。这与已经付费的 VPS 采用不同的计费方式,因此在迁移团队之前,应先查看超出免费套餐后 Tailscale 的费用。你刚刚构建的完整隧道在那里有直接对应方案,因为将 VPS 发布为 Tailscale 出口节点即可通过服务器使用相同的出站路由,只是改为在管理控制台中批准,而不是写入每个客户端的配置文件。子网限制也有对应方案,因为从 VPS 发布整个私有网络后,tailnet 中的每台设备都可以访问该网络,无需再编辑每个对等端的 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 吗?

可以。将代理放在 Web UI 端口前面,并在那里终止 TLS,同时不要设置 INSECURE。容器可以直接接受来自代理的纯 HTTP 跳转,官方 Caddy 和 Traefik 示例也完全没有设置 INSECURE 变量。由于浏览器通过 HTTPS 与代理通信,应让会话 Cookie 保留 Secure 标志;INSECURE=true 唯一会移除的就是该标志。请继续直接发布 UDP 51820,因为 VPN 流量使用 UDP,不会经过 HTTP 代理。

这篇文章对您有帮助吗?