LangENKO

Docker Compose

Multi-service scaffold — Mandu app + Postgres sidecar + `.env.example`. Ideal for local reproducible dev environments and single-host self-hosting.

since v0.24
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=docker to get the image, then write your own manifests. A first-party k8s adapter is not yet planned.
  • Fly.iomandu deploy --target=fly --execute gives you global multi-region out of the box (uses the same Dockerfile).

Security notes

  1. Internal network by default — app ↔ db traffic never leaves the compose bridge network. The DB port is not published on the host.
  2. Secrets come from .envdocker-compose.yml never references a secret value directly. Editing .env is the single source of truth for this deployment.
  3. 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

🤖 Agent Prompt — Docker Compose
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.

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

AI hint

`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.

Invariants
  • 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`)
Guard scope
deploy