Case study·Public·Live
Cargo
Self-hosted Platform-as-a-Service
- Platform containers
- 3
- Deploy strategy
- Blue/green
- Job queue
- River on Postgres
- Roles
- Owner · admin · member · viewer
Cargo is a self-hosted Platform-as-a-Service in the spirit of Dokploy and Coolify: a Vercel-like deploy experience on a server you own. Install it with one command, and anyone on the team can take an app from a GitHub repo or a registry image to a live HTTPS URL without touching SSH, YAML, or proxy config.
Three containers, one binary
The whole platform is three containers, plus one per deployed app. The controlplane is a single Go binary that serves the API and the embedded React UI, runs the job queue, and owns the deploy engine. db is Postgres 16, holding all platform state and the job queue, with migrations run at startup. traefik is the reverse proxy and issues Let's Encrypt certificates.
Putting the job queue in Postgres (via River) was the decision that kept it at three. There's no separate broker to deploy, back up, or keep in step with the database, and a job can be enqueued in the same transaction as the state change it belongs to. One backup covers both.
Deploys
Apps come from a GitHub repo (Dockerfile or Nixpacks, auto-detected), a plain registry image, or a Docker Compose file in the repo. Every app gets https://<app>.<apps-domain> with automatic SSL, custom domains, live build logs, encrypted environment variables, and one-click rollback. Push-to-deploy comes in through a GitHub App webhook.
By default a deploy is blue/green. The new version starts alongside the running one and only takes traffic once it passes its healthcheck. A version that never gets healthy is thrown away and the old one keeps serving, so a bad deploy doesn't need a rollback at all. Apps that can't tolerate two instances running at once (a singleton worker, say) can switch to recreate.
The security boundary
The control plane mounts the Docker socket read-write, and write access to that socket is root on the host. There's no way around that for a PaaS, so Cargo defends the boundary below it: everything a tenant controls. Compose files, build paths, container labels, repository URLs, and resource caps are all validated, not trusted.
- Repository URLs reach
git cloneon the control plane's own network, so onlyhttps://,ssh://andgit@host:pathare accepted, and hosts that resolve to loopback, RFC1918, or link-local addresses (the cloud metadata endpoint among them) are refused. - Apps run on
cargo-proxywithno-new-privilegesand memory/CPU/PID caps. The platform database lives oncargo-system, which apps can't reach. - Secrets are sealed with a master key, and
cargod rotate-keyre-seals every one of them under a new key in a single transaction. - An account can deploy containers on the host, so sign-up is invite-only by default.
I wrote the reasoning up in more detail in The Docker socket is root.
Running it for real
Most of the work after “it deploys” went into making it safe to leave running:
- A daily
pg_dump -Fcof the control-plane database plus the TLS certificates, keeping the last 14, with a restore runbook. - A disk guardrail that warns below 10% free and reclaims dangling images below 5%.
- Alerts to a Slack/Discord webhook on deploy failures, low disk, and failed backups.
/readyzchecking Postgres and Docker, and a Prometheus endpoint for deploy, queue, and DB-pool metrics on an internal-only listener.- An audit log of every state-changing action, retained for 180 days.
More than one server
Worker hosts (early access) let an admin register any machine reachable over SSH that runs Docker. Cargo seals the SSH key with the master key, pins the host fingerprint on first contact (a later mismatch aborts every operation), and gives each host its own Docker context rather than touching the control plane's ~/.ssh or ~/.docker. If a worker goes away mid-deploy, that deploy fails cleanly and other hosts carry on.
A risk I kept on purpose
The control-plane process runs as root inside its container. With a read-write Docker socket already mounted, dropping to a non-root user buys nothing an attacker couldn't undo through the socket in one command. It would also need a data-directory ownership migration on every existing install. So it's written down in the threat model as an accepted risk, not left for someone to discover.