Docker
Emit a multi-stage `Dockerfile` + `.dockerignore` tuned for Bun + Mandu. Artifact-only — users run `docker build` / `docker push` themselves.
On this page
Docker
mandu deploy --target=docker emits a multi-stage Dockerfile and a
tuned .dockerignore. It is artifact-only — Mandu never invokes
docker build for you.
# Emit artifacts only (idempotent)
mandu deploy --target=docker
# Preview what would be written
mandu deploy --target=docker --dry-run
Emitted files
.
├── Dockerfile
└── .dockerignore
Both are idempotent — re-running mandu deploy --target=docker
produces byte-identical output given the same mandu.config.ts.
The Dockerfile
The emitted Dockerfile is multi-stage — dependency install, build, runtime — with aggressive layer caching:
# syntax=docker/dockerfile:1.6
ARG BUN_VERSION=1.3.12
# --- Stage 1: deps ---
FROM oven/bun:${BUN_VERSION}-alpine AS deps
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile --production=false
# --- Stage 2: build ---
FROM deps AS build
COPY . .
RUN bun run build
# --- Stage 3: runtime ---
FROM oven/bun:${BUN_VERSION}-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3333
# Copy only runtime deps + build output
COPY --from=build /app/package.json /app/bun.lock ./
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/.mandu ./.mandu
COPY --from=build /app/public ./public
COPY --from=build /app/dist ./dist
USER bun
EXPOSE 3333
CMD ["bun", "run", "start"]
The .dockerignore
node_modules
.mandu
coverage
.mandu/coverage
dist
.git
.github
tests
**/*.test.ts
**/*.test.tsx
**/__snapshots__/**
.env*
!.env.example
The goal is to keep the build context small — without this ignore,
docker build sends every node_modules/ file over the wire to the
daemon.
Building and pushing
# Build
docker build -t myapp:latest .
# Run locally
docker run --rm -p 3333:3333 -e NODE_ENV=production myapp:latest
# Tag + push to GHCR
docker tag myapp:latest ghcr.io/acme/myapp:v1.2.3
docker push ghcr.io/acme/myapp:v1.2.3
Runtime environment
| Variable | Default | Purpose |
|---|---|---|
PORT |
3333 |
Listener port |
NODE_ENV |
production |
Cookie secure flag, etc. |
DATABASE_URL |
— | Primary DB (consumed by mandu db) |
SESSION_SECRET |
— | Session middleware secret |
JWT_SECRET |
— | Auth middleware secret |
Pass them through -e or --env-file .env.production:
docker run --rm -p 3333:3333 \
--env-file .env.production \
myapp:latest
Image size
The runtime image is approximately:
| Layer | Size |
|---|---|
oven/bun:1.3.12-alpine base |
~90 MB |
node_modules (production) |
~20–80 MB (app-dependent) |
.mandu build output |
~5–20 MB |
public + dist |
app-dependent |
Typical total: 120–200 MB. If you need to drop further, post-process
the runtime stage with docker-slim — the Dockerfile is already
minimal on the Bun side.
Health check
Add a Mandu-friendly health check to the Dockerfile if your orchestrator doesn't supply one:
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD bunx --bun wget --spider -q "http://127.0.0.1:${PORT}/healthz" || exit 1
Mandu exposes /healthz by default — it returns 200 when the server
is live and the manifest is loaded.
Security notes
- Non-root runtime — the emitted Dockerfile pins
USER bun. If you need root for a one-off install, add it to thedepsstage, notruntime. - No secrets in the image —
--set-secretstores values in the OS keychain; the adapter refuses to write them into the Dockerfile or any COPY source (CLI_E210). .dockerignorecovers.env*— the ignore file excludes every.env*except.env.example. Do not move secrets into a committed file.
Common errors
EACCES when running as USER bun — a file in /app was
created as root and then not chown'd. The emitted Dockerfile avoids
this; if you hand-edit, make sure COPY --chown=bun:bun is used for
files written in the runtime stage.
Image runs but no response on 3333 — check docker logs <id>.
Usually a missing DATABASE_URL or SESSION_SECRET at runtime.
🤖 Agent Prompt
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/deploy/docker to my project.
Summary of the page:
`mandu deploy --target=docker` emits a Dockerfile using `oven/bun` as the build and runtime base. Multi-stage: deps → build → runtime. Runtime stage uses `USER bun` (non-root) and exposes port 3333. `.dockerignore` excludes `.mandu/`, `node_modules/`, `coverage/`.
Required invariants — must hold after your changes:
- `mandu deploy --target=docker` NEVER invokes `docker build` — users run it themselves
- Runtime stage runs as `USER bun` (non-root) — avoid running as root
- Default port is 3333 — override with `PORT` env at runtime
- Build cache is optimized via layered COPY: `package.json` + `bun.lock` first, source last
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 Compose — multi-service scaffold with a Postgres sidecar.
- Fly.io — Fly consumes the same Dockerfile.
- Deploy index — full adapter matrix.
For Agents
{
"schema": "mandu.deploy.docker/v0.24",
"command": "mandu deploy --target=docker",
"artifacts": ["Dockerfile", ".dockerignore"],
"execute": false,
"base_image": "oven/bun:${BUN_VERSION}-alpine",
"runtime_user": "bun",
"default_port": 3333,
"rules": [
"Runtime stage always runs as non-root (`USER bun`)",
"`.env*` is always in `.dockerignore` — never bake secrets into the image",
"`docker build` is NEVER invoked by `mandu deploy` — users run it"
]
}For Agents
`mandu deploy --target=docker` emits a Dockerfile using `oven/bun` as the build and runtime base. Multi-stage: deps → build → runtime. Runtime stage uses `USER bun` (non-root) and exposes port 3333. `.dockerignore` excludes `.mandu/`, `node_modules/`, `coverage/`.
- `mandu deploy --target=docker` NEVER invokes `docker build` — users run it themselves
- Runtime stage runs as `USER bun` (non-root) — avoid running as root
- Default port is 3333 — override with `PORT` env at runtime
- Build cache is optimized via layered COPY: `package.json` + `bun.lock` first, source last