Deploy
`mandu deploy` turns a built Mandu project into the config files + pipeline each hosting provider expects. Seven adapters, one CLI surface, zero secrets-in-artifacts.
On this page
Deploy
mandu deploy is Mandu's single entry point for turning a built
project into the config files + pipeline each hosting provider
expects. Seven adapters ship out of the box; a shared security contract
keeps your secrets out of every artifact file.
# Produce a Dockerfile + .dockerignore
mandu deploy --target=docker
# Fly.io: validate config, emit fly.toml + Dockerfile, dry-run
mandu deploy --target=fly --dry-run
# Full pipeline, then actually deploy (requires provider CLI)
mandu deploy --target=vercel --execute
Adapter matrix
| Target | Invocation | Primary artifacts | --execute |
|---|---|---|---|
| Docker | --target=docker |
Dockerfile, .dockerignore |
no¹ |
| Docker Compose | --target=docker-compose |
Dockerfile, docker-compose.yml, .env.example |
no¹ |
| Fly.io | --target=fly |
Dockerfile, fly.toml |
harness² |
| Vercel | --target=vercel |
vercel.json, api/_mandu.ts |
harness² |
| Railway | --target=railway |
railway.json, nixpacks.toml |
harness² |
| Netlify | --target=netlify |
netlify.toml, netlify/functions/ssr.ts |
harness² |
| Cloudflare Pages | --target=cf-pages |
wrangler.toml, functions/_middleware.ts |
harness² |
¹ Artifact-only — users run docker build / docker compose up themselves.
² Harness: the adapter exposes a deploy() primitive that a CI pipeline can invoke. The out-of-box default returns CLI_E214 (not-implemented) so the user runs the provider CLI manually after prepare.
Pipeline
mandu deploy --target=<target> [flags]
1. validate mandu.config → fail fast on invalid config
2. architecture guard → skipped in --dry-run
3. build → skipped in --dry-run
4. adapter.check() → validate project + CLI toolchain
5. adapter.prepare() → emit artifacts (idempotent)
6. adapter.deploy() → only when --execute is passed
Flags
| Flag | Effect |
|---|---|
--target=<name> |
Required. Target platform (see matrix). |
--env=<name> |
production (default), staging, preview. |
--project=<name> |
Override the project slug used by provider config. |
--dry-run |
Skip guard + build; run check + prepare only. |
--execute |
Invoke the provider CLI after prepare. |
--set-secret KEY=VALUE |
Store a secret in OS keychain (repeatable). |
--verbose |
Print extra diagnostics (secret values always masked). |
Secret handling
Mandu does not write secret values to any artifact file. The
primary secret store is Bun.secrets — a thin wrapper over the OS
keychain (macOS Keychain, Windows Credential Manager, Linux libsecret).
When Bun.secrets is unavailable (older Bun), Mandu falls back to
.mandu/secrets.json with chmod 0600 and prints a one-time warning.
# Store a secret for the Fly adapter
mandu deploy --target=fly --set-secret FLY_API_TOKEN=fo1_...
# Later, execute the deploy. Missing required secrets abort the pipeline.
mandu deploy --target=fly --execute
Each adapter declares the secrets it needs. Running
mandu deploy --target=<target> --dry-run prints the adapter's secret
inventory with a present/absent marker for every required and optional
secret.
Minimum provider CLI versions
| Target | Binary | Minimum version |
|---|---|---|
| fly | flyctl |
0.1.0 |
| vercel | vercel |
28.0.0 |
| railway | railway |
3.0.0 |
| netlify | netlify |
17.0.0 |
| cf-pages | wrangler |
3.0.0 |
check() only probes the provider CLI when --execute is set, so
--dry-run and the default prepare flow succeed even on clean CI
runners.
Error codes
| Code | Meaning |
|---|---|
CLI_E200 |
Unsupported target value. |
CLI_E201 |
mandu.config invalid. |
CLI_E202 |
Build failed during deploy. |
CLI_E203 |
Architecture guard error count > 0. |
CLI_E204 |
Artifact write failed (permission/disk). |
CLI_E205 |
Required provider CLI is missing. |
CLI_E206 |
Provider CLI is older than the required minimum. |
CLI_E207 |
Required secret not present in the store. |
CLI_E208 |
OS keychain unavailable + fallback disabled. |
CLI_E209 |
--set-secret pair failed KEY=VALUE validation. |
CLI_E210 |
Artifact refused write — secret value would leak. |
CLI_E211 |
--execute required to invoke the provider CLI. |
CLI_E212 |
Routes manifest not built. |
CLI_E213 |
Edge-runtime compatibility warning (netlify / cf-pages). |
CLI_E214 |
Adapter has no deploy() implementation. |
Security invariants
- Secrets never touch artifact files. The
writeArtifacthelper (seeartifact-writer.ts) accepts aforbiddenValuesmap and throwsSecretLeakError(CLI_E210) if the content includes any value verbatim. - Secret values are never logged. All log formatters route
values through
maskSecret()which returns a constant"****". - Provider tokens are sourced from env vars, not argv. The
adapter
deploy()implementations read tokens from the environment so they don't surface inpslistings. - Fallback-file path is mode-0600 + one-shot warning. The
plaintext-JSON fallback (
.mandu/secrets.json) is only used whenBun.secretsis unavailable; it emits a single warning per process.
Pages
- Docker — single-service Dockerfile + multi-stage build.
- Docker Compose — multi-service with Postgres sidecar.
- Fly.io —
fly.toml+ flyctl integration. - Vercel —
vercel.json+ Node SSR function. - Railway —
railway.json+ nixpacks.toml. - Netlify —
netlify.toml+ Functions. - Cloudflare Pages — wrangler +
_middleware.
🤖 Agent Prompt
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/deploy/index to my project.
Summary of the page:
`mandu deploy --target=<adapter>` runs validate → guard → build → check → prepare → (optional) deploy. Secrets are stored in `Bun.secrets` (OS keychain), never written to artifact files. 7 adapters: docker, docker-compose, fly, vercel, railway, netlify, cf-pages.
Required invariants — must hold after your changes:
- `--target=<name>` is required — unsupported values return CLI_E200
- Secrets NEVER touch artifact files — writeArtifact throws SecretLeakError (CLI_E210) if a value appears verbatim
- Secrets NEVER get logged — all formatters route through `maskSecret()` → constant `\"****\"`
- `--execute` is required for adapter.deploy() to run; without it, prepare-only
- Adapter `check()` only probes provider CLIs when `--execute` is passed — clean CI runners still get through `--dry-run`
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
- Edge adapters — for Cloudflare Workers (long-lived
edge runtime) see
@mandujs/edge— distinct frommandu deploy --target=cf-pages. - CLI Reference —
mandu deploy— flag table. - Recipes — Deploy — task-oriented guides.
For Agents
{
"schema": "mandu.deploy/v0.24",
"command": "mandu deploy",
"required_flag": "--target",
"targets": ["docker", "docker-compose", "fly", "vercel", "railway", "netlify", "cf-pages"],
"pipeline": ["validate", "guard", "build", "check", "prepare", "deploy?"],
"secret_store_primary": "Bun.secrets (OS keychain)",
"secret_store_fallback": ".mandu/secrets.json (mode 0600)",
"security_invariants": [
"Secrets never written to artifact files (CLI_E210)",
"Secret values never logged — always masked as ****",
"Provider tokens sourced from env vars, not argv",
"Fallback-file path uses chmod 0600 + one-shot warning"
],
"rules": [
"Use `--dry-run` in CI gatekeeper jobs — no provider CLI required",
"Use `--execute` only in deploy CI jobs — requires provider CLI",
"Store secrets via `--set-secret KEY=VALUE` before the first deploy"
]
}For Agents
`mandu deploy --target=<adapter>` runs validate → guard → build → check → prepare → (optional) deploy. Secrets are stored in `Bun.secrets` (OS keychain), never written to artifact files. 7 adapters: docker, docker-compose, fly, vercel, railway, netlify, cf-pages.
- `--target=<name>` is required — unsupported values return CLI_E200
- Secrets NEVER touch artifact files — writeArtifact throws SecretLeakError (CLI_E210) if a value appears verbatim
- Secrets NEVER get logged — all formatters route through `maskSecret()` → constant `\"****\"`
- `--execute` is required for adapter.deploy() to run; without it, prepare-only
- Adapter `check()` only probes provider CLIs when `--execute` is passed — clean CI runners still get through `--dry-run`