furtka-gateway/examples/starter-stack
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
..
.env.example feat: ship control-plane standalone, bring your own Traefik/wg-easy 2026-09-19 13:34:29 +02:00
deploy.sh feat: rebuild the new-server stack on wg-easy v15 with a shared netns 2026-10-07 15:49:49 +02:00
docker-compose.yaml feat: rebuild the new-server stack on wg-easy v15 with a shared netns 2026-10-07 15:49:49 +02:00
README.md feat: ship control-plane standalone, bring your own Traefik/wg-easy 2026-09-19 13:34:29 +02:00

furtka-gateway starter stack

Zero-infra on-ramp: bundles its own Traefik + wg-easy alongside the control-plane, for an operator who doesn't already run either. This is a secondary path — if you already have Traefik and/or wg-easy running (a common self-hosting setup), use the main repo's root docker-compose.yaml

  • ops/deploy.sh instead, which plugs into what you already have rather than running a second copy. See the root README.md's Prerequisites checklist.

This stack always runs in GATEWAY_MODE=single, owns ports 80/tcp, 443/tcp, and 51820/udp on the host directly, and uses HTTP-01 certs (no DNS provider needed) — the tradeoffs that make single-parallel mode necessary elsewhere don't apply here, because this stack assumes nothing else is already using those ports.

Deploying on a VPS

  1. Get a VPS with a static public IP, and point two A/AAAA records at it: one for GATEWAY_PUBLIC_HOST (the WireGuard endpoint) and one for GATEWAY_CONTROL_PLANE_HOST (the registration API — needs its own hostname since the response carries a WireGuard private key in plaintext JSON, so that call must go over HTTPS).
  2. Run ./deploy.sh from this directory and answer its prompts. It generates the wg-easy admin password hash, GATEWAY_BOX_TOKEN, and GATEWAY_ADMIN_TOKEN, writes .env, renders traefik/static/traefik.active.yml and traefik/dynamic/control-plane.yml from their tracked templates, runs sudo ops/host-sysctls.sh (wg-easy needs net.ipv4.ip_forward=1 and net.ipv4.conf.all.src_valid_mark — see that script's comments for why Docker's own sysctls: key can't set these under network_mode: host), brings the stack up, runs sudo ops/firewall.sh (locks wg-easy's admin API off the public interface — network_mode: host means Docker itself can no longer do that automatically), and health-checks it.
  3. Open 80/tcp, 443/tcp, 51820/udp in whatever firewall/security group sits in front of the VPS.

Run ./deploy.sh --render-only to just (re)write .env and the rendered Traefik files without touching sysctls/compose/firewall — useful to inspect what would be generated, or after hand-editing .env.

Local dev

./deploy.sh --render-only
# GATEWAY_PUBLIC_HOST can be a LAN-reachable IP for a first smoke test.
docker compose up -d
curl http://127.0.0.1:8090/healthz

ops/firewall.sh and the control-plane.yml Traefik route are about reaching this gateway from the real internet — skip both for local dev.

Docker Swarm caveats

If deploying via docker stack deploy instead of docker compose up:

  • docker-compose.yaml's build: ../../control_plane doesn't work under Swarm at all — it only ever schedules pre-built, tagged images. Swarm falls back to the image: key on the same service instead, which CI keeps published to the registry on every push to master (tagged latest plus the short commit sha). Set GATEWAY_CONTROL_PLANE_IMAGE in .env first if you want a specific commit-sha tag rather than whatever latest currently points to.
  • docker stack deploy does not read a .env file next to the compose file the way docker compose up does — set -a; source .env; set +a first, or the ${VAR} substitutions come out empty. Do not source .env directly if WG_EASY_PASSWORD_HASH is set — bash expands $$ (its own PID) while sourcing, silently corrupting the hash's doubled-$ escaping. Export that one variable separately, unescaped, or set it directly on the docker stack deploy command line.
  • depends_on is silently ignored by Swarm — harmless here, since control-plane only talks to wg-easy/Traefik lazily on the first real request, not at startup.