Repository navigation
cmd deploy
Deploy the stack to a target environment (local, staging, production).
nself deploy [target] [flags]
nself deploy builds your stack and deploys it to a target environment using a per-service
sequenced rolling restart. It chains nself build then restarts services in dependency order,
waiting for each service to pass a health check before restarting the next.
The restart order is resolved at deploy time from the project's own compose file
(docker compose config --services), never from a fixed guess list — a project's actual
service names vary (e.g. object storage is always named minio, never storage; there is no
literal plugins service). See Rolling Restart, Service Order and Downtime.
The target environment can be supplied as a positional argument or via --env. The flag takes
priority when both are given. A target is local or any environment in the deploy inventory:
every environment in .nself/control-plane.yaml, or every NSELF_DEPLOY_HOST_<ENV> variable
(so NSELF_DEPLOY_HOST_QA makes qa a target). staging and prod keep working with no
inventory, and production is an alias of prod unless the inventory has an environment named
production. Only the named environment is deployed or probed; an unknown name is refused with
E483 and the list of known environments, and the deploy never falls back to another one.
When NSELF_DEPLOY_HOST_STAGING or NSELF_DEPLOY_HOST_PROD is set, the CLI rsyncs the compose
file and env to the remote host, pulls updated images, then runs the rolling restart via SSH.
With no host configured for a remote environment the deploy is refused (E483); use nself deploy local to deploy on this machine.
When .nself/control-plane.yaml is present (or --server is passed), nself deploy routes
through the topology-aware pipeline (controlplane.Run). The pipeline reads each server's
role and SSH capability, skips read-only and hidden servers automatically, and returns a
non-zero exit code if the primary app server was skipped. Without that file and without
--server, behavior is byte-identical to the legacy single-host path.
Targets accept both short and long forms:
| Target | Accepts | Description |
|---|---|---|
| local | local |
Build and rolling-restart on this machine |
| staging | staging |
Staging environment (uses NSELF_DEPLOY_HOST_STAGING if set) |
| prod |
prod, production
|
Production (uses NSELF_DEPLOY_HOST_PROD if set; requires --force or --dry-run) |
| any other name | the environment's name, e.g. qa
|
An environment from the inventory (uses NSELF_DEPLOY_HOST_<ENV> on the single-host path); loads .env, .env.<name>, .env.secrets (the same list nself build uses), never .env.dev or .env.prod, and a deploy to any remote host never reads .env.local (your personal override stays on your machine; nself deploy local still loads it) |
An environment named prod or production is production-class and requires --force (or
--yes) unless you pass --dry-run. The check runs before every deploy path, including the
blue/green canary path. Releases before this change deployed every environment in the
inventory whenever a .nself/control-plane.yaml existed, whatever target you named.
A deploy to a remote environment with no host (no inventory entry and no NSELF_DEPLOY_HOST_<ENV>)
is refused with E483 before anything is built; it never deploys on the current machine. The
blue/green canary flags apply to local only; with a remote environment they are refused. In the
control-plane pipeline, a server whose deploy fails makes the command exit non-zero and lists it.
With a .nself/control-plane.yaml, a remote deploy first runs the same remote build (no .env.local) and writes the validated env snapshot, then ships both the compose file and .env.<env> to every server; if the build or snapshot fails nothing is probed or sent. A prod-class environment always uses the prod env cascade and is written to .env.prod on the host. The env file pushed to the host is read with the same dotenv reader as the build, so both see the same values. An inventory with two environment names that differ only by case is refused, here and in nself deploy environments.
nself deploy access grant, revoke, and list accept --env, --tier, and
--server. The selectors may be combined and resolve the matching inventory
hosts in environment, role, and server order. --host remains available for an
explicit single host and cannot be combined with a selector. A selector that
matches no hosts is refused before SSH runs. --dry-run on grant or revoke
shows each host's resulting change without writing any host. In v1.5 mode,
changes to a prod-tier host require --yes or terminal confirmation; an
unconfirmed noninteractive command exits 4 (E403). Revoking the last key
still requires --force on each host. In v1.5 mode nself deploy access list --json
answers with one envelope whose data.hosts has one object per host, with its
env, server, host, entries, foreign_count and status (an explicit
--host is one row). NSELF_JSON_LEGACY=1 keeps the bare arrays of v1.4.
In v1.5 mode --json on deploy, deploy status, deploy check-access,
deploy environments, deploy access, deploy security setup, doctor security audit|status and update images|upgrade|verify-sbom prints one v1 envelope. A
dry run (deploy --dry-run --json) carries the plan: the steps, or the planned
servers of a pipeline deploy. A failed run prints the error envelope only, never a
data document. Commands that print JSON in v1.4 keep that bare form with
NSELF_JSON_LEGACY=1.
nself deploy --profile ops builds the ops compose profile and defaults to
environment ops. An explicit target or --env overrides that default.
NSELF_DEPLOY_HOST_OPS supplies a single-host target when no inventory file
is present. OPS_DEPLOY_HOST remains a v1.5 fallback with a deprecation
warning. In v1.5, nself deploy ops and nself ops deploy use the same
pipeline path as nself deploy --profile ops. --dry-run prints the profile,
target environment, host, and remote path before any build or SSH action.
nself deploy promote <source> <target> --yes promotes between inventory environments.
Tiers must move forward or stay equal: local, local-servers, then prod.
Source and target must differ. A prod-tier target requires --yes or terminal
confirmation; an unconfirmed noninteractive promotion exits 4 (E403).
--dry-run prints the ordered plan without contacting or changing a host. With
--json, it emits one v1 envelope containing the result and planned steps.
The steps are resolve, confirm, verify, preflight, source-health, capture, build,
compare, snapshot, save, apply, migrate, health, and record. Builds use the target
cascade in a scratch directory. Production values never enter the operator's tree.
Compare proves per-platform image digest equality before taking a snapshot or
changing the target. It resolves every staged compose fragment with the target's
environment files and captures the source using its compose manifest. Each service's
platform override selects its manifest; a source platform mismatch is drift.
linux/arm64/v8 and linux/arm64 select the same platform. Other variants,
including linux/arm/v6 and linux/arm/v7, remain distinct.
Drift exits 1 (E490) and names the affected services and lookup failures.
Services with a compose build: context are unverifiable: prod refuses them;
other tiers warn. The snapshot is created, located by tag, and verified before
save or apply. A failed verification exits with E206 without applying anything.
After apply, running image identities are inspected again before migration. A
changed tag fails promotion and restores files without restoring the database.
Failures after the verified snapshot restore the saved artifacts. The database
is restored only if migration started, followed by another health check.
Successful rollback exits 2 (E488) naming the failed step. Failed rollback
stops immediately with E489, the snapshot tag, backup ID, and a manual restore
command. The local .nself/state/promote.json records each host's saved tar and
changed or absent files. nself deploy rollback <target> and the v1.5
nself promote rollback shim restore that record when it is the latest change,
using the verified backup on its recorded target host.
Before a promotion database restore starts, recovery persists db_restore_started;
after success it persists DBRestoredAt. If completion cannot be saved or the
process stops, a retry refuses with E489, naming the backup and warning that
the restore may already have run. Verify the database on the backup host first.
If the restore completed (including a restore performed by hand), run
nself deploy rollback <target> --db-restore-done to record completion and finish
recovery without restoring again. The flag asserts operator verification; it
does not run a database restore.
Record rollback reapplies all saved stacks before restoring the database, only
when migration started and the backup has not already been restored. An already
rolled-back record changes nothing. The project deploy operation lock covers record
reload, file reapply, database restore, health, and persistence. For deploy rollback,
foreign or superseded promotion records use the v1.5 deploy record; an unusable
same-target record is refused with E485 before remote writes.
Supersession uses the deployment identity saved by the promotion, rather than
timestamp order. On platforms without operation-lock support, rollback follows
the deploy guard's unlocked fallback. If promote.json is corrupt, the E485
message names the file and tells you to move it aside; deploy rollback <target>
then uses deploy.json.
Promotions also update the record used by deploy status and the next deployment.
With no arguments, deploy promote keeps the existing canary flip. In v1.4,
its arguments retain their previous behavior and promote keeps its legacy
implementation. In v1.5, promote <source> <target> and
deploy promote-env <source> <target> delegate to the environment promotion
runner with one deprecation warning per process.
nself deploy promote <source> <target> --release <dir> verifies a release
before any snapshot or remote change. <dir> holds release-manifest.dsse.json
(a DSSE envelope) and evidence.json. The checks run in this order and each
refusal has its own code:
| Step | Refusal |
|---|---|
Wrong payloadType, unknown key id, key of another purpose, bad signature, unreadable trust files |
E493 (exit 3) |
Signing key listed in ci-release.revoked
|
E494 (exit 3) |
| Unreadable manifest, or a schema major other than v1 | E492 |
| Evidence missing, not the file the manifest pins, or the plugin rejects it (stale, unbound, invalid, failing, not release-class) | E497 |
| Manifest revision differs from the evidence revision | E496 |
| Release quarantined | E498 |
| An image digest promote would deploy is not listed in the manifest (checked after the compare step, still before the snapshot) | E495 |
Verification required but --release or the ci plugin is missing |
E499 |
Signatures use sdk/go/signing pinned to purpose ci-release. Trust roots are
per project and are never read from the working tree: keys and revocations come
from ~/.nself/ci/trust/ci-release.{keys,revoked} and from
.nself/trust/ci-release.{keys,revoked} as committed on the default branch of
origin (cli runs git fetch origin first; a failed fetch refuses with
E493). A working-tree copy that differs prints one warning and is ignored.
Each keys line is <key-id> <base64 public key>; each revoked line is a key id;
# starts a comment line.
The evidence is judged by the ci plugin (`nself ci evidence verify --file
--release belongs to the environment form. deploy promote --release <dir>
with no source and target is refused with E401 instead of flipping the canary.
--dry-run with --release checks that the ci plugin is usable, prints the plan and verifies nothing. The hidden
nself promote shim has no --release; deploy promote-env has it. The digest
check covers the services that carry an image identity: a build-context service
has none, so on a non-prod target it passes the check unproven (prod refuses such
a service earlier, in the compare step).
The digest set is flat: a digest listed anywhere in the manifest passes for any
service, so the manifest proves "this image came from the release", not "this
image belongs to this service". --release with the legacy nself promote
form needs NSELF_V15=1 and is refused with E401 otherwise.
The inventory field require_verified_release on an environment makes
--release mandatory. It defaults to true for tier prod when the ci plugin is
installed. The working copy is not a trust root, so the requirement is the stricter of
the working-tree inventory and the inventory committed on the default branch of
origin: editing the field or the tier locally cannot turn the gate off. If the
committed copy cannot be read and the ci plugin is installed, the promotion
refuses with E499. Git runs without GIT_DIR/GIT_CONFIG_* and replacement
objects; anyone who can edit the project's .git/config can already run code as
the operator, so by default the project's own origin is trusted. To pin it,
put the expected URL in ~/.nself/ci/trust/origin.url (first non-comment line):
a different effective origin (local config or insteadOf) then refuses with
E493 before any trust file is read, and so does a project whose origin was
removed. A project with no origin, or with no committed
.nself/control-plane.yaml, has no committed policy to consult and promotes
without --release unless the origin is pinned: then, with the ci plugin
installed, it refuses with E499.
Without --release and without the requirement, promote prints one
warning and proceeds. With the field true and no --release, even --dry-run
refuses with E499.
nself deploy <env> --json --stream and nself deploy promote <source> <target> --json --stream print one JSON envelope per line (NDJSON) while the run goes on. An agent can follow a long deploy without parsing text.
-
--streamneeds--json. Without it the command exits 1 withE401and does nothing. -
deploy promote --streamneeds both<source>and<target>(v1.5). With no arguments (the canary flip) it exits 1 withE401. - stdout holds only envelopes. Human progress and any error text go to stderr.
-
--jsonwithout--streamis unchanged: one document, as before. - The exit status is the one the run has without
--stream. A promote that rolled back exits 2 (E488).
Every line is a compact v1 envelope. A progress line has data.type progress and these keys, all always present (null when unknown): seq (1, 2, 3, ...), step, phase (start, done, failed, skipped or rollback), env, server, elapsed_ms (since the stream began) and detail. The schema is schemas/deploy-progress.v1.schema.json.
The last line is always a result line, also when the run failed: data.type result, data.result (ok, failed, rolled_back or rollback_failed) and data.steps (each step with its last status).
$ nself deploy staging --json --stream --dry-run
{"schema_version":"1","command":"deploy","data":{"type":"progress","seq":1,"step":"Deploy staging-app","phase":"skipped","env":"staging","server":"staging-app","elapsed_ms":2,"detail":"dry-run: capability manage"}}
{"schema_version":"1","command":"deploy","data":{"type":"result","result":"ok","steps":[{"step":"Deploy staging-app","status":"planned"}]}}
The registry marks the --stream flag output: stream, so MCP and HTTP keep the single-document form of deploy.
| Strategy | Status | Behavior |
|---|---|---|
rolling |
Default (v1.1.1) | Per-service sequenced restart with health-gating. Each service waits for service_healthy (60s, or the healthcheck's own declared timing when that is longer, up to 10m) before the next restarts. |
blue-green / canary |
Available via --canary N (Y17) |
Zero-downtime: green containers run alongside blue, Nginx shifts N% traffic to green during soak, then promotes to 100%. Requires NSELF_FEATURE_BLUE_GREEN_DEPLOY=true. |
preview |
Not yet implemented | Falls back to rolling with an explicit warning. |
The --strategy=blue-green and --strategy=canary flags still fall back to rolling for backwards
compatibility with existing scripts. Use --canary N to activate the implemented blue/green path.
See cmd-deploy for the full per-strategy behavior spec and downtime expectations.
Enable with NSELF_FEATURE_BLUE_GREEN_DEPLOY=true (feature flag blue_green_deploy via nself flag list).
1. Pull new images tagged as green
2. Start green containers (docker compose -p nself-green up -d)
3. Health check green (30s timeout)
4. Nginx upstream: route N% to green (via upstream weights)
5. Canary soak period (default 5 min)
- Monitor error rate from green containers
- If error_rate > NSELF_CANARY_ERROR_THRESHOLD (default 1%) → auto-rollback
6. Promote Nginx upstream to 100% green
7. Health check green at 100%
8. Stop and remove blue containers
9. Green becomes blue for the next deploy
| Color | Port offset | Example (Hasura) |
|---|---|---|
| Blue | 0 (base ports) | 8080 |
| Green | +100 | 8180 |
# Canary at 10% (requires NSELF_FEATURE_BLUE_GREEN_DEPLOY=true)
nself deploy local --canary 10
# Canary at 25%
nself deploy staging --canary 25 --force
# Skip canary — flip directly to 100% green
nself deploy local --skip-canary
# Promote a canary to 100% after manual review
nself deploy promote
# Rollback to blue in < 5 seconds
nself deploy rollback localIf a pending migration is not safe to run during canary (DROP COLUMN, DROP TABLE, RENAME COLUMN, ALTER COLUMN TYPE, NOT NULL without DEFAULT), the deploy is blocked:
ERROR: Migration 0042_drop_column_old_name.sql is not backward-compatible.
Run with --force-migration to apply (disables canary, full downtime deploy).
Use --force-migration to proceed with a full downtime deploy.
Blue/green state is persisted to .nself/bluegreen/state.json. Run nself deploy status --blue-green
to inspect the current active environment and canary traffic split.
The rolling strategy restarts services in dependency order, one docker compose up -d --no-deps <service> at a time, and gates each service before the next one starts. If a service fails its
gate, the deploy halts, names the service, and restarts nothing after it.
Manifest and env files. Every compose call uses the same inputs as nself restart: all files
listed in .nself/compose-files.txt (the base file, plugin fragments, the user override) as -f,
and the env files .env then .nself/compose.env as --env-file (later wins). Plugin services
therefore get the variables nself build computed for them. The root .env is no longer a
fallback for plugin variables: a plugin variable must be in .nself/compose.env (run nself build).
Health gate by service kind. The kind comes from the resolved config (docker compose config --format json over the manifest):
| Service kind | Waits for | Limit | On failure |
|---|---|---|---|
| Declares a healthcheck | healthy |
60s, or the healthcheck's own timing (start_period + retries × (interval + timeout)) when that is longer, capped at 10m |
E251 (health check timeout), service named |
One-shot (restart: "no", no healthcheck) |
a new run that exits 0 | 60s | non-zero exit fails at once with E250; never exiting, or never starting again, is E251 |
| Anything else |
running, seen as the same run on two polls 2s apart |
30s | E251, service named |
The rows apply top to bottom: a service with restart: "no" and a healthcheck is long-running, so it
waits for healthy. For every kind, a container that exited non-zero or is restarting fails the
deploy at once with E250 when a poll sees it. After the last service, every gated service is read once
more, and the deploy fails with E250 naming any that restarted, is restarting, or exited non-zero since its
gate passed. That catches a crash loop that stays up for a few seconds, but it is not a guarantee: a crash
after that final re-check is not detected, and the health checks (nself doctor) that follow the roll are
the only backstop. A service with no restart line that exited 0 counts as a finished run (Docker's
default policy is "no"), so it is done. Containers left behind by docker compose run are ignored: they are not replicas of the
service. A service that appears in the restart order but not in the resolved config fails the deploy
with E252, checked for every service before the first restart. If Docker cannot be read (daemon down, compose ps fails), the
deploy stops with Docker's own message and no service code, because nothing is known about the
service; an interrupt (ctrl-c) stops the same way.
Bounded up, and an interrupted one. The progress line prints the limit the gate really
applies, for example healthy, max 105s. docker compose up -d --no-deps itself is bounded at 15
minutes per service (it can pull an image before it returns). If it does not finish, docker is
stopped and the deploy fails naming the service. A Ctrl-C during up, or docker exiting 130 before
nself saw the interrupt, ends the deploy as an interrupt, not a failure. up stops the old
container before it starts the new one, so the service may be left stopped: both messages end with
Service <name> may be stopped: run 'nself restart <name>' to bring it back.
What a failed deploy leaves behind. Nothing rolls back. Services before the failed one already run
the new config. The failed service is in whatever state up left it (running but not ready, restarting,
or exited). Services after it are not touched and keep running the old config. Fix the cause and run
nself deploy again: it restarts every service in order.
Init containers that run once and exit never report healthy; the old gate waited for that and
aborted real deploys.
The order is derived from the project's own resolved compose file, not a fixed list. Before
each restart, nself deploy runs docker compose config --services over the manifest (locally, or over SSH for a
remote push target) to get the set of services that actually exist, then orders them:
-
Core services first, in dependency order, but only the ones actually present:
postgres→hasura→auth. -
Every other present service next, in the order docker reported them (e.g.
minio,nginx,redis,mailpit,ping-api,auth-server,nself-admin— whatever this project's compose actually defines). - Plugin-contributed services last — any service a plugin fragment in the compose manifest adds (and the base file does not define) restarts after everything else.
A service name is never invented. Restarting a name that doesn't exist in this project's
compose (the old fixed order included storage and plugins, neither of which is a real
compose service name — object storage is always minio) used to abort the deploy with
no such service: storage after earlier services had already been recreated, leaving the
stack half-restarted. This is why the order can no longer be hardcoded.
Example for a project whose compose defines postgres hasura auth auth-server mailpit minio ping-api nginx redis nself-admin (no storage, no plugins):
| Service | Restart order | Expected downtime |
|---|---|---|
| postgres | 1st | 5–30s (WAL recovery time) |
| hasura | 2nd | 0–10s (waits for postgres) |
| auth | 3rd | 0–5s |
| auth-server, mailpit, minio, ping-api, nginx, redis, nself-admin | 4th onward, in compose order | 0–5s each |
| (any installed plugin's service) | last | 0–5s per plugin |
Total deploy time scales with the number of services actually present (roughly ~15s each). Total user-visible downtime is lower than the deploy duration because each service continues serving until its replacement becomes healthy.
--dry-run cannot query a live docker compose config --services (nothing has been built or
pulled yet), so it prints the resolution rule instead of a concrete list — the real order is
only known once the build for this deploy has run.
Use --skip-health-check to bypass the health gate for known-slow services like MeiliSearch or
Grafana. Set HEALTHCHECK_TIMEOUT_<SERVICE> (e.g. HEALTHCHECK_TIMEOUT_GRAFANA=120s) to
extend the per-service timeout.
--dry-run also prints, for each declared custom service (CS_1..CS_10), the full list of
environment variable names it will receive: the fixed core set (project identity, Postgres,
Hasura, Auth connection info) plus whatever CS_N_ENV_PASSTHROUGH and CS_N_ENV add. Values
are never printed. This exists so a plain addition to .env.prod doesn't get mistaken for
something a custom service will automatically see — it only reaches the service's container if
that service's CS_N_ENV_PASSTHROUGH allowlist names it, or CS_N_ENV sets it explicitly. See
Config: Custom Services for the full precedence rules.
Set NSELF_DEPLOY_HOST_STAGING or NSELF_DEPLOY_HOST_PROD to a user@host:/remote/path
value to enable remote deployments. The CLI:
- rsyncs
docker-compose.ymland.env.<target>to the remote - Runs
docker compose pullon the remote to fetch updated images - Runs the rolling restart on the remote via SSH
export NSELF_DEPLOY_HOST_STAGING=ubuntu@203.0.113.10:/opt/nself-staging
nself deploy stagingSSH key defaults to ~/.ssh/id_ed25519. Override with NSELF_DEPLOY_SSH_KEY.
Agent forwarding is always disabled (-o ForwardAgent=no). ssh, scp and rsync get their options as
separate arguments from one builder; no option, key path or remote path is ever parsed by a shell.
Before any process starts, the CLI refuses a key path outside [a-zA-Z0-9/_.~-] or starting with
-, a remote path with shell metacharacters, a leading - or a .. segment, and any ssh option
holding whitespace or a quote (rsync re-splits its -e string on spaces).
Host keys. Which policy a deploy uses depends on what it sends:
| Deploy path | v1.4 (default) | v1.5 (NSELF_V15=1) |
|---|---|---|
Secrets push (.env.<target> copied with scp) |
strict | strict |
Deploy that ships an env file (inventory deploy, nself deploy <target> push) |
strict, every connection of the run | strict |
| Compose file only | StrictHostKeyChecking=accept-new |
strict |
Read-only commands (probes outside a shipping run, logs, health) |
StrictHostKeyChecking=accept-new |
accept-new with nself's own key file (below) |
Strict means the CLI scans the host's live key and continues only when that key is already trusted,
either in your ~/.ssh/known_hosts or in the deploy pin file ~/.config/nself/deploy_known_hosts.
ssh then runs with StrictHostKeyChecking=yes, ControlMaster=no and ControlPath=none (a client that joins an existing connection-sharing master skips the host-key check, so a strict connection never does). Nothing is copied before the check. The scan asks
for an ED25519 key first and falls back to ECDSA, then RSA, one ssh-keyscan connection per try
(an ED25519 host needs one call). A host that offers only an RSA or ECDSA key can be pinned with
--trust-host-key and then passes the strict check. A host whose entry in ~/.ssh/known_hosts
holds only a non-ED25519 key fails with E487 when the scan finds an ED25519 key, because that key
is not trusted: pin the key the scan reports.
A run that ships an env file is strict from its first connection. The inventory pipeline checks
every remote server of the environment before any probe, preflight, load-balancer step or copy, and
an unknown key stops the whole run with E487 (it is never reported as a skipped server), and so does a key that stops verifying later in the run. The probes
that follow use strict ssh too, so no probe can record a key that the secrets check then trusts.
Outside a shipping run, v1.4 keeps accept-new byte for byte, and ssh records a first-seen key in
~/.ssh/known_hosts. Verify a new host's fingerprint before its first deploy (or pin it with
--trust-host-key), or run with NSELF_V15=1.
In v1.5 the deploy connections never seed your ~/.ssh/known_hosts (nself runner still does). An accept-new connection reads
~/.ssh/known_hosts as a read-only global file (a changed key there still fails) and records
first-seen keys in nself's own file ~/.config/nself/deploy_tofu_hosts. The strict check never
reads that file, so a key seen once by a read-only command is never trusted for secrets. If nself's
file cannot be created (an unwritable ~/.config), the connection records nothing
(UserKnownHostsFile=/dev/null) and warns once. If your home path holds a space, the two options are
skipped with a one-line warning and plain accept-new is used, which can record a first-seen key in
~/.ssh/known_hosts; such a home already fails every strict path, so no secret ships on that key.
First contact with a new host. A strict deploy to a host you have never connected to
fails with E487 and prints the live fingerprint:
E487 unknown host key SHA256:... for deploy@203.0.113.10; verify it, then run nself env target add <env> <server> --trust-host-key SHA256:...
(or connect once with plain ssh and accept the key into ~/.ssh/known_hosts)
Compare the fingerprint with the one your provider shows (console or ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub on the server), then either pin it with
--trust-host-key, or ssh to the host once and accept the key. Before P7-DEPL-28 and P7-DEPL-29 the v1.4
secrets push and env-shipping deploys accepted any key on first contact (accept-new).
IP-address hosts are pinned under the alias nself-<ip>-<port> (for example
nself-203.0.113.10-22) and ssh gets a matching HostKeyAlias; hosts on a non-default port use
nself-<host>-<port>. A pin file holding a line the CLI did not write (a wildcard,
@cert-authority or hashed entry) or with group- or world-writable permissions is refused with
E487; fix or remove that line and pin again.
-
nself deploy environments, list all environments and per-server SSH capabilities (JSON) -
nself deploy status [--blue-green] [--server <name>], report current deploy state;--serverfilters to one server;--blue-greenadds slot info -
nself deploy rollback [target], roll back the last deployment (see below) -
nself deploy promote, flip Nginx to 100% green after a manual canary review -
nself deploy logs [target] [--server <name>], tail Docker logs;--serverstreams logs from a specific server via SSH -
nself deploy health [target] [--server <name>] [--json], runnself doctor;--serverruns doctor over SSH on that server -
nself deploy check-access(deprecated), verifyNSELF_DEPLOY_HOST_*values resolve — usenself env target probeinstead
Lists every environment defined in .nself/control-plane.yaml (or synthesized from
NSELF_DEPLOY_HOST_<TARGET> env vars) and the resolved SSH capability of each server.
No key material appears in the output.
nself deploy environmentsOutput:
{
"environments": [
{
"name": "local",
"kind": "local",
"servers": [
{"name": "local", "role": "app", "capability": "manage"}
]
},
{
"name": "staging",
"kind": "remote",
"servers": [
{"name": "staging-app", "role": "app", "capability": "manage"},
{"name": "staging-lb", "role": "lb", "capability": "manage"}
]
},
{
"name": "prod",
"kind": "remote",
"servers": [
{"name": "prod-app", "role": "app", "capability": "manage"},
{"name": "prod-lb", "role": "lb", "capability": "read-only", "reason": "ssh: permission denied"}
]
}
]
}Capability values:
The reason field appears only when capability is not manage.
This command fixes a latent Admin 500 error: the Admin UI calls this endpoint to populate the
server-selector before starting a deploy.
When --server <name> is passed and .nself/control-plane.yaml exists, the output includes
a per-server capability section alongside the existing state:
{
"target": "staging",
"state": "running",
"servers": [
{
"env": "staging",
"server": "staging-app",
"role": "app",
"capability": "manage",
"latency_ms": 42
},
{
"env": "staging",
"server": "staging-lb",
"role": "lb",
"capability": "manage",
"latency_ms": 38
}
]
}Streams logs from a specific server over SSH:
nself deploy logs staging --server staging-appThe SSH connection uses BatchMode=yes -o ForwardAgent=no -o StrictHostKeyChecking=accept-new.
Key material is never logged.
Runs nself doctor on a specific server over SSH and streams the output:
nself deploy health staging --server staging-app
nself deploy health staging --server staging-app --jsonWith --json, the remote nself doctor --json is called and the structured output is passed
through to stdout.
Deprecated. Use nself env target probe for per-server SSH capability resolution.
check-access continues to work for backward compatibility. When .nself/control-plane.yaml
is present, it probes all servers and emits a deprecation warning, then outputs:
{
"servers": [
{"name": "staging-app", "env": "staging", "role": "app", "capability": "manage", "ok": true},
{"name": "prod-app", "env": "prod", "role": "app", "capability": "manage", "ok": true}
],
"all_ok": true
}Without a control-plane.yaml, it falls back to the legacy NSELF_DEPLOY_HOST_* env-var check
with the same JSON schema.
In v1.5 mode every remote deploy (a control-plane environment, or the single-host
NSELF_DEPLOY_HOST_<TARGET> path) applies a full artifact set instead of pushing one compose
file. v1.4 mode keeps the push described above, byte for byte. One flow runs per environment:
-
Target snapshot (read-only). Over ssh, plain file reads on the primary server: the
plugins installed in its plugin dir (
PLUGIN_DIRorNSELF_PLUGIN_DIRfrom<remote_path>/.env, else~/.nself/plugins; name, version and.disabledmarkers) and the certificate dirs under<remote_path>/ssl/certificates(which offullchain.pem,privkey.pem,chain.pemexist; the files are never opened).nselfis never run on the target. If the local enabled plugin set or a version differs, the deploy stops withE485(target drift) naming each difference, before anything is written. Every other server of the environment is read again just before its apply and must match the primary. -
Scratch build. The project is copied to
.nself/deploy-build/<env>/(rebuilt every deploy, without.nself/,.git,node_modules,ssl/,backups/,.volumes/,logs/and the olddocker-compose.yml). A stubssl/certificates/<dir>/*.pemtree mirrors the target's names, so generated cert paths are the target's.nself buildruns there with the target env cascade. Your own project tree is not written to; the stub files are never shipped. The copy leaves outnginx/files that carry the build marker (# Generated by nself build) unless allowlisted: they are stale generated output and the build writes what it owns. Only the nginx files this build generated, plus the files you name inNSELF_DEPLOY_NGINX_INCLUDE, are shipped. Allowlisted marked sites survive the build's stale-site prune; a freshly regenerated site takes precedence. Every other file of yournginx/tree is still copied (the build may read it) but not shipped, and the deploy output lists it before anything is sent:[not shipped] nginx/<path> (not generated by this build; add it to NSELF_DEPLOY_NGINX_INCLUDE to ship it). A marked file of yours that the build did not write again is listed as[not shipped] nginx/<path> (stale generated output this build did not regenerate). A dry run (--dry-run) stops before the scratch build, so it does not stage and does not print these lines; they appear in a real deploy. -
Path map. Every absolute local path in
.nself/compose-files.txt,.nself/compose-env-files.txt, the merged compose files and the env files becomes the remote layout: the project root is<remote_path>, plugin fragments live in<remote_path>/.nself/plugins/<slug>/. A shipped file that still names a local root (scratch project, your project, the local plugin dir or your home) fails the deploy before anything is sent. -
Deploy lock and pre-deploy backup tar (every tier). The host takes
<remote_path>/.nself/deploy.lock(an atomicmkdir; itsholderfile names pid, host, user, time and id). A second deploy or rollback finds it and refuses, naming the holder. If the holder process is gone, remove the lock by hand:rm <remote_path>/.nself/deploy.lock/holder && rmdir <remote_path>/.nself/deploy.lock. Then, before any sync,<remote_path>/.nself/backups/pre-deploy-<id>.tar.gz(mode 600: it holds env files) takes the compose files, env files, plugin fragments,nginx/(notconf.d*/, except thenginx/conf.d/<name>.conffiles this deploy ships, which the tar also holds when the host has them) andhasura/that exist, andpre-deploy-<id>.absentlists the artifact paths the host did not have yet (a createdconf.d/default.confincluded, so a rollback removes it). After a fully successful apply only the newest 5 pre-deploy tars plus this deploy's own are kept; older ones are removed, one[pruned] <name> (old pre-deploy tar)line each. A failed deploy prunes nothing, and a prune error is only a warning. Both paths are recorded in.nself/state/deploy.json(pre_deploy_tars, one per host;pre_deploy_tarfor the primary). Next to each tar the apply writespre-deploy-<id>.images(mode 600): the image id every non-digest image ref pointed at before the pull. A rollback points those refs back at those ids and pulls only the services it has no record for, so a mutable tag (app:latest) the deploy moved does not bring the new image back. If a recorded image is gone from the host the rollback stops withE489before the database restore; load the old image again (or pin it by digest) and rerun. A deploy made before this record existed rolls back with the plain pull. -
Verified backup (tier prod). Right after the tar and before anything is synced or any compose
command runs:
nself backup create --tag pre-deploy-<id>on the host, the backup is found innself backup list --format json, andnself backup verify <id> --restore-test --cleanupmust exit 0. Verification has a ten-minute deadline and restores the dump in a test Postgres container with smoke and sentinel checks. An encrypted dump must decrypt with the host's default backup identity, as rollback would. Cleanup of the test container is requested. Any failure or timeout isE206and nothing is applied. The id is recorded asbackup_id, the host asbackup_host. The host needsnselfon the sshPATH. -
Artifact sync, in place.
rsync -awith no--delete: manifests, merged compose files, fragments, env files (--chmod=F600, mode 600 asserted afterwards),nginx/andhasura/. The project.envis the host's own file and is never overwritten: it stays in the compose--env-filelist, and the sync only creates it (empty, mode 600) when the host has none. The host keeps.nself/deploy-shipped.txt, the list of what nSelf shipped. Before the sync the host removes only files that list names, the new set drops, and that carry thegenerated by nselfmarker. A marker-carrying conf nSelf never shipped here (another project's), an unmarked file or anything underconf.d*/stays. The generatednginx/conf.d/<name>.conffiles (todaydefault.conf) ship, and only that one directory: neverconf.d-<env>/, never a nested path, and a conf.d file is never pruned. The host decides, from the file alone: a file at that path whose first 5 lines have none starting exactly with# Generated by nself build(case-sensitive; a hand-written file, or another nself writer's such as# Generated by nself ssl add), or that is a symlink, is kept and not overwritten:[kept] nginx/conf.d/<name>.conf (hand-written on the host; not overwritten). Adefault.confthat is absent on the host is also kept out when any other file undernginx/conf.d*/ornginx/sites/already declares adefault_server:[kept] nginx/conf.d/default.conf (the host already declares a default_server in <file>; not shipped). Neither case fails the deploy. -
nginx -t. In a throwawaydocker run --rmcontainer made from the composenginxservice's image (the lock-pinned image when the stack is locked), with that service's new bind mounts read-only at the same container paths. The container is not on the stack network, so every compose service name,container_nameand network alias is added as a host entry (127.0.0.1):proxy_pass http://hasura:8080passes as it does inside the stack. A failure stops the deploy before any reload or start. A stack without an nginx service prints one skip line. -
Pull, then recreate preview.
docker compose pull, thendocker compose ... up -d --dry-runon the host lists the services it would recreate or create, so a moved tag that recreates a service shows up. A service is stateful when it has a volume or a bind mount under<remote_path>/.volumes. On a production-class target (tierprod), a stateful service in the recreate set needs--yes(or a typedyeson a terminal); otherwise the deploy stops withE403naming the services and nothing is applied. A dry-run line about a recreate or create that cannot be parsed or names no known service also stops a production-class deploy withE403(--yesdoes not cover it); elsewhere it is a warning. -
Apply. Each service in
docker compose config --servicesorder:up -d --no-deps -- <service>and the health gate (healthcheck: healthy within 60 s; one-shot: exit 0; otherwise running within 30 s and the same run a poll later).E250means the service is broken,E251that it was too slow. After the last service every gated service is read once more, a running nginx is reloaded (nginx -s reload, only after the throwawaynginx -tpassed), and the host runs the samenself db hasura metadata applyas the v1.4 push (strict on staging and production-class targets, a warning elsewhere;NSELF_HASURA_METADATA_STRICToverrides).
Limit: build contexts. The artifact set does not ship build contexts. Before anything is
written, a service with a build: context that is not already a directory on the host, or whose
context holds no Dockerfile (the dockerfile: key, default Dockerfile; dockerfile_inline needs
none), stops the deploy with E485 (build context not shippable) naming the services. A service
that also sets image: runs on that image and is not checked. Publish an image for the service,
or place its context on the host.
Limits.
- Only generated
nginx/conf.d/<name>.conffiles ship fromconf.d(see step 6);conf.d-<env>/, nested paths and unmarked files never do.api-docs.confis a site conf undernginx/sites. - A
remote_paththat is not directly under a directory named likeNGINX_FRONTED_BYis refused withE485(see "Projects fronted by another stack's nginx" below). - A deploy refused at
E403(unconfirmed stateful recreate) is restored, but the images the pull fetched stay on the host. - A symlinked directory under the project dir is followed only when its real path is inside the
real
remote_path. A link that resolves outside it (for examplenginx/pointing at a shared dir next to the project, or a symlinked.env.prod) stops the deploy withE485(symlink outside remote_path) before the lock, the tar and any sync. Move the shared dir inside the project dir. A link that stays inside is written through, and the deploy prints a[notice]line with the real path. The sync never changes the mode of a directory that already exists. - A
nginx/file the set ships is never overwritten when it already exists on the host without the nself generated marker, the last recorded deploy did not ship it and the host's own.nself/deploy-shipped.txtdoes not list it: the deploy stops withE485(unmarked file would be overwritten) before the lock, naming the path. A first deploy onto a host with hand-written nginx files stops at the first collision; move or delete the file, then deploy again. - The sync runs one rsync per destination directory over bare file names, so no directory entry is
transferred and it works with any rsync as sender or receiver. Proven operator-side rsync versions:
Apple's 2.6.9-compatible rsync (CI on macOS 14, also openrsync) and 3.x (the Linux sim). A Windows
operator's compose files may hold
C:\...paths; the path map rewrites them (any separator spelling, drive letters case-insensitive) and refuses a leftover. - Remote steps are non-interactive (
BatchMode=yes,ConnectTimeout=10): use ssh-agent or an unencrypted deploy key. A password or passphrase prompt would hang a step that runs in its own process group. - The prune (generated files the new set dropped) is driven by the last recorded deploy in
.nself/state/deploy.json; the host'sdeploy-shipped.txtcan only narrow it. With no record nothing is pruned.
If any step from the sync to the preview fails, no container has been touched yet. The files this
deploy shipped are put back from the pre-deploy tar in place, the ones it created are removed, and
the host is left as it was. Which paths those are comes from this process (and, for a rollback, from
deploy.json), never from files read back from the host; the host's own .env and anything the
deploy did not ship are never restored or removed. Directories that already exist keep their mode
and owner. A failure after the first up keeps the new files and the error names the tar and
nself deploy rollback <target>. A deploy that was refused or restored cleanly leaves
.nself/state/deploy.json byte for byte as it was.
Ctrl-C. SIGINT, SIGTERM and SIGHUP (a closed terminal or ssh session) during a v1.5 apply or rollback cancel the step in flight (ssh and
rsync run in their own process group, so the terminal does not kill them behind the tool's back).
The step in flight is not killed: it gets up to two minutes to finish, so its remote script is not
still running when the restore starts. A second signal while the restore runs prints one line saying so and does not kill it. Ctrl-C at the
Type yes prompt aborts the command at once. Before the first up, the tar is then restored (on a fresh context, at most 5 minutes) and the
deploy lock is released; after the first up the new files stay and the error names the rollback.
This is tested with real signals sent to the apply and to the rollback. SIGKILL, a lost connection or a powered-off
laptop are not covered: the lock stays, and the error for the next deploy says how to remove it.
For v1.5 environment promotion, Ctrl-C cancels the current step and runs compensation
on a fresh context, returning E488 when recovery succeeds. A provisional
.nself/state/promote-attempt.json keeps snapshot and pre-sync tar evidence until
a host keeps changes, when it atomically becomes the promote.json authority.
A failed attempt with no changed hosts preserves the previous live promotion.
Interrupted records can be recovered with deploy rollback <target>; the database is restored only if migration started and it has not already
been restored. If the final record write fails after health succeeds, E489 says
the promotion is live and names the snapshot, backup and pre-deploy tars. It leaves
the provisional record available and does not undo the healthy promotion.
Record rollback checks each recorded remote path, backup tar location, and the
host's applied compose manifest identity before changing any host. A changed path
or artifact identity refuses with E485. Restoring a production-class database
requires deploy rollback <target> --yes or terminal confirmation; without either,
non-interactive use returns E403 (exit 4). The promote rollback --yes shim uses
the same confirmation rule in v1.5.
Recovery checkpoints completed file and database restores. After a transient DB
restore failure, retry accepts the restored prior manifest (including absence)
and skips file restoration. An interrupted apply without matching tar evidence
refuses with E485 and names the verified backup for operator recovery.
All ssh and rsync commands use the strict host-key policy; an unknown key is E487.
A project that sets NGINX_FRONTED_BY=<stack> has no nginx service of its own. The host layout is
<dir>/<stack>/<project>: remote_path sits directly under the fronting stack's directory (the
front root, path.Dir(remote_path)). The build writes the per-service nginx/sites/*.conf there,
and the v1.5 deploy ships them as a second artifact root with the same guards as the project root.
-
Target snapshot. Besides the project's own
ssl/certificates, the snapshot lists the front root'sssl/certificates(directory names and which PEM files exist, never a key), and the scratch build's stub ssl tree for the fronting stack mirrors it. The apply checks it did not change. -
Container rule (before the lock). The deploy finds the fronting nginx by compose labels:
exactly one running container with
com.docker.compose.service=nginxwhosecom.docker.compose.project.working_diris the front root (as given or its real path). Zero or several isE485, naming every running nginx-service container with its working_dir label and the fix; nothing is sent. The container must bind-mount<front>/nginxor<front>/nginx/sites(elseE485). The container ID is kept, never a guessed name, and the deploy only runsdocker exec <id> nginx -tanddocker exec <id> nginx -s reload: nothing on the fronting stack is restarted or recreated, and its.env, compose files andssl/are never read, archived, copied or removed. The container is re-checked running beforenginx -t, before the reload and before the rollback reload. -
Guards (before the lock). The real front root must be the parent of the real
remote_path. The deploy looks only atnginx,nginx/sitesand thenginx/sites/<name>.confpaths it ships or may prune: a front file that is a symlink (dangling or not) isE485,nginxas a symlink isE485, andnginx/sitesmay be a symlink only to a directory under the real<front>/nginx, one that the container's bind mount covers. Generated site names have no project prefix (hasura.conf,auth.conf), so a shipped path never replaces a host file that is not this project's: an existing file must carry the generated marker (a line among the first 5 that starts with# Generated by nself build; a comment such as# hand-written, NOT generated by nself buildis not the marker), elseE485. A marked file is replaced only when itsserver_nameset equals the shipped file's (order-insensitive and not empty); a path this host's previous deploy shipped (front_shipped) is also replaced when the set equals the one recorded for it (front_names). A recorded path is no pass by itself: if another project put its own file there,E485names the file and both sets, so a sibling project's site is never overwritten, while a file the fronting stack generated earlier for this project can be adopted. The fronting stack's own.nself/deploy-shipped.txtis never read. -
Front tar. Right after the project's tar,
<remote_path>/.nself/backups/pre-deploy-<id>-front.tar.gzholds the shipped front paths that already exist (and the files the previous deploy shipped that this one may prune); symlinks are never followed into it. Onlynginx/sites/<name>.confpaths are ever in it. -
Sync and prune. Right after the project sync, the front files are copied without
--delete. A front file is pruned only when this host's previous deploy shipped it (front_shippedindeploy.json: files shipped, not files pruned, and never merged across hosts) and it carries the strict marker above and still has theserver_nameset recorded for it infront_names; otherwise it is kept ([kept] ... (server_name changed since this project shipped it; not pruned)). A record withoutfront_shippedorfront_namesprunes nothing. The host files are read again right before the first front write (after the tar), because the front root has no lock; a change since the first check isE485and the pre-touch restore runs, leaving the changed file alone. Another project's sites are never touched. -
Test and reload. Step 7 runs
docker exec <id> nginx -tin the live fronting container (it tests what will actually be loaded) instead of the throwaway container; step 9 reloads it withnginx -s reload. A failure before the firstup, includingnginx -tor a container that stopped, restores both roots in place (changed files back, created files removed) and reloads nothing. The front root has no lock of its own (the deploy lock coversremote_path): if anything else reloads the fronting nginx (another fronted project's deploy, a certificate renew hook) between the front sync and a restore, it loads the new files and keeps serving them until the next reload. The restore says so; reload the fronting nginx again in that case. -
Rollback.
deploy.jsonrecords, per host,front_tars,front_changed,front_absent,front_shippedandfront_names.nself deploy rollbackresolves the container first (sameE485rules), restores the front files in place from the front tar, then runsnginx -tandnginx -s reloadthere, even when the re-apply of the project failed (the error then says the fronting nginx was reloaded). A record without these fields, or with an emptyfront_changed, rolls back as before. With the fronting nginx down the rollback stops withE485before changing anything: start it, then run the rollback again.
nself deploy rollback <target> reads .nself/state/deploy.json. When it holds a pre_deploy_tar
for the target, it restores the tar on every host in pre_deploy_tars in place: extract to a temp
dir under .nself/backups/, rsync -ac back only the files the record lists as shipped (no
directory is moved or re-moded, so a bind-mounted nginx/ keeps its inode), and remove only the
listed files that did not exist before. On backup_host it then runs nself backup restore <backup_id> --yes when a verified backup was taken (tier prod), and re-applies the restored compose
set with the same pull, per-service up, health gate, nginx -t, nginx reload and hasura metadata
apply. The database restore returns the data to the moment of the backup: writes made since then,
while the new containers ran, are lost.
The database is restored at most once per record: db_restored_at is saved right after
nself backup restore succeeds, before the re-apply, so a retry of a rollback that failed later
(on any host) skips it and redoes only files and services. The backup_host is rolled back first. If it
kept no changes of the deploy (so it has no tar), the database is not restored: the rollback prints the
backup id and the manual nself backup restore <id> --yes command and keeps backup_id in the record.
backup_id is recorded only once a server ran up. If that command itself fails midway the
marker is not set and a retry runs it again. A record already rolled back is refused. The record's
success covers every server of the env, with each server's outcome in host_outcomes. Without such
a record the older promote rollback below runs.
nself deploy rollback is wired to the last nself promote tag. Every nself promote <src> <target>
call creates a backup snapshot tagged pre-promote-<target>-<unix> and writes a promotion record
to .nself/promotions/<id>.json. Rollback reads the most recent record and restores the env,
migration state, and Hasura metadata from that snapshot.
Rollback does not roll back container images. If you need an older image, pull it manually
(docker pull nself/...:v1.0.8) before running rollback.
# After a failed or unwanted production promotion:
nself deploy rollback prod
# Finds last pre-promote-prod-* backup tag
# Runs: nself backup restore pre-promote-prod-<ts>
# Prints: Rollback for prod completed — prior promote state restoredWhen no promote history exists:
Error: rollback failed: find backup: no promotion records found
Run 'nself promote staging' before a deploy to create a rollback point.
When the last promote tag is older than 7 days, rollback prompts for confirmation. A stale rollback may re-apply migrations that no longer match the current code. Review the diff shown before confirming.
Requires: nself promote must have run at least once against the target environment to create
a rollback point. On a fresh production host, run nself promote staging prod before the first
nself deploy production to ensure a rollback baseline exists.
After each deploy (unless --skip-health is set), the CLI calls nself doctor and checks the
output. If any service is unhealthy or has not started within 60s, the deploy result is failed
and the specific service name is reported:
[running] Health checks (calling nself health)
[failed] Health check failed (service: hasura). Run 'nself doctor --verbose' for details.
Use --skip-health as a break-glass escape hatch. A visible warning is always emitted when used.
When --json is set, the command writes a structured result:
{
"target": "staging",
"strategy": "rolling",
"steps": [
{"name": "Build images", "status": "done"},
{"name": "Restart postgres", "status": "done"},
{"name": "Restart hasura", "status": "done"},
{"name": "Restart auth", "status": "done"},
{"name": "Restart minio", "status": "done"},
{"name": "Restart nginx", "status": "done"},
{"name": "Health checks", "status": "done"}
],
"durationMs": 78234,
"success": true
}The "Restart <service>" step names shown above are an example, not a fixed list — they mirror
whatever this project's own compose file actually defines, per Rolling Restart, Service Order
and Downtime.
Human output uses the same step vocabulary (done, failed, skipped, running, pending,
unhealthy) inside [...] brackets so the Admin UI's deploy API route can parse it without
a separate protocol.
| Var | Default | Purpose |
|---|---|---|
NSELF_DEPLOY_HOST_STAGING |
— | SSH/rsync target for staging: user@host:/path
|
NSELF_DEPLOY_HOST_PROD |
— | SSH/rsync target for production: user@host:/path
|
NSELF_DEPLOY_HOST |
— | Generic remote host used when target-specific vars are unset: user@host:/path
|
NSELF_DEPLOY_USER |
— | SSH user for remote deployments (used when host is specified without a user prefix) |
NSELF_DEPLOY_KEY_PATH |
~/.ssh/id_ed25519 |
SSH private key path for remote deployments (alias: NSELF_DEPLOY_SSH_KEY) |
STAGING_DEPLOY_HOST |
— | Fallback alias for NSELF_DEPLOY_HOST_STAGING
|
PROD_DEPLOY_HOST |
— | Fallback alias for NSELF_DEPLOY_HOST_PROD
|
NSELF_DEPLOY_SSH_KEY |
~/.ssh/id_ed25519 |
SSH key path (superseded by NSELF_DEPLOY_KEY_PATH; both accepted) |
HEALTHCHECK_TIMEOUT_<SERVICE> |
60s |
Per-service health-check timeout |
NSELF_FEATURE_BLUE_GREEN_DEPLOY |
false |
Enable blue/green canary path (Y17). Set to true to activate. |
NSELF_DEPLOY_STRATEGY |
canary |
Deploy strategy when blue/green is active: canary, blue-green, direct
|
NSELF_CANARY_PERCENT |
10 |
Initial canary traffic % (used when --canary is not passed) |
NSELF_CANARY_SOAK_MINUTES |
5 |
Soak duration before auto-promote |
NSELF_CANARY_ERROR_THRESHOLD |
1.0 |
Error rate % that triggers auto-rollback |
NSELF_DEPLOY_HEALTH_TIMEOUT |
30 |
Seconds to wait for green container health |
NSELF_BLUE_PORT_OFFSET |
0 |
Port offset for blue containers |
NSELF_GREEN_PORT_OFFSET |
100 |
Port offset for green containers |
NSELF_DEPLOY_NGINX_INCLUDE |
— | v1.5 remote deploy: comma-separated globs relative to nginx/ (sites/hand.conf,includes/*.conf; path.Match per path segment) naming nginx files to ship besides the ones the build generated. Marked sites are preserved across the build's prune unless regenerated. Read from the target's env cascade. Absolute entries, .. and anything under conf.d are refused. |
NSELF_DEPLOY_ENV |
production |
Deploy target environment set by nself deploy after resolving --env / positional argument. Values: local, staging, production. Exposed for subprocesses and plugins. |
A remote environment needs a host; with none the deploy is refused (E483). Multi-region orchestration is available via the dedicated nself region command (list, add, status, promote).
--maintenance-banner is deferred to a future release (DEP-07). To show a maintenance page manually,
configure an nginx static page via nginx/conf.d/.
- Production deploys without
--forceare rejected with a clear message - Use
--dry-runfirst to preview steps on any target - Agent forwarding is disabled by default for all SSH connections (
ForwardAgent=no) - SSH connections use
BatchMode=yes StrictHostKeyChecking=accept-new - SSH keys are never logged or included in command output
-
--rollbackvalidates the promote tag's env against current code before applying; drift causes an explicit error rather than a silent misapply - Primary-server skip gate: when the topology pipeline detects that the primary app server
has
read-onlycapability and was skipped, the command exits non-zero even if other servers succeeded. Usenself deploy environmentsto diagnose capability issues before deploying. - Inline secrets in
--hostor--key-refflags are rejected with an error
-
cmd-build, generates
docker-compose.ymland nginx configs - cmd-start, boot the stack with health checks
- cmd-promote, promote env-to-env with rollback support
-
cmd-env, probe SSH capability and manage per-server access (
nself deploy check-accessreplacement) - cmd-deploy, full per-strategy spec and downtime expectations
- Home
| Name | Shorthand | Type | Default | Env | Escalates to | JSON override | Description |
|---|---|---|---|---|---|---|---|
--canary |
int |
0 |
Start a canary deploy at N%% traffic to green (0 = full flip) | ||||
--dry-run |
bool |
false |
Preview the deploy without executing | ||||
--env |
string |
"" |
Target environment: local, or any inventory environment such as staging, prod or qa (overrides positional arg; required env vars: NSELF_DEPLOY_HOST, NSELF_DEPLOY_USER, NSELF_DEPLOY_KEY_PATH) | ||||
--exclude-frontends |
bool |
false |
Exclude frontend apps from the deploy | ||||
--follow |
bool |
false |
Stream container logs after deploy until Ctrl-C (staging/prod only) | ||||
--force |
bool |
false |
Skip confirmation prompts (prod requires --confirm or --force) | ||||
--force-migration |
bool |
false |
Force deploy even with backward-incompatible migrations (disables canary) | ||||
--include-frontends |
bool |
false |
Include frontend apps in the deploy | ||||
--json |
bool |
false |
Emit JSON output | ||||
--profile |
string |
full |
Compose build profile: full or ops | ||||
--rolling |
bool |
false |
Alias for --strategy=rolling | ||||
--server |
string |
"" |
Deploy to a specific server only (name from control-plane inventory) | ||||
--skip-canary |
bool |
false |
Skip canary phase and flip directly to 100% green | ||||
--skip-health |
bool |
false |
Skip post-deploy health checks | ||||
--strategy |
string |
rolling |
Deploy strategy: rolling|blue-green|canary|preview | ||||
--stream |
bool |
false |
Print NDJSON progress events and a final result line (requires --json) | ||||
--yes |
bool |
false |
Skip prod confirmation prompt (alias for --force) | ||||
--help |
-h |
bool |
false |
Show help |
| Name | Description |
|---|---|
access |
Manage SSH key access on an already-deployed server |
check-access |
Verify access to configured deploy targets (deprecated: use 'nself env target probe') |
environments |
List configured deploy environments and server capabilities |
health |
Run health checks against a target deployment |
logs |
Show deployment logs for a target |
ops |
Deploy with the ops profile to the ops server |
promote |
Promote a canary or an environment |
promote-env |
Promote one environment to another (e.g. staging to prod) |
rollback |
Roll back the last deployment for a target |
security |
Apply server security |
status |
Show deployment status for a target |
targets |
List deploy targets without connecting to them |
web |
Build web apps locally and deploy prebuilt output to Vercel |
-
Side effect:
remote -
Output:
document -
JSON:
envelope(data schema) - Exit codes: none declared
# Local build + rolling restart (positional form)
nself deploy local
# Local build + rolling restart (flag form)
nself deploy --env local
# List all environments and per-server SSH capabilities
nself deploy environments
# Staging dry-run — shows what would happen without executing
nself deploy staging --dry-run
# Staging dry-run with topology plan (when control-plane.yaml exists)
nself deploy staging --dry-run --server staging-app
# Staging dry-run using --env flag
nself deploy --env staging --dry-run
# Deploy to a single server only
nself deploy staging --server staging-app
# Staging deploy with JSON output
nself deploy staging --json
# Production deploy (rolling)
nself deploy production --force
# Production deploy using --env, skipping the confirmation prompt via --yes
nself deploy --env prod --yes
# Production deploy and stream logs until Ctrl-C
nself deploy --env prod --force --follow
# Production deploy with explicit strategy (blue-green falls back to rolling with warning)
nself deploy production --strategy=blue-green --force
# Roll back to the last promoted state
nself deploy rollback prod
# Per-server status (requires control-plane.yaml)
nself deploy status --server staging-app
# Stream logs from a specific server
nself deploy logs staging --server staging-app
# Run doctor on a specific server
nself deploy health staging --server staging-app --json
# Check SSH access (deprecated — use 'nself env target probe' instead)
nself deploy check-access- Commands — full command index
- Core-Services — what a stack is made of
Getting Started
Commands
- Commands, Overview
- Lifecycle: cmd-init · cmd-build · cmd-start · cmd-stop · cmd-restart · cmd-dev
- Monitoring: cmd-status · cmd-logs · cmd-health · cmd-urls · cmd-doctor · cmd-monitor · cmd-alerts · cmd-sentry · cmd-watchdog
- Data: cmd-db · cmd-backup · cmd-dr · cmd-queue · cmd-webhooks
- Config: cmd-config · cmd-service · cmd-env · cmd-promote
- Networking: cmd-ssl · cmd-trust · cmd-dns-setup
- Security: cmd-access · cmd-security · cmd-secrets
- Tenancy: cmd-tenant · cmd-billing
- Plugins: cmd-plugin · cmd-license · cmd-dogfood (extracted, CLI-R11) · cmd-k8s (extracted, CLI-R11) · cmd-encryption (extracted, CLI-R11) · cmd-waf (extracted, CLI-R11) · cmd-federation (extracted, CLI-R11) · cmd-mail (extracted, CLI-R11) · cmd-dlq (extracted, CLI-R11)
- AI: cmd-ai · cmd-claw · cmd-model
- Templates: cmd-template
- Utilities: cmd-exec · cmd-clean · cmd-reset · cmd-update · cmd-upgrade · cmd-version · cmd-admin · cmd-migrate · cmd-migrate-firebase · cmd-migrate-supabase · cmd-completion
Features
- Features, Overview
- Feature-Auth
- Feature-Storage
- Feature-Search
- Feature-Functions
- Feature-Email
- Feature-Monitoring
- Feature-Plugins
- Feature-nClaw, AI Assistant
- Feature-nChat, Messaging
- Feature-nFamily, Family Social
- Feature-nCloud, Managed Hosting
- Feature-Memory-Rooms, Knowledge Organization
- Feature-Agent-Dashboard, Agent Metrics
- Feature-Image-Generation, AI Image Generation
Configuration
- Configuration, Overview
- Config-Env-Vars
- Config-Postgres
- Config-Hasura
- Config-Auth
- Config-Nginx
- Shared-Proxy
- Config-Optional-Services
- Config-Custom-Services
- Config-System
Plugins (87 + 10 monitoring)
- Plugin-Overview
- Plugin-Catalog
- Plugin-Install
- Plugin-Licensing
- Plugin-Architecture
- Plugin-Dev-Guide
- Plugin-Migrations-Contract
Free (25)
- plugin-backup
- plugin-content-acquisition
- plugin-content-progress
- plugin-cron
- plugin-donorbox
- plugin-feature-flags
- plugin-github
- plugin-github-runner
- plugin-invitations
- plugin-jobs
- plugin-link-preview
- plugin-mdns
- plugin-mlflow
- plugin-monitoring
- plugin-notifications
- plugin-notify
- plugin-paypal
- plugin-search
- plugin-shopify
- plugin-stripe
- plugin-subtitle-manager
- plugin-tokens
- plugin-torrent-manager
- plugin-vpn
- plugin-webhooks
Pro (62)
- plugin-access-controls
- plugin-activity-feed
- plugin-admin-api
- plugin-analytics
- plugin-auth
- plugin-bots
- plugin-browser
- plugin-calendar
- plugin-cdn
- plugin-chat
- plugin-claw
- plugin-claw-budget
- plugin-claw-news
- plugin-claw-web
- plugin-cloudflare
- plugin-cms
- plugin-compliance
- plugin-ddns
- plugin-devices
- plugin-documents
- plugin-entitlements
- plugin-file-processing
- plugin-game-metadata
- plugin-geocoding
- plugin-geolocation
- plugin-google
- plugin-home
- plugin-idme
- plugin-knowledge-base
- plugin-linkedin
- plugin-livekit
- plugin-media-processing
- plugin-meetings
- plugin-moderation
- plugin-mux
- plugin-object-storage
- plugin-observability
- plugin-photos
- plugin-podcast
- plugin-post
- plugin-realtime
- plugin-retro-gaming
- plugin-rom-discovery
- plugin-social
- plugin-sports
- plugin-support
- plugin-tmdb
- plugin-voice
- plugin-web3
- plugin-workflows
Planned (26)
plugin-auditplugin-blogplugin-checkoutplugin-commerceplugin-drmplugin-exportplugin-flowplugin-importplugin-ldapplugin-mailgunplugin-mediaplugin-oauth-providersplugin-pagesplugin-postmarkplugin-rate-limitplugin-reportsplugin-samlplugin-schedulerplugin-sendgridplugin-ssoplugin-subscriptionplugin-thumbplugin-transcoderplugin-twilioplugin-wafplugin-watermark
Guides
- Guide-Production-Deployment
- Guide-SSL-Setup
- Guide-Multi-Tenancy
- Guide-Security-Hardening
- Guide-Monitoring-Setup
- Guide-Backup-Restore
- Guide-Custom-Services
- Guide-Migration-from-v1
Architecture
- Architecture
- Architecture-Guardrails
- Service-Graph
- Compose-Generation
- Nginx-Generation
- Plugin-Architecture
Reference
Machine interface
-
JSON-Output,
--jsonenvelope -
Command-Registry,
nself help --json - Exit-Codes
- error-codes, Error Codes
-
Compat-V15,
NSELF_V15gate
Licensing
Security
Brand
Operations
- operations/release-cascade, Release Cascade
- operations/self-healing, Self-Healing Schema
- operations/redis-tuning, Redis Pool Tuning
- operations/meilisearch-warmup, MeiliSearch Warm-Up
- operations/jwt-rotation, JWT Key Rotation
- operations/windows-wsl2-setup, Windows / WSL2 Setup
- operations/gemini-oauth-reauth, Gemini OAuth Reauth
Contributing
Admin
- USER-ACTION-QUEUE, Pending Admin Actions
All commands (21)
- A: cmd-add · cmd-admin
- B: cmd-backup · cmd-build
- C: cmd-clean · cmd-config
- D: cmd-db · cmd-deploy · cmd-doctor
- E: cmd-exec
- I: cmd-init
- L: cmd-license · cmd-logs
- M: cmd-mcp
- R: cmd-remove · cmd-reset · cmd-restart
- S: cmd-start · cmd-status · cmd-stop
- U: cmd-update