DocsStacks & databases
Compose stacks
Deploy a docker-compose file from a repo as one multi-service stack, complete with a domain per service.
- Source
- A Git repo (compose file) or a template
- Runs as
- docker compose project aoox-<slug>
- Status
- idle → deploying → running | stopped | error
Application, compose stack, or template?
A compose stack trades some application features (health checks, blue/green, rollback, metrics) for the flexibility of running a compose file as-is — including any build:, networks, and volumes defined inside it.
Creating a stack
Project page → New compose stack
Field Notes Name A unique slug; the compose project name = aoox-<slug> (its own group in Docker Desktop). Git repository / Branch / Git credential Same as an application. Compose file Path relative to the repo root, defaulting to docker-compose.yml. Relative paths inside the file are resolved from that file's folder.Environment variables Supports ${{vars.KEY}}(from the stack's linked variable set environments, in order; the stack's own env wins) and${{database.<slug>.url}}.Click Deploy
- 01helpera one-off docker:29-cli container (compose + git)
- 02clonegit clone --depth 1 into a checkout volume
- 03env.aoox.env written from the resolved env
- 04updocker compose config → up -d --build --remove-orphans
The helper's output streams into the Last action log panel (refreshing every 3 seconds while
deploying). Git tokens and database passwords are redacted.Verify
The stack page shows the containers compose created and their status. A stack status of
runningmeansup -dfinished without error — it doesn't mean every service is healthy; check a service's logs withdocker compose logsin the terminal.
Env & interpolation
.aoox.env is used as the --env-file: its values become the source for ${VAR} interpolation in the compose file, they are not automatically injected into containers. Pass them through explicitly via environment: on the service.DATABASE_URL=${{database.app-db.url}}
APP_SECRET=secretservices:
web:
build: .
environment:
DATABASE_URL: ${DATABASE_URL} # from the stack env
APP_SECRET: ${APP_SECRET}
worker:
build: .
command: node worker.js
environment:
DATABASE_URL: ${DATABASE_URL}- An aoox managed database is reachable from the stack because the service joins the
aooxnetwork whenever there's a domain (see below); without a domain, addnetworks: [default, aoox]yourself plus an external network declaration.
services:
web:
networks: [default, aoox]
networks:
aoox:
external: trueAccess: domain or IP & port
The Access card on the stack page has two kinds of rows for each service you want reachable from outside:
| Type | What you fill in | Result |
|---|---|---|
| Domain | Service, container port, host, HTTPS toggle | A Traefik label in the override file; requires a reverse proxy. |
| IP & port | Service, container port, host port | A ports: line in the override file — the stack can then be opened at http://<server-ip>:<host port> without a domain. Each port has a Listens on choice: Public (default) or This server only, which writes 127.0.0.1:<host port>:<port>. Docker-published ports are not filtered by ufw. |
- A port clash with an application, database, another stack, or any container that's currently running is rejected when you save — not at
uptime, so an existing stack is never torn down for nothing. - Access changes only take effect after a redeploy; the panel shows a reminder.
- Deploying from a template also accepts host ports directly in the dialog.
Domain per service
Domain card on the stack page
Pick a Service, the Port inside the container, a Hostname, and toggle HTTPS.
Redeploy
On deploy, aoox writes an override file
docker-compose.aoox.ymlnext to the compose file, containing the Traefik label (router<slug>-<service>-<port>) and theaoox+defaultnetworks, then runs-f compose -f override.
- New domain changes only take effect after a redeploy.
- Stop/start use the override already sitting in the volume, so they stay consistent with the running stack.
- With no domain, there's no override — the compose file runs as-is.
Operations
| Action | What runs | Notes |
|---|---|---|
| Deploy | up -d --build --remove-orphans | Re-clones the branch, builds the image if there's a build:. |
| Stop | stop | Containers stop, volumes are kept. |
| Start | start | No clone/build. |
| Delete | down --volumes --remove-orphans + delete the checkout volume | The stack's volume data is deleted too. |
History, webhooks & metrics
Every time the compose runner runs — Deploy, Stop, Start, Delete, or via a webhook — it's recorded as one history row (action, trigger, logs, commit). A stack is N containers, so this is a history of actions, not an image/rollback history like an application's Deployment.
- Auto-deploy on push: enable the Webhook on the Deploy tab to get a URL containing a token. The contract is the same as an application's webhook — an optional GitHub/GitLab signature, and irrelevant pushes still get a 200 so the provider doesn't disable the hook. Stacks from a template have no repo, so pushes are accepted but ignored.
- Metrics & container-down notifications now work — stack containers are recognized by the compose project name (
aoox-<slug>), not an aoox component label. Notifications stay quiet while a run is in progress, sinceup -d --buildstops and restarts every container. - Resource limits still can't be set from the UI — use
deploy.resourcesin the compose file.
Behavior & limitations
build:is fully supported — the compose CLI uses BuildKit (unlike Nixpacks applications).- No rollback or health check/blue-green per stack — Deploy always runs
up -d --buildagain, rather than swapping images like an application does. - No mounts from the UI for stacks — configure volumes in the compose file.
- Jobs are available under the stack's Jobs tab: pick a Service, then exec inside that service's container or a separate container from its image — see Scheduled jobs.
- Databases inside a stack don't get aoox's backup features — for important data, use a managed database.
- A compose file from a repo may request
privileged/ host mounts — treat the repo like code running on the host. - A failed deploy sends a deployment-failure notification.
Common pitfalls
| Symptom | Cause & fix |
|---|---|
| A service doesn't see the env | The stack env is only for interpolation. Add environment: on the service. |
| Bind mount ./file fails: not a directory | Already handled (checkout happens at the volume's mountpoint). If it still fails, relative paths are resolved from the compose file's folder, not the repo root. |
| Services can't find each other after adding a domain | The override already includes the default network. If your compose file declares its own networks: on a service, include default. |
| Can't connect to the managed database | The service isn't on the aoox network. Add a domain (automatic) or an external network manually. |
| Domain returns 404 | Not redeployed yet after adding the domain, or the service port is wrong. |
| Host port rejected when saving | The port is already used by an application, database, another stack, or a running container. Pick another port. |
| Push doesn't trigger a deploy | A stack from a template has no repo — pushes are always ignored. For a stack from a repo, check the webhook URL and the provider's signature. |
Template
A stack from the catalog (source: template) uses the same runner, but its compose content is stored in aoox and can be edited from the UI without a repo — see One-click templates.
Next steps
Found a mistake? Edit this page on GitHub ↗