NineDeploy provides a robust, zero-downtime deployment engine supporting Docker, PM2, Compose and Nixpacks builds with health checks, blue-green port flipping, rollback safety, and preview environments.
When deploying a containerized service:
- Parallel Build: NineDeploy clones the git repository or pulls the specified image, building the new container without stopping the active one.
- Dynamic Port Binding: The new container starts on an ephemeral port.
- Healthcheck Gate: NineDeploy polls the configured health endpoint (e.g.
GET /healthreturning200 OK). - Traefik Traffic Flip: Once the new container passes health verification, Traefik dynamically routes incoming traffic to the new container.
- Graceful Drain: The old container receives
SIGTERMand is stopped after draining in-flight requests.
- In-flight deployments can be safely cancelled at any stage (clone, build, image pull, healthcheck).
- The engine uses process tree termination (
tree-kill) to ensure no orphan Docker processes or hung subshells consume resources.
- Push Webhooks: Automatic deployments on GitHub/GitLab pushes with HMAC-SHA256 signature verification.
- Monorepo Watch-Paths: Set glob patterns (e.g.
apps/api/**,packages/shared/**). Deployments trigger only when matching files change in the commit diff. - CI Opt-Out: Commits containing
[skip ci]or[skip cd]in the commit message are ignored by webhook triggers.
- Automatically deploy isolated preview environments for Pull Requests / Merge Requests.
- Each preview environment receives a unique dynamic subdomain (e.g.
pr-42.app.yourdomain.com), constrained to the instance's own wildcard zone before routing goes active. - Previews inherit non-secret configuration from the parent service — secrets are withheld, and the webhook response reports how many were withheld.
- Since 0.10.42 a preview also never receives the parent project's shared secret env vars, any vault reference (
${{provider:KEY}}), or a database the production service is attached to (including the one a.ninedeploydatabase:section names). Each deploy log lists what was withheld, by name. To give a preview its own values, set env vars directly on the preview service, or attach a separate non-production database to it under Service → Databases (needsadminon that database). There is no preview-scoped env var set on the parent yet. - When the PR is closed or merged, NineDeploy automatically tears down the containers, removes Traefik routes, and purges ephemeral storage.
- Every deployment records an immutable image digest and exact configuration snapshot.
- Rollback redeploys that exact verified digest — preventing unexpected changes from floating
:latesttags. - Deployment history stays honest: when a new deploy goes live, older rows are settled into a
supersededstate instead of lingering as "Running", both at finalize time and during a reconciliation pass at panel boot.
A multi-container app deploys as type: compose. The stack runs as the compose
project ndcmp-<slug>; composeService names the service Traefik routes to and
whose healthcheck gates the deploy. There is no blue-green here — compose
replaces the project in place, so expect a brief gap. Compose files often
bind-mount host paths or request privileged containers, so creating and
deploying one is operator-only.
Three ways to get a stack in:
| Source | Where the file lives | Edit it in |
|---|---|---|
| Git repo | in the repository (path from the build config's Dockerfile / compose file path, default docker-compose.yml) |
your repo |
| Hub template | shipped inside the template (composeContent) |
reinstall from the Hub |
| Pasted YAML | on the service row, rewritten into the workspace before every deploy | the service's Compose File tab |
New Service → Type Compose → Paste YAML. The panel analyses the file as you type and will not let you continue until it can actually run here:
- Refused:
env_file:(inline the values or add them as environment variables), bind-mounts with inlinecontent:, a file that declares no services, unparsable YAML. - Warned, but allowed:
external: true(the resource must already exist),network_mode: host|service:|container:(deployed as-is, with no sandbox bridge attached).
${VAR:-default} references are offered as prefilled environment rows;
${VAR} references with no default must be filled in yourself, or the stack
starts with empty values.
SERVICE_* tokens are resolved once per stack and stored as ordinary
environment variables, so the same token means the same value in every service
of the file:
| Token | Value |
|---|---|
SERVICE_USER_* / SERVICE_LOWERCASEUSER_* |
16 random alphanumerics |
SERVICE_PASSWORD_*, SERVICE_PASSWORD_<n> |
random alphanumerics (32 by default) |
SERVICE_PASSWORD_HEX_<n>, SERVICE_HEX_<n> |
<n> hex characters |
SERVICE_BASE64_<n> |
<n> random characters (not base64, despite the name) |
SERVICE_REALBASE64_<n> |
base64 of <n> random bytes |
SERVICE_URL_<SERVICE>[_<port>] |
the stack's public URL |
SERVICE_FQDN_<SERVICE>[_<port>] |
just the host of that URL |
Existing values are never rotated by a retry or a redeploy.
The Compose File tab (inline stacks only) saves a new revision and
optionally redeploys. The service row is the source of truth: the workspace
copy is rewritten from it before every deploy, so a deleted or hand-edited
docker-compose.yml in the workspace repairs itself on the next run, and an
exported service carries its stack to another host.