Docker Compose
Multi-service scaffold — Mandu app + Postgres sidecar + `.env.example`. Ideal for local reproducible dev environments and single-host self-hosting.
On this page
Docker Compose
mandu deploy --target=docker-compose emits a multi-service scaffold
— a Mandu app service, a Postgres sidecar, an env example file, plus
the same Dockerfile as --target=docker. Ideal for local
reproducibility and single-host self-hosting.
# Emit artifacts
mandu deploy --target=docker-compose
# Skip the DB sidecar (app-only)
mandu deploy --target=docker-compose --no-db
Emitted files
.
├── Dockerfile
├── docker-compose.yml
├── .env.example
└── .dockerignore
All four are idempotent and safe to re-emit. If docker-compose.yml
already has non-Mandu services, the adapter refuses to overwrite and
points at --force (experimental).
The compose file
# docker-compose.yml
services:
app:
build:
context: .
dockerfile: Dockerfile
env_file: .env
environment:
DATABASE_URL: postgres://mandu:${POSTGRES_PASSWORD}@db:5432/mandu
PORT: 3333
ports:
- "3333:3333"
depends_on:
db:
condition: service_healthy
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: mandu
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: mandu
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U mandu"]
interval: 5s
timeout: 3s
retries: 5
restart: unless-stopped
volumes:
postgres_data:
.env.example
# .env.example — copy to .env and fill in
POSTGRES_PASSWORD=change-me-please
SESSION_SECRET=change-me-please
JWT_SECRET=change-me-please
NODE_ENV=production
The real .env lives in .gitignore. docker-compose reads it
automatically at the project root — no extra --env-file flag needed
for the default setup.
First run
# 1. Seed env
cp .env.example .env
$EDITOR .env
# 2. Bring up the stack
docker compose up -d
# 3. Apply migrations (from the host)
docker compose exec app bunx mandu db apply
# 4. Seed fixtures (if desired)
docker compose exec app bunx mandu db seed --env=production
# 5. Tail logs
docker compose logs -f app
Open http://localhost:3333.
Lifecycle commands
# Rebuild the image after source changes
docker compose build app
# Recreate just the app (keep the DB volume)
docker compose up -d --force-recreate app
# Full teardown, INCLUDING the DB volume (destructive!)
docker compose down -v
Scaling considerations
Docker Compose is a single-host orchestrator. For multi-host, switch to:
- Kubernetes — run
mandu deploy --target=dockerto get the image, then write your own manifests. A first-party k8s adapter is not yet planned. - Fly.io —
mandu deploy --target=fly --executegives you global multi-region out of the box (uses the same Dockerfile).
Security notes
- Internal network by default — app ↔ db traffic never leaves the compose bridge network. The DB port is not published on the host.
- Secrets come from
.env—docker-compose.ymlnever references a secret value directly. Editing.envis the single source of truth for this deployment. - Volumes are scoped — only the DB gets a persistent volume. The app image is stateless.
Common errors
db service never becomes healthy — check docker compose logs db.
Usually a missing POSTGRES_PASSWORD in .env.
DATABASE_URL inside the app points at 127.0.0.1 — it should
point at db:5432 (the compose service name). The emitted file does
this correctly; verify the env var override isn't re-pointing at
localhost.
docker compose exec app bunx mandu db apply hangs — ensure the
app container passed the DB health check. docker compose ps shows
the current state.
🤖 Agent Prompt
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/deploy/docker-compose to my project.
Summary of the page:
`mandu deploy --target=docker-compose` emits Dockerfile + docker-compose.yml + .env.example. The compose file scaffolds a Postgres service by default (Phase 4c DB integration). Runtime service uses the same non-root Dockerfile as --target=docker.
Required invariants — must hold after your changes:
- Postgres sidecar is scaffolded by default; disable with `--no-db`
- Services communicate over a private bridge network (`internal` by default)
- `.env.example` is committed; `.env` is gitignored (compose reads `.env` automatically)
- No volumes for app code — only for database data (`postgres_data:/var/lib/postgresql/data`)
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Related
- Docker — single-service, no DB sidecar.
- CLI —
db seed— seeding fixtures inside the compose stack. - Deploy index — full adapter matrix.
For Agents
{
"schema": "mandu.deploy.docker-compose/v0.24",
"command": "mandu deploy --target=docker-compose",
"artifacts": ["Dockerfile", "docker-compose.yml", ".env.example", ".dockerignore"],
"execute": false,
"sidecars": ["postgres:16-alpine"],
"network_default": "internal bridge — DB port not published",
"volumes": ["postgres_data:/var/lib/postgresql/data"],
"rules": [
"`.env` is the single source of truth for secrets — never commit",
"Database port is not exposed on the host in the default config",
"Use `docker compose exec app bunx mandu db apply` for migrations"
]
}For Agents
`mandu deploy --target=docker-compose` emits Dockerfile + docker-compose.yml + .env.example. The compose file scaffolds a Postgres service by default (Phase 4c DB integration). Runtime service uses the same non-root Dockerfile as --target=docker.
- Postgres sidecar is scaffolded by default; disable with `--no-db`
- Services communicate over a private bridge network (`internal` by default)
- `.env.example` is committed; `.env` is gitignored (compose reads `.env` automatically)
- No volumes for app code — only for database data (`postgres_data:/var/lib/postgresql/data`)