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> |
||
|---|---|---|
| .. | ||
| .env.example | ||
| deploy.sh | ||
| docker-compose.yaml | ||
| README.md | ||
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.shinstead, which plugs into what you already have rather than running a second copy. See the rootREADME.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
- 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 forGATEWAY_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). - Run
./deploy.shfrom this directory and answer its prompts. It generates the wg-easy admin password hash,GATEWAY_BOX_TOKEN, andGATEWAY_ADMIN_TOKEN, writes.env, renderstraefik/static/traefik.active.ymlandtraefik/dynamic/control-plane.ymlfrom their tracked templates, runssudo ops/host-sysctls.sh(wg-easy needsnet.ipv4.ip_forward=1andnet.ipv4.conf.all.src_valid_mark— see that script's comments for why Docker's ownsysctls:key can't set these undernetwork_mode: host), brings the stack up, runssudo ops/firewall.sh(locks wg-easy's admin API off the public interface —network_mode: hostmeans Docker itself can no longer do that automatically), and health-checks it. - Open
80/tcp,443/tcp,51820/udpin 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'sbuild: ../../control_planedoesn't work under Swarm at all — it only ever schedules pre-built, tagged images. Swarm falls back to theimage:key on the same service instead, which CI keeps published to the registry on every push tomaster(taggedlatestplus the short commit sha). SetGATEWAY_CONTROL_PLANE_IMAGEin.envfirst if you want a specific commit-sha tag rather than whateverlatestcurrently points to.docker stack deploydoes not read a.envfile next to the compose file the waydocker compose updoes —set -a; source .env; set +afirst, or the${VAR}substitutions come out empty. Do notsource.envdirectly ifWG_EASY_PASSWORD_HASHis 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 thedocker stack deploycommand line.depends_onis silently ignored by Swarm — harmless here, sincecontrol-planeonly talks to wg-easy/Traefik lazily on the first real request, not at startup.