Skip to content

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:

allocus secrets set DB_PASSWORD=$(openssl rand -hex 16)
allocus 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

  • web is public. It joins the edge network and gets a Traefik router for shop.<you>.allocus.dev with automatic HTTPS. You can expose more than one service — see Multiple public endpoints below.
  • Everything else is private. api and db sit 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 is api. Put those in your env vars (see DATABASE_URL above).
  • 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 — raise memory: 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/... reaches api and everything else reaches web.
  • strip_path defaults to false — your backend receives the full /api/users. Set it true only 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:

allocus rollback <sha>

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.