Reference
Architecture
Astrodock is one docker compose stack: five services that together route, run,
authenticate, store, and monitor your apps on a single box. This page shows how the pieces fit
and how a request flows through them.
The stack at a glance
┌──────────────── host / VPS ────────────────┐
*.<base-domain> ─TLS──▶ │ caddy reverse proxy + auto-HTTPS │
(the only public ports) │ │ serves static frontends (vol) │
│ │ proxies /api/* and Docker apps │
│ ▼ │
│ api control plane (Express) │
│ │ admin API · /verify · webhooks │
│ │ health monitor · orchestration │
│ ▼ (internal, token-authed) │
│ runner clones, builds, runs apps │
│ │ holds the Docker socket + GitHub PAT │
│ ├─ PM2 ───▶ node app <slug> (buildpack) │
│ └─ docker ▶ app-<slug> container (Dockerfile) │
│ │
│ postgres bundled DB (control plane + apps)│
│ objectstore bundled S3-compatible storage │
└───────────────────────────────────────────────┘
volumes: pgdata · objectdata · static (builds) · apps (server code) · repos (clone cache)
network: one bridge net; Caddy resolves app containers by name; runner reachable as "runner"
What each service does
| Service | Role |
|---|---|
caddy |
Reverse proxy and automatic HTTPS. The only thing on ports 80/443.
Serves built static frontends from the shared static volume, proxies
/api/* to Node apps, and whole-proxies Dockerfile-app subdomains. |
api |
The control plane (Express). Hosts the admin API and dashboard, the /verify
end-user auth endpoint, GitHub webhooks, the health monitor, and deploy orchestration.
Talks to postgres. Holds no Docker socket and
no GitHub PAT. |
runner |
Clones, builds, and runs your apps. Runs Node buildpack apps as per-app non-root OS users via PM2 (inside the runner), and Dockerfile apps as sibling containers. Holds the Docker socket and the GitHub PAT. Reachable only on the internal network, token-authed. |
postgres |
The bundled database — both the control-plane store and the
internal per-app database option. |
objectstore |
The bundled S3-compatible object storage (SeaweedFS) — the internal per-app
storage option. Internal network only. |
How a request flows
Everything from the outside world enters through Caddy. Where it goes next depends on the host and the path:
- Loading an app's frontend:
browser → caddy → static files from the shared
staticvolume (for Node buildpack apps), or → the app's own container (for Dockerfile apps). - An app's API call:
browser → caddy →
/api/*proxied to the running app. - Using the dashboard:
the dashboard host → caddy →
api. - An app checking a login:
the app →
/verifyon the control plane, over the internal network (your app never needs to be public to verify a user). - A push to GitHub:
GitHub → webhook →
api→ tells therunnerto deploy.
The api / runner split (and why)
The api and runner are separate containers. This is
deliberate, and it's the platform's main internal trust boundary.
The runner is the powerful, dangerous part: it holds the Docker socket (which
is roughly root on the host) and the GitHub PAT, and it executes code from your
repos when it builds and runs apps. The control plane — the thing that serves the admin API and
/verify — holds neither the socket nor the PAT. Keeping the build/exec
surface in its own container isolates that risk away from the API.
The two talk over the internal compose network only, and the runner's internal API is
token-authed with ASTRODOCK_RUNNER_TOKEN. Nothing outside the box reaches the
runner directly.
Two roles, one image
The api and runner are built from the same image, started in
different roles (ASTRODOCK_ROLE). What differs is what each one is handed: the
runner gets the socket, the PAT, and the build volumes; the api gets neither.
Where data lives
State is kept in named Docker volumes, so it survives restarts and upgrades:
| Volume | What's in it |
|---|---|
pgdata | Postgres data — control-plane records and internal app databases. |
objectdata | The bundled object store's contents (internal app buckets). |
static | Built frontends (and the admin SPA), served by Caddy. |
apps | Server code for running Node buildpack apps. |
repos | The runner's clone cache for connected repos. |
Back these up
For a real install, the volumes that hold your data are pgdata and
objectdata. See Backups & restore.
The deploy & provision flow
At a high level, a deploy goes:
-
Trigger
A webhook from a
git push(viaapi), orastrodock deploy. A deploy record is created so progress is observable. -
Provision & gate
Resources are ensured (an internal Postgres DB + role, or an internal storage bucket + scoped key), and the required-variable gate runs — if any required secret or external credential is missing, the deploy is rejected before any code runs.
-
Build
The runner clones the repo, then builds. Node apps: install deps and run the
buildCommand; the frontend'sdist/is synced to thestaticvolume. Dockerfile apps:docker buildan image. -
Run
Node apps start as a PM2 process bound to
ASTRODOCK_PORT; Dockerfile apps run as a sibling container on the compose network. Caddy routes the subdomain accordingly. -
Health probe
The runner probes the app (a
GET /health200 means healthy) and marks the deploysuccessorfailed, with the full streamed log attached.
The injected environment for the app process is the reserved ASTRODOCK_* set
(computed from the resource modes) plus the app's declared vars — the platform's own stack
secrets are never injected into an app.
Related
- The full technical contract — manifest, env model, runner internals: Platform Design Spec
- The trust model and protections: Security