furtka-gateway/examples/starter-stack
Robert Syrnicki e240f071d5
Some checks failed
Build control-plane image / build-image (push) Has been cancelled
feat: ship control-plane standalone, bring your own Traefik/wg-easy
Restructure the deployment model: instead of bundling and owning its own
Traefik + wg-easy, the control-plane now plugs into a Traefik and wg-easy
the operator already runs (as sibling containers, or otherwise reachable).
This avoids duplicating infra self-hosters commonly already have, and
sidesteps having to run a second ACME/DNS setup entirely — the operator's
own Traefik already has a working cert resolver.

- New control_plane/wgeasy_v15.py: a wg-easy v15 REST client, verified live
  against a real running container (auth model, route shapes, and the new
  per-client PresharedKey all differ materially from v14's API). Threaded
  the preshared key through to the box side (furtka/wireguard.py in the
  furtka repo) so v15's handshake actually completes.
- GATEWAY_CERT_RESOLVER is now a real env var instead of a GATEWAY_MODE-
  derived hardcoded "le", so it can mirror whatever resolver name (if any)
  the operator's own Traefik already has configured.
- Root docker-compose.yaml/ops/deploy.sh: control-plane only now. deploy.sh
  asks how the operator's Traefik reaches it (same docker network, joined
  via `docker network connect`, or a separate host) instead of prompting
  for DNS-01 providers and port remaps single-parallel mode needed.
- single-parallel mode is retired outright — nothing needs it once
  furtka-gateway doesn't own a second Traefik/wg-easy to remap ports for.
- The old bundled stack (own Traefik+wg-easy, HTTP-01) moves to
  examples/starter-stack/ as the documented zero-infra on-ramp.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3jVwAqESRjPdW9P9nvfem
2026-09-19 13:34:29 +02:00
..
ops feat: ship control-plane standalone, bring your own Traefik/wg-easy 2026-09-19 13:34:29 +02:00
traefik feat: ship control-plane standalone, bring your own Traefik/wg-easy 2026-09-19 13:34:29 +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: ship control-plane standalone, bring your own Traefik/wg-easy 2026-09-19 13:34:29 +02:00
docker-compose.yaml feat: ship control-plane standalone, bring your own Traefik/wg-easy 2026-09-19 13:34:29 +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.