Skip to content

cmd build

github-actions[bot] edited this page Oct 15, 2026 · 17 revisions

nself build

Generate docker-compose.yml, nginx configs, and SSL certificates from .env.

Synopsis

nself build [flags]

Description

nself build reads your .env cascade and generates all infrastructure configuration files: a docker-compose.yml with every enabled service, nginx reverse-proxy configs, and SSL certificates. It must be run after nself init and after any configuration change before restarting services.

The build pipeline loads configuration from .env → .env.{dev|staging|prod} → .env.secrets → .env.local (later files override earlier ones), merges plugin configurations from ~/.nself/plugins/, and applies security validation (password strength, no wildcard CORS in production, port binding checks). The result is a single docker-compose.yml that includes core services, optional services, monitoring, custom services (CS_1–CS_10), and any installed plugins.

By default, nself build is smart-cached: it compares .env modification time against docker-compose.yml and skips regeneration when nothing has changed. Use --force to override the cache.

ENV resolution — which .env.{ENV} layer loads

ENV decides which of .env.dev / .env.staging / .env.prod loads as the second cascade layer. It is resolved in this order, and the build logs which source decided it:

  1. Process environment — an ENV=... already exported (by your shell, systemd, CI, etc.) always wins.
  2. .env's own ENV= key — when the process environment has no ENV set, the build reads the ENV= line directly out of the project's own .env file (a single targeted lookup, before the rest of the cascade — whose own file selection depends on this value — runs).
  3. dev — the default when neither of the above sets anything.

This matters most on a server: a checked-out production project's .env normally carries ENV=prod, and no shell on the box necessarily exports ENV itself. Before this was fixed, a bare nself build on such a checkout silently fell back to dev and generated a dev-shaped compose (dev images, *.local.nself.org routes, mailpit/nself-admin wired in) instead of loading .env.prod. Setting ENV=prod in the shell (or CI) still works exactly as before and takes precedence over .env.

Declared plugins (nself.yaml)

Since v1.2.2, nself build reads the project manifest (nself.yaml or nself.yml) and guarantees every declared plugin is wired into the generated stack. Two declaration shapes are accepted:

# flat list
plugins:
  - cron
  - notify

# tiered map, plus bundle expansion
bundle: nsentry
plugins:
  free: [cron, notify]
  pro: [ai-gateway]

bundle: and bundles: expand through the canonical bundle catalog. For each declared plugin the build checks the plugin install directory, then the core-service alias (auth; storage is the free storage plugin and MinIO does not cover it), then attempts a best-effort auto-install from the registry (60 second timeout per plugin). Any plugin that still cannot be wired is reported with a per-plugin warning and a printed nself plugin install ... fix command. A declared plugin is never silently dropped.

Env var Default Effect
NSELF_AUTO_INSTALL_PLUGINS true Set false to disable auto-install (offline builds, hermetic CI)
NSELF_PLUGIN_DIR ~/.nself/plugins Override the plugin install directory (per-project plugin sets, CI)

Projects without a manifest keep the install-then-discover flow unchanged.

Secret templating

Since v1.2.2, generated docker-compose.yml files contain no literal secret values. Secrets (Postgres password, Hasura admin secret, JWT configuration, MinIO credentials, SMTP password) are emitted as ${VAR} references. The interpolation values are written to .nself/compose.env with mode 0600, and nself start/stop/restart pass it to docker compose via --env-file. Editing a secret in .env and re-running nself build updates the running stack on next start. Projects built before v1.2.2 keep default .env discovery until rebuilt.

Unknown env var warnings

nself build warns on unrecognized variables in the .env cascade. Common app-owned variables (NODE_ENV, JWT_SECRET, LOG_LEVEL, COOKIE_SECRET, SSL_AUTO_TRUST, ENABLE_DEBUG, NSELF_PROJECT_NAME) are recognized and never warn. For project-specific variables, set ENV_ALLOWLIST to a comma-separated list of exact names or prefix patterns ending in *:

ENV_ALLOWLIST=MY_APP_TOKEN,FEATURE_*

Redis auto-enable

nself build automatically includes a Redis service in docker-compose.yml when any installed plugin requires it, even if REDIS_ENABLED is not set in .env.

Plugins that trigger Redis auto-enable: ai, claw, mux, cron, notify.

When auto-enable fires, the build prints:

Note: Redis auto-enabled because a BullMQ-backed plugin (cron, notify, ai, claw, or mux) was detected.

