Architecture

Cargo is deliberately small: exactly three platform containers, plus one container per deployed app. No Redis, no external dependencies — Postgres holds both platform state and the job queue.

Internet public 80 / 443 CARGO-PROXY · EXTERNAL, SHARED traefik v3 · ports 80/443 controlplane Go binary · UI · queue app containers one per app app routes (container labels) docker socket CARGO-SYSTEM · INTERNAL db Postgres 16 · state + queue db:5432 CARGO-DATA · EXTERNAL managed databases Postgres · MySQL · Mongo · Redis on attachment
Traefik is the only container publishing host ports. The db service joins only cargo-system, so it has no address on the network tenant apps live on; the controlplane is the only member of both. An app joins cargo-data only when it has a database attachment.

The three containers

Container Role
controlplane Single Go binary: chi HTTP API, embedded React SPA (served via go:embed), River job queue on Postgres, deploy engine. Mounts the Docker socket to build and run apps. The only stateful piece besides the DB.
db Postgres 16 — all platform state and the job queue. goose migrations are embedded in the binary and run automatically at startup; queries are generated with sqlc. Reachable only by the controlplane.
traefik Traefik v3 reverse proxy. The only container publishing host ports (80/443). Configured entirely via container labels on the cargo-proxy network; issues Let’s Encrypt certificates.

The platform UI itself is routed through Traefik like any app (dogfooded) — the controlplane carries the same labels Cargo generates for user apps.

CONTROLPLANE · SINGLE GO BINARY chi HTTP API :8080 · REST + SSE embedded SPA React 19 via go:embed self-metrics :9090 · Prometheus River job queue on Postgres deploy engine build · run · route periodic jobs metrics · checks · prune db · Postgres 16 state · job queue Docker Engine mounted socket sqlc · goose build · run containers
One binary serves the API and the embedded UI, runs the River queue on the same Postgres it keeps state in, and drives Docker through the mounted socket. goose migrations are embedded and run at startup; queries are generated with sqlc.

Networks

Three Docker networks keep a deployed app from ever reaching the control-plane database:

Network Scope Members
cargo-proxy External, shared, created once per host Traefik, controlplane, and every tenant app — the traffic network
cargo-system Compose-managed, internal Only controlplane ↔ platform DB. internal: true also denies the DB any outbound route
cargo-data External Managed-database instances; an app joins only when it has an attachment

The db service joins only cargo-system, so it has no address on the network tenant apps live on. You can verify this on a running stack: the controlplane reaches db:5432, a container attached to cargo-proxy cannot.

Tenant containers additionally run with memory/CPU/PID caps, no-new-privileges, and rotated logs — see Operations & Hardening.

Stack

Layer Technology
Backend Go — chi, River, goose, sqlc
Frontend React 19, Vite, TypeScript, Tailwind 4, TanStack Query, and a hand-built “freight” component set — embedded in the Go binary
State Postgres 16 (control state + job queue)
Proxy Traefik v3, label-configured
Runtime dependency Docker Engine with the compose plugin on the host
Third-party services GitHub (App + webhooks), Let’s Encrypt, optional DNS provider API, optional SMTP, optional OIDC provider

Endpoints and listeners

Listener Exposure Purpose
Traefik :80 / :443 Public The only host ports published
Traefik :8082 Internal Prometheus metrics scraped for per-app traffic charts
Controlplane :8080 Internal (routed via Traefik) API + embedded SPA
Controlplane :9090 Internal only Prometheus self-metrics (CARGO_METRICS_ADDR)
GET /healthz Public Liveness
GET /readyz Public Readiness — 200 only when Postgres and Docker are both reachable

Background work

The job queue (River, on Postgres) runs deploys plus a set of periodic jobs: metrics collection every 15s, domain and disk checks every 10 minutes, and daily prune, housekeeping, and control-plane backup jobs. The full schedule is in Operations & Hardening.

State and volumes

Volume Contents
cargo-db Postgres data
cargo-data The controlplane’s <dataDir> — databases/ (managed DB volumes), db-backups/ (snapshots), db-logs/ (provisioning logs), platform-backups/ (control-plane dumps + certificate copies), hosts/ (per-worker docker contexts and SSH homes)
cargo-acme Traefik’s certificate store (acme.json)

The production image is ghcr.io/bograh/cargo:latest — a multi-stage build (web build → Go build → alpine with the docker CLI and compose plugin).

Security & performance highlights

  • Invite-only signup by default; configurable per instance
  • Resource ceilings — CARGO_MAX_MEM_LIMIT / _CPU_LIMIT / _PIDS_LIMIT cap how far tenants can raise per-app limits
  • Git-clone SSRF screening — blocks non-https/ssh transports and private-address resolution
  • Compose validation — rejects tenant-supplied Traefik labels, external networks, and images carrying their own Traefik labels; validated against resolved config, not raw YAML
  • DB connection pool — sized explicitly (20 max / 2 min) instead of defaulting to NumCPU
  • Indexed lookups — critical query paths use covering indexes
  • SSE stream caps — per-user (8) and instance (64) limits with access revalidation

Development

cd deploy
docker compose -f docker-compose.dev.yml up --build

Serves the platform on http://localhost:8080 without Traefik/SSL.

  • Backend tests: go test ./... (integration tests run against real Postgres via testcontainers)
  • Frontend tests: npm test in web/
  • End-to-end smoke test (install → register → deploy → live): scripts/smoke.sh