# 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 ```bash ./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.