Multi-container apps (stacks)¶
An Allocus app isn't limited to one container. A stack — a frontend, a backend, and a database, say — deploys and rolls back together as a single unit, at one git SHA. This is one of the things Allocus does that a "one container per app" host can't.
You describe the stack in allocus.yaml under services:. The CLI does the heavy
lifting: it builds every service that has a build: context, pulls stock images
(postgres, redis, …) straight through, and generates the Traefik routing, the
networks, and the volumes for you — you never hand-write a compose file.
The shape¶
name: shop
services:
web: # a public entrypoint
build: ./frontend # a local build context → built & pushed for you
port: 8080
expose: true # gets shop.<you>.allocus.dev + HTTPS
api:
build: ./backend # internal — reachable by other services at http://api:3000
environment:
DATABASE_URL: postgres://app:${DB_PASSWORD}@db:5432/app # ${DB_PASSWORD} = a secret
db:
image: postgres:16 # stock image → pulled directly, never built
volumes: ["pgdata:/var/lib/postgresql/data"] # named volume = survives redeploys
environment:
POSTGRES_USER: app
POSTGRES_DB: app
# POSTGRES_PASSWORD is provided by `allocus secrets` (see below) — not in git
Set the password once — it's stored encrypted, never committed — then deploy:
flowchart LR
net["internet"] -->|HTTPS| web
subgraph box["your box"]
direction LR
web["web<br/>(public)"] --> api["api<br/>(internal)"]
api --> db[("db<br/>+ volume")]
end
classDef pub fill:#4665fa,stroke:#2b3fd0,color:#fff;
class web pub;
How it maps to your box¶
webis public. It joins theedgenetwork and gets a Traefik router forshop.<you>.allocus.devwith automatic HTTPS. You can expose more than one service — see Multiple public endpoints below.- Everything else is private.
apianddbsit on an internal network, unreachable from the internet — only the other services can talk to them. - Services find each other by name. Inside the stack, the API's database host is
db, the frontend's API host isapi. Put those in your env vars (seeDATABASE_URLabove). - State lives in named volumes. Anything you want to survive a redeploy (a database's data) must be in a declared volume — containers are recreated on every deploy, so the container filesystem is scratch space.
- Every service gets a memory cap, and the default is small.
256M, deliberately, so one runaway service can't wedge the whole box. That's fine for a small web process and nowhere near enough for Postgres, a JVM, or server-side rendering — raisememory:on the services that need it, or they'll be killed with nothing useful in the logs.
A database on your box is a database with no backup
A named volume survives redeploys and resizes, and it exists in exactly one place.
Nothing on the platform copies it anywhere, and there is no restore button — see
backups are yours. Running Postgres in your stack is
supported and convenient; treating it as durable storage is your job. A pg_dump to
somewhere off the box is the usual answer, and it can be another service right here.
The rules¶
| Field | Meaning |
|---|---|
build: <path> |
A local directory with a Dockerfile. The CLI builds it for linux/amd64 and pushes it, tagged with your git SHA. |
image: <ref> |
A stock image (e.g. postgres:16). Pulled as-is on the box; not built. |
build: {context, dockerfile, args} |
Mapping form, when the Dockerfile isn't at the context root or you need build args. |
port + expose: true |
Marks a public service and the port it listens on. The root (/) service uses this. |
path: /prefix |
Exposes a service under a URL prefix on the app's host (implies public). Lets several services share one hostname. |
strip_path: true |
Removes the path prefix before forwarding, for backends that serve from /. Default: keep the prefix. |
restart: <policy> |
no | always | on-failure | unless-stopped (default). Use no for a one-shot job (a migration). |
depends_on: {svc: {condition: …}} |
Start ordering with conditions, e.g. service_completed_successfully (pair with restart: no). |
secrets: [KEY, …] |
Which secrets this service gets. Default: all. Scope it so a public frontend needn't hold the DB password; [] = none. |
cpus: / memory: |
Per-service resource limits. Default 0.5 / 256M — raise for Postgres or SSR servers. |
build_args: {…} |
Build-time args. ${SECRET} resolves only in environment: at run time, not in build args — pass literals. |
volumes: ["name:/path"] |
Named volumes for persistence. Declared automatically. |
environment: {…} |
Environment variables for that service. |
Migrations
There's no separate release phase. Run migrations as a one-shot service
(restart: "no") and gate the app on it with
depends_on: {migrate: {condition: service_completed_successfully}}.
Multiple public endpoints¶
Each app has one hostname (shop.<you>.allocus.dev) with a wildcard TLS cert one
level deep. To put several services on the internet — a frontend and an API, say —
route them by path, not by subdomain. Give each extra public service a path:;
the root service (no path) catches everything else:
name: shop
services:
web:
build: ./frontend
port: 3000
expose: true # root: shop.<you>.allocus.dev/…
api:
build: ./backend
port: 8000
path: /api # shop.<you>.allocus.dev/api/… (implies public)
# strip_path: true # forward as /… instead of /api/…
db:
image: postgres:16 # private
- A
path:implies public and must start with/. Path routers take priority over the root, so/api/...reachesapiand everything else reachesweb. strip_pathdefaults to false — your backend receives the full/api/users. Set ittrueonly if the backend serves its routes from the root (/users).- At most one public service may omit
path:(the root at/). Give every public service a path and there's no root — unmatched URLs return 404.
Paths, not subdomains
Per-service subdomains (api.shop.<you>.allocus.dev) aren't supported — the
wildcard certificate is one level deep and wouldn't cover them. Use paths.
Atomic deploys and rollback¶
The whole stack is pinned to one immutable git SHA. A deploy swaps every service together; a rollback returns every service together:
No half-updated stacks, and no rebuild on rollback.
Secrets¶
Anything sensitive — a database password, a third-party API key — goes in the
secret store, not allocus.yaml:
allocus secrets set STRIPE_KEY=sk_live_…
allocus secrets list # names only; values are never shown back
allocus secrets rm STRIPE_KEY
Secrets are encrypted at rest and delivered to your box at deploy time, written to
the app's .env (never committed, never in the deploy record). Every service in the
stack can read them as environment variables, and you can reference them as
${NAME} inside allocus.yaml (as with ${DB_PASSWORD} above). Set or change a
secret, then allocus deploy to roll it out.
Prefer Claude to write the stack? Just say "deploy this to allocus" — the skill
authors the services: block for you. Want it on every push? See GitHub
Actions.