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.
2.5 KiB
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 tomainthat 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:
- Gitea's own built-in container registry (
git.synthossystems.com) — zero new infrastructure, already authenticated the same way the repo itself is. Preferred default. - A self-hosted registry (e.g. a
registry:2container 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).