# 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: ```yaml # 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 `*`: ```bash 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.: 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-/` (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 ` | 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](https://github.com/nself-org/cli/blob/main/schemas/commands/build.v1.schema.json)) - **Exit codes:** none declared ## Examples ```bash # 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 # Generate a security analysis report nself build --security-report # Rebuild for a specific environment nself build --force --verbose ``` ## See Also - [[Commands]] — full command index - [[Core-Services]] — what a stack is made of ← [[Commands]] | [[Home]] →