To disable auto-enable, uninstall the plugin or explicitly set REDIS_ENABLED=false and confirm you do not need the plugin's queue features. To opt in explicitly and suppress the note, set REDIS_ENABLED=true in .env.

v0.9 project detection

nself build scans for v0.9 project artifacts before generating any files. Two or more detected artifacts trigger a hard error pointing to the migration guide. A single artifact produces a non-blocking warning. Use --no-migration-check in automation (CI) where you are certain no v0.9 projects exist. Use --allow-legacy only as a temporary workaround while running nself migrate. See Upgrade-From-v0.9.

Docker Compose v5 compatibility

nself build generates compose files that are compatible with both Docker Compose v4 and v5.

Docker Compose v5 introduced a stricter parser: it treats the top-level pids_limit service field as a canonical alias for deploy.resources.limits.pids. When both are present, v5 rejects the compose file with:

services.<name>: can't set distinct values on 'pids_limit' and 'deploy.resources.limits.pids': invalid compose project

The generator uses the deploy.resources.limits.pids form exclusively. The pids_limit top-level field is never emitted. This form is also valid on Compose v3.4+ and v4, so generated files work across all current Docker Compose versions.

Each long-running service gets a default pids limit of 100 to prevent fork-bomb attacks. Services that need more (such as postgres under high concurrency) override this in their configuration.

nginx/sites regeneration is additive-safe

