Adds platform/lab-router/ — a narrow service that runs on the Lab Host, authenticates the Gateway via mTLS, and resolves an opaque routing token to a local container purely from Docker label state (no Postgres access, no callback to the Provisioner). Replaces the Gateway's env.internal_ip assumption (only valid when Gateway and Lab share a Docker daemon) with a data-driven direct/router decision per lab_host (lab_hosts.routing_mode/router_endpoint, migration 004), resolved in one place (platform/gateway/src/lab-endpoint.js's labTarget()) that every proxy/fetch call site in server.js now goes through. Extends the existing Docker TLS CA (infra/scripts/docker-tls/generate-docker-tls.sh) with a second mTLS leaf pair (router-server/gateway-client) for this new data-plane connection, deployed by the docker-tls Ansible role into a separate directory from the control-plane pair. Firewall (rn-lab-host-01.rules) replaces the old speculative wide port-range allowance with one narrow rule: platform_ip -> the Router's port only. dev-local stays routing_mode: direct — zero behavior change for local dev, confirmed by the full existing regression suite passing unmodified.
51 lines
2.5 KiB
Markdown
51 lines
2.5 KiB
Markdown
# Container image strategy
|
||
|
||
## Images
|
||
|
||
| Image | Built from | Registry path |
|
||
|---|---|---|
|
||
| `rootnode/platform-api` | `platform/api/Dockerfile` | `{{ registry }}/rootnode/platform-api` |
|
||
| `rootnode/gateway` | `platform/gateway/Dockerfile` | `{{ registry }}/rootnode/gateway` |
|
||
| `rootnode/provisioner` | `platform/provisioner/Dockerfile` | `{{ registry }}/rootnode/provisioner` |
|
||
| `rootnode/egress-proxy` | `platform/egress-proxy/Dockerfile` | `{{ registry }}/rootnode/egress-proxy` |
|
||
| `rootnode/lab-router` | `platform/lab-router/Dockerfile` (Phase 4.1) | `{{ registry }}/rootnode/lab-router` |
|
||
| `rootnode/lab` | root `Dockerfile` (the student Lab image) | `{{ registry }}/rootnode/lab` |
|
||
|
||
## Tagging
|
||
|
||
**Never `:latest` in staging or production.** Every build is tagged with:
|
||
|
||
- the full git SHA (`sha-<40 hex>`) — always, every build, the ground truth
|
||
- `staging-<short-sha>` on every push to `main` that passes CI (moving pointer, staging only)
|
||
- a semantic tag (`vX.Y.Z`) only on an explicit, human-triggered production release
|
||
|
||
`infra/scripts/ci/tag-and-push.sh` implements this — see §18 in
|
||
`docs/PHASE4_PRODUCTION_INFRASTRUCTURE.md`.
|
||
|
||
Deployment configs (`infra/environments/*/*.env.template`, Ansible `group_vars`) reference the
|
||
tag as a variable (`image_tag`), never a hardcoded value baked into a compose file — rolling
|
||
back is changing one variable and re-running the `platform`/`lab-host` Ansible role, not editing
|
||
a compose file by hand (see `docs/RUNBOOK_ROLLBACK.md`).
|
||
|
||
## Registry
|
||
|
||
No paid registry required. Two supported options, chosen at deploy time via `registry` in
|
||
`infra/ansible/group_vars/production.yml`:
|
||
|
||
1. **Gitea's own built-in container registry** (`git.synthossystems.com`) — zero new
|
||
infrastructure, already authenticated the same way the repo itself is. Preferred default.
|
||
2. **A self-hosted registry** (e.g. a `registry:2` container on rn-platform-01 itself, or
|
||
elsewhere in the homelab) — a documented fallback if the Gitea registry isn't enabled on
|
||
this instance.
|
||
|
||
Either way: private registry, credentials pulled the same way any other production secret is
|
||
(`docs/PRODUCTION_DEPLOYMENT.md` §10), never a public Docker Hub push of anything beyond what's
|
||
already public (this project doesn't publish anything to Docker Hub in Phase 4).
|
||
|
||
## Rollback
|
||
|
||
Every image is content-addressable by SHA tag, so "roll back" is always available as long as
|
||
the previous tag hasn't been garbage-collected from the registry — see
|
||
`docs/RUNBOOK_ROLLBACK.md` for the exact procedure (re-point `image_tag`, re-run the Ansible
|
||
role, verify).
|