furtka-gateway/examples/starter-stack/docker-compose.yaml
Robert Syrnicki 53ff30f273 feat: rebuild the new-server stack on wg-easy v15 with a shared netns
wg-easy and Traefik now share one network namespace owned by a tiny
gateway-net container, so Traefik reaches box tunnel addresses directly.
This drops host networking, the host sysctl and firewall scripts, and
keeps wg-easy's admin UI on loopback. wg-easy v15 is set up unattended
via INIT_* variables, and deploy.sh needs only domain, hostnames and an
ACME email; secrets are generated.

Verified locally with a simulated box: register, tunnel, publish, and
restarts of wg-easy, Traefik and gateway-net.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:49:49 +02:00

133 lines
4.6 KiB
YAML

# furtka-gateway on an empty server: wg-easy + Traefik + control-plane.
# Configure via .env (deploy.sh writes it — see README.md in this folder).
#
# The one trick: wg-easy and Traefik share one network namespace, owned
# by the tiny `gateway-net` container. wg0 and its 10.8.0.0/24 route live
# in that namespace, so Traefik has a direct path to every box's tunnel
# address — no host routes, NAT rules, host sysctls, or host-networked
# wg-easy needed. `gateway-net` does nothing else and never restarts, so
# wg-easy and Traefik can each restart without cutting the other off
# (a namespace owned by wg-easy itself would vanish on every wg-easy
# restart, leaving Traefik with no network at all).
services:
gateway-net:
image: alpine:3.20
container_name: furtka-gateway-net
command: ["sleep", "infinity"]
init: true
ports:
# WireGuard
- "51820:51820/udp"
# Traefik
- "80:80"
- "443:443"
# wg-easy admin UI, loopback only: ssh -L 51821:127.0.0.1:51821 <server>
- "127.0.0.1:51821:51821"
sysctls:
- net.ipv4.ip_forward=1
- net.ipv4.conf.all.src_valid_mark=1
networks:
- internal
restart: unless-stopped
wg-easy:
image: ghcr.io/wg-easy/wg-easy:15
container_name: furtka-gateway-wg-easy
environment:
# Unattended first-run setup (only applied while wg-easy's database
# is still empty — change the password later in its UI, then update
# WG_EASY_ADMIN_PASSWORD in .env to match).
- INIT_ENABLED=true
- INIT_USERNAME=admin
- INIT_PASSWORD=${WG_EASY_ADMIN_PASSWORD}
- INIT_HOST=${GATEWAY_PUBLIC_HOST}
- INIT_PORT=51820
# Split tunnel: boxes only route the gateway's peer subnet through
# the tunnel, never their whole internet traffic.
- INIT_ALLOWED_IPS=10.8.0.0/24
- DISABLE_IPV6=true
# The admin UI is only published on the server's loopback (see
# gateway-net) and reached over an SSH tunnel, so plain HTTP is fine.
- INSECURE=true
volumes:
- wg_easy_data:/etc/wireguard
- /lib/modules:/lib/modules:ro
network_mode: service:gateway-net
cap_add:
- NET_ADMIN
- SYS_MODULE
depends_on:
gateway-net:
condition: service_started
restart: true
restart: unless-stopped
traefik:
image: traefik:v3.7
container_name: furtka-gateway-traefik
network_mode: service:gateway-net
command:
- --entrypoints.web.address=:80
- --entrypoints.web.http.redirections.entrypoint.to=websecure
- --entrypoints.web.http.redirections.entrypoint.scheme=https
- --entrypoints.websecure.address=:443
- --providers.file.directory=/dynamic
- --providers.file.watch=true
- --certificatesresolvers.le.acme.email=${GATEWAY_ACME_EMAIL}
- --certificatesresolvers.le.acme.storage=/acme/acme.json
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=web
- --log.level=INFO
volumes:
# control-plane.yml (written by deploy.sh) + one route-*.yml per
# published app (written by the control-plane).
- ./dynamic:/dynamic:ro
- traefik_acme:/acme
depends_on:
gateway-net:
condition: service_started
restart: true
restart: unless-stopped
control-plane:
build: ../../control_plane
# Always build from this checkout — never run a registry image that may
# be older than the code you just pulled. (The image name only matters
# for Docker Swarm, which can't build; see README.md.)
pull_policy: build
image: ${GATEWAY_CONTROL_PLANE_IMAGE:-forgejo.sourcegate.online/robert/furtka-gateway-control-plane:latest}
container_name: furtka-gateway-control-plane
environment:
- GATEWAY_MODE=single
- GATEWAY_STATE_DIR=/data
- GATEWAY_DYNAMIC_DIR=/dynamic
- GATEWAY_ADMIN_TOKEN=${GATEWAY_ADMIN_TOKEN}
- GATEWAY_BOX_TOKEN=${GATEWAY_BOX_TOKEN}
- GATEWAY_CERT_RESOLVER=le
- WG_EASY_URL=http://gateway-net:51821
- WG_EASY_API_VERSION=15
- WG_EASY_ADMIN_USERNAME=admin
- WG_EASY_ADMIN_PASSWORD=${WG_EASY_ADMIN_PASSWORD}
- GATEWAY_WG_ALLOWED_IPS=10.8.0.0/24
volumes:
- control_plane_data:/data
- ./dynamic:/dynamic
ports:
# Loopback-only, for health checks from the server itself. Boxes
# reach the API through Traefik over HTTPS (dynamic/control-plane.yml).
- "127.0.0.1:8090:8090"
networks:
- internal
depends_on:
# The control-plane configures wg-easy on startup.
wg-easy:
condition: service_healthy
restart: unless-stopped
networks:
internal:
volumes:
wg_easy_data:
traefik_acme:
control_plane_data: