LangENKO

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.

since v0.24
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

  1. Secrets never touch artifact files. The writeArtifact helper (see artifact-writer.ts) accepts a forbiddenValues map and throws SecretLeakError (CLI_E210) if the content includes any value verbatim.
  2. Secret values are never logged. All log formatters route values through maskSecret() which returns a constant "****".
  3. Provider tokens are sourced from env vars, not argv. The adapter deploy() implementations read tokens from the environment so they don't surface in ps listings.
  4. Fallback-file path is mode-0600 + one-shot warning. The plaintext-JSON fallback (.mandu/secrets.json) is only used when Bun.secrets is unavailable; it emits a single warning per process.

Pages

  • Docker — single-service Dockerfile + multi-stage build.
  • Docker Compose — multi-service with Postgres sidecar.
  • Fly.iofly.toml + flyctl integration.
  • Vercelvercel.json + Node SSR function.
  • Railwayrailway.json + nixpacks.toml.
  • Netlifynetlify.toml + Functions.
  • Cloudflare Pages — wrangler + _middleware.

🤖 Agent Prompt

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

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

AI hint

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

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