2026-08-24 11:50:25 +02:00
|
|
|
services:
|
|
|
|
|
wg-easy:
|
|
|
|
|
image: ghcr.io/wg-easy/wg-easy:14
|
|
|
|
|
container_name: furtka-gateway-wg-easy
|
2026-08-24 12:51:22 +02:00
|
|
|
# Host networking, not the "internal" bridge network: wg-quick creates
|
|
|
|
|
# the wg0 interface and its `10.8.0.0/24 dev wg0` route only inside
|
|
|
|
|
# whatever network namespace this container runs in. On the "internal"
|
|
|
|
|
# bridge network that route existed only inside wg-easy's own isolated
|
|
|
|
|
# namespace, so sibling containers (Traefik, in particular) had no path
|
|
|
|
|
# to reach a WireGuard peer's address at all. With network_mode: host,
|
|
|
|
|
# wg0 and its routes live in the VPS host's own namespace, which the
|
|
|
|
|
# host can then route to/from its docker bridge interfaces normally
|
|
|
|
|
# (given net.ipv4.ip_forward=1 — see ops/host-sysctls.sh; compose's own
|
|
|
|
|
# `sysctls:` key can't set this here at all, see that script's comment).
|
|
|
|
|
network_mode: host
|
2026-08-24 11:50:25 +02:00
|
|
|
environment:
|
|
|
|
|
- WG_HOST=${GATEWAY_PUBLIC_HOST}
|
|
|
|
|
- PASSWORD_HASH=${WG_EASY_PASSWORD_HASH}
|
|
|
|
|
- PORT=51821
|
|
|
|
|
- WG_PORT=51820
|
2026-08-24 12:51:22 +02:00
|
|
|
# wg-easy's session/CSRF handling assumes HTTPS by default; we talk to
|
|
|
|
|
# it over plain HTTP from control-plane (see WG_EASY_URL below), which
|
|
|
|
|
# is fine precisely because this port is never meant to be reachable
|
|
|
|
|
# from the public internet — see the ops/firewall.sh note.
|
|
|
|
|
- INSECURE=true
|
|
|
|
|
# Default is "0.0.0.0/0, ::/0" — a full-tunnel config that would route
|
|
|
|
|
# every box's entire internet traffic through this gateway. We only
|
|
|
|
|
# want boxes reachable *from* the gateway for proxying, not routed
|
|
|
|
|
# *through* it — split-tunnel, restricted to wg-easy's own peer
|
|
|
|
|
# subnet (confirmed via config.js: WG_DEFAULT_ADDRESS defaults to
|
|
|
|
|
# 10.8.0.x, i.e. this same /24).
|
|
|
|
|
- WG_ALLOWED_IPS=10.8.0.0/24
|
|
|
|
|
# IPv6 isn't handled anywhere else in this repo yet (routes.py/
|
|
|
|
|
# traefikconf.py assume IPv4 peer addresses, ops/firewall.sh is
|
|
|
|
|
# IPv4-only) — turn it off here too rather than leave a half-wired
|
|
|
|
|
# IPv6 tunnel nothing else accounts for.
|
|
|
|
|
- DISABLE_IPV6=true
|
2026-08-24 11:50:25 +02:00
|
|
|
volumes:
|
|
|
|
|
- wg_easy_data:/etc/wireguard
|
|
|
|
|
cap_add:
|
|
|
|
|
- NET_ADMIN
|
|
|
|
|
- SYS_MODULE
|
|
|
|
|
restart: unless-stopped
|
2026-08-24 12:51:22 +02:00
|
|
|
# network_mode: host means wg-easy's admin UI/API (port 51821) binds
|
|
|
|
|
# directly on the VPS's interfaces, including the public one — unlike
|
|
|
|
|
# the old bridge-network setup, Docker itself can no longer keep this
|
|
|
|
|
# off the internet. We can't fix this by binding wg-easy to a single
|
|
|
|
|
# private address either (it would then also refuse the control-plane
|
|
|
|
|
# container's own connections, which arrive via the docker bridge
|
|
|
|
|
# interface, not loopback). The actual fix is a host firewall rule —
|
|
|
|
|
# see ops/firewall.sh. RUN THAT SCRIPT (or an equivalent rule) BEFORE
|
|
|
|
|
# this is reachable from the internet.
|
2026-08-24 11:50:25 +02:00
|
|
|
|
|
|
|
|
traefik:
|
|
|
|
|
image: traefik:v3.1
|
|
|
|
|
container_name: furtka-gateway-traefik
|
|
|
|
|
command:
|
|
|
|
|
- --configFile=/etc/traefik/traefik.yml
|
|
|
|
|
volumes:
|
|
|
|
|
- ./traefik/static/traefik.${GATEWAY_MODE:-single}.yml:/etc/traefik/traefik.yml:ro
|
|
|
|
|
- ./traefik/dynamic:/etc/traefik/dynamic:ro
|
|
|
|
|
- traefik_acme:/acme
|
|
|
|
|
ports:
|
|
|
|
|
- "80:80"
|
|
|
|
|
- "443:443"
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
networks:
|
|
|
|
|
- internal
|
|
|
|
|
# Traefik's own dashboard/API is disabled in both static configs
|
|
|
|
|
# (api.dashboard: false, api.insecure: false) — never reachable at all,
|
|
|
|
|
# let alone publicly.
|
|
|
|
|
|
|
|
|
|
control-plane:
|
|
|
|
|
build: ./control_plane
|
|
|
|
|
container_name: furtka-gateway-control-plane
|
|
|
|
|
environment:
|
|
|
|
|
- GATEWAY_MODE=${GATEWAY_MODE:-single}
|
|
|
|
|
- GATEWAY_STATE_DIR=/data
|
|
|
|
|
- GATEWAY_DYNAMIC_DIR=/dynamic
|
|
|
|
|
- GATEWAY_ADMIN_TOKEN=${GATEWAY_ADMIN_TOKEN}
|
|
|
|
|
- GATEWAY_BOX_TOKEN=${GATEWAY_BOX_TOKEN}
|
2026-08-24 12:51:22 +02:00
|
|
|
# wg-easy is host-networked now (see above), so it's no longer a
|
|
|
|
|
# fellow member of the "internal" bridge network reachable by service
|
|
|
|
|
# name — host.docker.internal (mapped below) reaches the VPS host
|
|
|
|
|
# itself, where wg-easy's port 51821 is listening.
|
|
|
|
|
- WG_EASY_URL=http://host.docker.internal:51821
|
|
|
|
|
# This build of wg-easy (ghcr.io/wg-easy/wg-easy:14 — a legacy
|
|
|
|
|
# Express-based codebase, confirmed by reading the actual container's
|
|
|
|
|
# /app/lib/Server.js, NOT the newer rewrite wg-easy's GitHub `master`
|
|
|
|
|
# branch and docs describe) has no username/session-login API at
|
|
|
|
|
# all — every request just carries this plaintext password as a bare
|
|
|
|
|
# `Authorization` header, checked with bcrypt against
|
|
|
|
|
# WG_EASY_PASSWORD_HASH above.
|
2026-08-24 11:50:25 +02:00
|
|
|
- WG_EASY_ADMIN_PASSWORD=${WG_EASY_ADMIN_PASSWORD}
|
2026-08-24 12:51:22 +02:00
|
|
|
extra_hosts:
|
|
|
|
|
- "host.docker.internal:host-gateway"
|
2026-08-24 11:50:25 +02:00
|
|
|
volumes:
|
|
|
|
|
- control_plane_data:/data
|
|
|
|
|
- ./traefik/dynamic:/dynamic
|
|
|
|
|
ports:
|
2026-08-24 12:51:22 +02:00
|
|
|
# Loopback-only convenience for local health checks/debugging
|
|
|
|
|
# (`curl 127.0.0.1:8090/healthz` from the VPS itself). This is NOT
|
|
|
|
|
# how a remote Furtka box reaches the registration API — that goes
|
|
|
|
|
# through Traefik over HTTPS; see traefik/dynamic/control-plane.yml.example.
|
2026-08-24 11:50:25 +02:00
|
|
|
- "127.0.0.1:8090:8090"
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
networks:
|
|
|
|
|
- internal
|
|
|
|
|
depends_on:
|
|
|
|
|
- traefik
|
|
|
|
|
|
|
|
|
|
networks:
|
|
|
|
|
internal:
|
|
|
|
|
driver: bridge
|
2026-08-24 12:51:22 +02:00
|
|
|
# Pinned (rather than Docker's usual dynamic allocation) so
|
|
|
|
|
# ops/firewall.sh has a stable subnet to reference when it allows this
|
|
|
|
|
# network through to wg-easy's admin API but blocks everyone else.
|
|
|
|
|
ipam:
|
|
|
|
|
config:
|
|
|
|
|
- subnet: 172.28.0.0/24
|
2026-08-24 11:50:25 +02:00
|
|
|
|
|
|
|
|
volumes:
|
|
|
|
|
wg_easy_data:
|
|
|
|
|
traefik_acme:
|
|
|
|
|
control_plane_data:
|