nself build regenerates nginx/sites/*.conf on every run. Before regenerating, it:

  1. Snapshots the directory's current contents to .nself/backups/nginx-sites-<timestamp>/ (keeping the last 5 snapshots).
  2. Removes only the files it generated itself — identified by the # Generated by nself build marker every generated conf carries on its first line.

Any file in nginx/sites/ without that marker (a hand-written conf, or one belonging to another project that happens to share the directory when NGINX_FRONTED_BY is unset) is left in place, and its path is printed as a warning instead of being deleted silently. This directory is never swept at all when NGINX_FRONTED_BY points this project's routes at another stack's nginx — see Config-Nginx § Fronted by Another Stack.

/etc/hosts is only ever touched for local development

When SSL_MODE=local (the default), nself build generates local certificates and, historically, also added the project's domains to /etc/hosts so they resolve to 127.0.0.1. This is now gated:

  • ENV=prod always skips it, regardless of what BASE_DOMAIN looks like or any flag passed. This is the strongest available signal that the build is running against a live box.
  • A recognized local-dev domain (localhost, or a domain ending in .local.nself.org, .localhost, or .local) is still updated automatically outside prod — no flag needed, this is the normal local dev experience.
  • Any other domain is left untouched unless you pass --hosts to opt in explicitly (still subject to the ENV=prod veto above).

When it does write, the build logs exactly which entries it's about to add before writing them, and first checks that /etc/hosts is actually writable — skipping with a warning (and the manual sudo nself dns-setup instructions) rather than attempting a write it already knows will fail. Use cmd-trust (nself trust dns) to manage /etc/hosts explicitly at any time, independent of nself build. With NSELF_HEADLESS=1 or CI=1 the build never trusts the CA or edits /etc/hosts; it prints the manual commands instead (see Config-Env-Vars).

Plan, confirm, apply

nself build --plan shows what a build would change and writes nothing. It runs the real build in memory, compares the result with the files on disk and with the running containers, and prints one plan: the files that would be added, changed or removed, the host effects (certificates, /etc/hosts, trust store, plugin fragments, nginx snapshot, orphan containers) and the containers the next nself start would recreate. nself build with no flags computes the same plan first and then applies it, so what --plan showed is what is written. See Safe-On-Live for the guarantees and the confirmation rules.

Flag What it does
--plan Print the plan and exit. With --json the output is one v1 envelope whose data is the change plan (plan_id, artifacts, effects, containers, destructive, requires_confirmation, empty).
--yes Confirm a change on a prod-class env (ENV = prod or staging) without a prompt.
--plan-id <id> Apply only if the plan still has this id. A stale id fails with E450 and writes nothing.
--diff Print unified diffs of the planned changes to stderr. Env files show key names only, never values.

Non-env diffs also redact secret-named keys, pairs and flags and their repeated values on either side of the diff. A secret flag or pair that reaches the end of a YAML block scalar line keeps its continuation lines hidden until the next flag or the end of the scalar. Secret values containing the literal text [REDACTED] receive the same protection.

A first build generates secrets, so --plan-id is refused for it (E451); confirm at the prompt or use --yes without it. A change on a prod-class env needs --yes or an interactive yes. In v1.5 mode (NSELF_V15=1) a non-interactive build without --yes is refused with E403 (exit 4). In the default v1.4 mode it prints the plan summary and a one-line notice on stderr, then proceeds as before. A dev env never asks. nself build --json without --plan applies and prints the applied plan as the envelope data.

Certificate bytes and host trust state are runtime effects outside artifact byte confirmation. A plan that generates or copies certificates, or installs the mkcert CA, refuses --plan-id with E454 before applying anything; its human plan and apply output disclose this exclusion, including on dev. Confirm at the prompt or use --yes without --plan-id, or provision certificates and host trust first. Plugin installation and removal similarly refuse --plan-id (E453): the build shows and, interactively on prod-class envs, confirms the render again after those effects. Downloaded plugin bytes are checked against the registry checksum rather than bound by the plan id; edits during that install window are reflected in the second render.

Flags

Name Shorthand Type Default Env Escalates to JSON override Description
--allow-insecure bool false Allow insecure config (dev only)
--allow-legacy bool false Bypass v0.9 artifact check and proceed with WARNING (not recommended)
--check bool false Validate only, don't build
--debug bool false DEBUG Enable debug mode
--diff bool false Print unified diffs of the planned changes to stderr (env files show key names only)
--force -f bool false Force rebuild all components and overwrite hand-edited generated files
--hosts bool false Opt in to /etc/hosts management for a BASE_DOMAIN that isn't a recognized local-dev domain (localhost/.local.nself.org/.localhost/*.local). Never overrides ENV=prod, which always skips /etc/hosts.
--no-auto-redis bool false Disable automatic Redis enablement when a BullMQ-backed plugin is detected
--no-cache bool false Disable build cache
--no-migration-check bool false Skip v1 artifact detection (for automation/CI)
--no-monorepo bool false Disable automatic monorepo backend detection
--plan bool false Show what build would change (files, host effects, containers) and write nothing
--plan-id string "" Apply only if the change still matches this plan_id (from nself build --plan --json)
--profile string "" Service profile: curated subset of services to include in docker-compose.yml. app (default) — full service set, identical to pre-profile behaviour. ops — observability + CI server: postgres, hasura, auth, nginx, monitoring stack; excludes minio, mailpit, admin, functions, search. Overrides NSELF_PROFILE env var. Valid values: app, ops.
--quiet -q bool false Suppress non-error output (for CI use)
--remove-orphans bool false Remove containers with no matching service in the freshly generated compose (G-014). Detection always runs; removal is opt-in.
--security-report bool false Generate security analysis
--verbose -v bool false Show environment cascade
--write-lock bool false Write and keep nself.lock.yaml (pins images and plugins) even before v1.5, where the lock is always on
--yes bool false Confirm a change on a prod-class env (prod, staging) without a prompt
--help -h bool false Show help

Contract

  • Side effect: write
  • Output: document
  • JSON: envelope (data schema)
  • Exit codes: none declared

Examples

# Standard build
nself build

# Validate config only, don't write files
nself build --check

# Force rebuild everything, ignoring cache
nself build --force

# CI mode — quiet output
nself build -q

# Show the environment cascade as it loads
nself build --verbose

# See what a build would change, write nothing
nself build --plan

# Machine-readable plan, then apply exactly that plan on a prod-class env
nself build --plan --json
nself build --yes --plan-id <plan_id from the plan>

# Generate a security analysis report
nself build --security-report

# Rebuild for a specific environment
nself build --force --verbose

See Also

← Commands | Home →

Home


Getting Started


Commands


Features


Configuration


Plugins (87 + 10 monitoring)

Free (25)
Pro (62)
Planned (26)
  • plugin-audit
  • plugin-blog
  • plugin-checkout
  • plugin-commerce
  • plugin-drm
  • plugin-export
  • plugin-flow
  • plugin-import
  • plugin-ldap
  • plugin-mailgun
  • plugin-media
  • plugin-oauth-providers
  • plugin-pages
  • plugin-postmark
  • plugin-rate-limit
  • plugin-reports
  • plugin-saml
  • plugin-scheduler
  • plugin-sendgrid
  • plugin-sso
  • plugin-subscription
  • plugin-thumb
  • plugin-transcoder
  • plugin-twilio
  • plugin-waf
  • plugin-watermark

Guides


Architecture


Reference


Machine interface


Licensing


Security


Brand


Operations


Contributing


Admin


Changelog


All commands (21)

Clone this wiki locally