# 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 - "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: