---
name: nocobase-deployment
description: Plan, build, deploy, verify, upgrade, and troubleshoot NocoBase 3 applications on an existing Studio or a standalone Node.js or Docker server. Use when an application must move from source to a deployment environment or when a deployment failure needs diagnosis.
---

# NocoBase production deployment

Use this Skill to execute a complete deployment workflow. Read the application's `AGENTS.md`, the `.agents/rules/` files it indexes for building and deployment, and `README.MD` before changing files or running stateful commands: the README documents this application's build targets, archive layout and configuration variables.

Do not treat a process being healthy as proof that the application is usable. A deployment is complete only after the database, configuration, application behavior, and persistence checks below have evidence.

## Stop and identify the deployment

Before building or changing a server, record:

- The deployment mode: an App managed by an existing Studio, standalone Node.js managed by app-installer, standalone Node.js by hand, or standalone Docker.
- The source revision, application version, Node.js and pnpm versions, target CPU/OS/libc, and the destination host.
- Whether the destination uses the existing database and storage, a new database, or a restore. Business data is not included in `dist`, `dist.tar.gz`, or a Docker image.
- The application base path, public origin, database type, external services, reverse proxy, persistent directories, and service identity.
- Whether the requested action is a first install, an update, a migration, a restart, a rollback, or a recovery.

Do not run production writes, migrations, seeds, uploads, deployments, or destructive removal until the target and the user's authorization are explicit. If the target or data plan is unresolved, stop and ask for it.

## Deploy to an existing Studio

When the user asks to deploy this application to an existing NocoBase Studio, read [Deploy to Studio](references/deploy-to-studio.md) before building. It covers installing and signing in with the target Studio's `nb-studio` CLI, choosing an upload route, deploying and verifying the application. The application being deployed does not need its own business CLI.

Confirm the source application, target Studio, App ID and environment. A source directory, deployment directory and App ID can have different names; do not infer application identity from one of them. Follow the reference for Studio-managed configuration, storage and deployment; use the shared artifact and verification guidance below where applicable, not the standalone installation commands.

Deploying Studio itself to a server follows the standalone deployment workflow below.

## Prepare the source and artifact

In the application project root, use the project's lockfile and declared pnpm version:

```bash
pnpm install --frozen-lockfile
pnpm typecheck
pnpm test
pnpm lint
```

Run the checks that the project actually defines; do not invent a test or lint command when it is absent. Build for the destination platform:

```bash
pnpm build --target linux-x64 --node-version 24 --tar
```

`pnpm build` creates the production `dist` tree and installs or retargets production dependencies for the selected platform. The build is not tied to a mount path: the client uses relative asset URLs by default, or an absolute CDN prefix when configured below, and the server serves it at the `APP_BASE_PATH` it runs with, so one archive can be mounted at `/crm` or at the origin root. `--tar` additionally creates `storage/exports/dist.tar.gz`, containing `dist/` and `config.example.yml`, and the application's `Dockerfile` and `Dockerfile.dockerignore` when it has them; it does not contain the runtime configuration, database, uploads, or other business data. Extracting it produces exactly those paths rather than scattering `server/` and `node_modules/` into the directory you unpacked in. `config.example.yml` travels with it because a deployment has to write a `config.yml` before it can start, and the example is the only statement of what may go in it. Directories of executable shims are left out: a `.bin` entry points at a path on the machine that installed it, and a dangling one makes `pnpm install` in the extracted tree report a corrupt store rather than repair it. Without `--tar` no archive is produced, which is what you want when the build is only going to be run locally. Inspect the archive before transfer:

```bash
tar -tzf storage/exports/dist.tar.gz | head -30
```

For a direct server deployment, transfer the archive and extract it into the deployment root. Nothing needs installing there: `pnpm build` already ran `pnpm install --prod` inside `dist/`, and the archive carries the resulting `dist/node_modules`. `dist/package.json` stays in the tree so that the same command can be rerun inside `dist/` on the server if `node_modules` was left out of a copy; that is a repair, not a step of a normal deployment, and it is never run in the application source tree. For Docker, build with the application's own `Dockerfile` instead, which runs `pnpm build` inside the image; keep configuration and storage outside the image. Otherwise ensure the build target matches the server's architecture, libc, and Node ABI.

A `dist/` built for the wrong platform does not need a full rebuild: `pnpm nocobase dist retarget --target <platform> --node-version <major>` in the source checkout reinstalls only its native modules, and `pnpm nocobase dist check` re-verifies that everything the server imports is installed. On the server, run the application's commands as `node dist/cli/index.js <topic> <command>` from any directory, or `pnpm nocobase <topic> <command>` inside `dist/`; only runtime commands exist there. `.agents/skills/nocobase-app-development/references/cli.md` lists them and the flags that must not be added unasked.

### CDN assets

For templates supporting `CDN_BASE_URL` in `vite.config.ts`, set it before building, for example `CDN_BASE_URL=https://cdn.example.com/my-app/v1/ pnpm build --target linux-x64 --tar`. Use a full HTTPS URL ending in `/`. `pnpm build` reads the process environment before `.env.local` and `.env`; an unset, empty or whitespace-only value keeps `./`, and development ignores it. This sets Vite's build-time `base`: changing it requires rebuilding, not editing `config.yml` or restarting the server. See the application's README for its build entry points.

Upload the static files from `dist/client/` to that prefix with paths preserved, and allow cross-origin module and font requests from the application origin. Keep the complete client tree on the application server and serve HTML through it for runtime configuration injection. Vite-processed assets and static files addressed with `resolveAssetUrl` from `@nocobase/app-client`, including template logos, use the CDN. That helper reads Vite's build-time `BASE_URL` and falls back to the runtime mount path without an absolute CDN base. API/router paths and runtime-generated files keep using `resolveAppUrl` and `APP_BASE_PATH`; pointing them at a CDN does not upload them there. Verify entry scripts, logos, styles, lazy chunks and CSS assets against the chosen prefix; retain older assets while clients still reference them.

For a Docker source build, pass `--build-arg CDN_BASE_URL=https://cdn.example.com/my-app/v1/`; the host's `.env` does not enter the build. With `DIST=prebuilt`, configure the earlier `pnpm build` instead. A prebuilt image's build argument or `docker run -e CDN_BASE_URL=...` cannot change the compiled URLs.

## Decide the data operation

A deployment package carries code and production dependencies, not business data. Keep the existing database and storage mounts for an in-place update. When switching to a new database or server, perform a separate data migration or restore a coordinated backup before allowing users to write.

Before an update that can change schema, take a database and file backup and confirm the restore path. Ensure the database account can perform the configured migration and seed operations. Do not point a new instance at an empty database and describe the result as a data migration.

Read [database upgrades](../nocobase-app-upgrade/references/database-upgrades.md) when the update changes migrations or reports checksum drift, before starting the new runtime. It covers target-version upgrade notes, reviewed checksum repair, data prerequisites, and irreversible changes, including plugin migrations that the application's source diff does not show. `db repair` acknowledges source checksums only; it does not upgrade schema or recover a partially failed installation.

The default configuration uses managed schema migrations and automatic seeds. Determine whether the deployment should run them on startup. If migrations or seeds are run separately, set the corresponding automatic options to `false` and execute the supported CLI operation before starting the application. Record the migration and seed result; a successful process start alone is insufficient.

## Build and review production configuration

Prepare the complete runtime configuration before starting the service. At minimum review:

- Database dialect, host, port, database, credentials, schema and migration policy. A container's `localhost` means that container, not the host or another service. `DB_DIALECT`, `DB_HOST`, `DB_PORT`, `DB_DATABASE`, `DB_USERNAME`, `DB_PASSWORD`, `DB_SSL` and `DB_FILENAME` set the main connection from the environment, above `config.yml`.
- `secrets.keys` (or `SECRETS_KEYS=1:<key>`): the master keys the application encrypts stored secrets with and derives its sign-in and session keys from, current first, each at least 32 random bytes (`openssl rand -hex 32`). Keep them unchanged across restarts and upgrades, backed up apart from the database, and out of artifacts, source control, and logs. An application that already had `auth.secret` keeps it beside the keys so older authentication data still decrypts; adding `secrets.keys` to it, and every later change of the current key, signs every user out once. To rotate, put a new key first with a higher version, keep the old ones, run `node dist/cli/index.js secrets rotate` until `secrets status` reports nothing to reseal, then remove the old key.
- `users.initialAdmin.username`, `users.initialAdmin.email` and `users.initialAdmin.password` for a new empty user table, or `INITIAL_ADMIN_USERNAME`, `INITIAL_ADMIN_EMAIL` and `INITIAL_ADMIN_PASSWORD`. These settings apply only during the initial seed and do not reset an existing account. Replace the example's `admin123` before exposing the application.
- `app.sampleData` (`APP_SAMPLE_DATA=true`) loads the application's sample data while the database is first installed, and is ignored afterwards; leave it off for production.
- Registered runtime configuration variables are listed in `dist/variables.json`, which `pnpm build` writes: name, configuration path, description, whether it is a secret, whether a deployment must supply it, whether one may be generated, and whether it is read only on the first start. `pnpm nocobase config variables` prints the same list from the sources. Build parameters such as `CDN_BASE_URL` are documented separately in the application's README and do not appear in that runtime list.
- `APP_PUBLIC_ORIGIN` as the external scheme and host without the application path, and `APP_BASE_PATH` as the public mount path, read when the server starts; it defaults to `/main`.
- `APP_SERVER_HOST` and `APP_SERVER_PORT`, with containers normally listening on `0.0.0.0` and the proxy controlling external exposure.
- Persistent storage paths, file permissions, service identity, and any external database, object storage, mail, or callback settings.
- The `jobs` backend, which runs background tasks — Notification deliveries, plugin jobs — and scheduled jobs, Scheduler's included. Without `jobs.default` they run on the built-in memory adapter, which keeps its state in the process, reads it from `storage/jobs` at startup and writes it back when the service stops: it serves one process, every other process or instance would fire its own copy, and a process that is killed rather than stopped loses what changed since it started. For more than one instance set `jobs.default` to the `redis` configuration and its `connection`; that Redis must persist its data (AOF or RDB) and use `maxmemory-policy noeviction`, and it opens connections per scheduling plugin. Set `jobs.default: memory` to keep a single-instance deployment on memory without the startup warning.
- The `queue` backend, when the application or a plugin uses `@nocobase/queue`. Without `queue.default` queues run on the built-in memory configuration, one process, with pending jobs written under `storage/queue` when the service stops. For more than one instance set `queue.default` to the `redis` configuration, under the same Redis requirements; `queue.default: memory` keeps a single instance on memory without the warning.
- The optional `api` limits for every `/api` request: `api.bodyLimit` (such as `10mb`), `api.timeout` (such as `30s`) and `api.rateLimit` with `max` and `window` (such as `600` per `1m`), all off by default; `API_BODY_LIMIT` and `API_TIMEOUT` set the first two. The rate limit counts per client connection address in each process, so behind a reverse proxy every request shares the proxy's address and one budget, and each instance of a multi-instance deployment counts on its own; size `max` for that, or leave it off and limit at the proxy.

The reverse proxy must preserve the public `Host` and protocol headers, forward cookies, and support WebSocket `Upgrade` and `Connection` headers.

## Choose the deployment path

### Standalone Node.js

Extract the archive as the service user or transfer ownership to that user. Write the configuration by running `pnpm nocobase config init` inside `dist/`, which generates `config.yml` beside it from the `config.example.yml` the archive carries, with fresh secrets; it installs nothing, so a dialect whose driver the build does not include has to be added in the application sources and built again. Set its values with `pnpm nocobase config set`, and `--from-env` for passwords. Then run `pnpm nocobase config check` inside `dist/` on the target machine before the first start: it loads the configuration the way the service will, connects to every database but SQLite, and exits non-zero with the cause when something would stop the start. Keep `config.yml` and `storage/` beside `dist/`, configure `APP_CONFIG_FILE`, and run `node ./dist/server/standalone.js` through the service manager. Replace `dist` during an update while retaining configuration and storage. Do not start a second process against the same data directory.

### Standalone with app-installer

On a server without a container platform, prefer `@nocobase/app-installer` to running the archive by hand: it installs the archive into a directory of its own, writes `config.yml` and `app.env`, applies migrations, runs the application under pm2, and later upgrades to a new archive with a backup of every SQLite database and an automatic rollback when the new release fails to start. The server needs Node.js 24 and a global pm2, nothing from the project. The global `nocobase-app-installer` Skill drives it, and its `--help` documents every flag:

```bash
npx --registry=https://registry.npmjs.org @nocobase/app-installer@beta install /srv/nocobase/crm --archive /tmp/crm.tar.gz --origin https://apps.example.com
npx --registry=https://registry.npmjs.org @nocobase/app-installer@beta upgrade --dir /srv/nocobase/crm --archive /tmp/crm.tar.gz
```

Published product applications can also be deployed with an app-installer version whose `--help` offers `--app`: `install /srv/nocobase/crm --app @acme/crm@beta` explicitly selects the beta channel. This route needs pnpm 11+ and builds the published package on the server without a repository checkout, preserving its identity and version. An npm installation upgrades the same package with `upgrade --dir <root> --to beta`, an explicitly requested `--to <version or tag>`, or `--rebuild` (installed version). Read `source.kind` in `status` before choosing the upgrade command; an archive installation still needs `--archive`. The same persistent configuration, backup and rollback rules apply.

`install --base-path /crm` chooses the mount path and writes it to `app.env` as `APP_BASE_PATH`; without it the server default `/main` applies. Upgrades keep the path `app.env` names, and editing it there moves the application on its next start. The installer refuses an archive for another application, an older version or another machine. The same version built again deploys as a new release, so the version need not be bumped for each deployment. An archive from an `@nocobase/app-cli` that predates relocatable builds has its mount path compiled in and runs only at that path, so a mismatch is refused with `BASE_PATH_MISMATCH`; one older still, which records no build time, or a release whose `@nocobase/app-server` predates `APP_STORAGE_DIR`, is refused: upgrade the project's NocoBase packages and build again. Each application on the server gets its own directory, port and pm2 process.

### Standalone Docker

Build the image from the application root with its own `Dockerfile`: `docker build -t crm:<release> .`. The image is not tied to a mount path: it serves at `/main`, and `docker run -e APP_BASE_PATH=/crm` mounts it elsewhere, which the image's health check follows. `Dockerfile.dockerignore` must sit beside the `Dockerfile` — without it `config.yml`, `.env` and `storage/` enter the build context — and an application created before the template shipped them copies both from a newer template version. For another architecture use `docker buildx build --platform`; the build stage cross-targets native modules itself. To package a `dist/` already built, pass `--build-arg DIST=prebuilt` after `pnpm build --target linux-<arch>`; the image build rejects a `dist/` built for another platform, libc or Node major, or by an `@nocobase/app-cli` that predates relocatable builds, and never copies `dist/.env`. Use the source build for release images: a prebuilt `dist/` reflects the building machine's working tree. A server that has only the release archive builds the image from it alone, without the sources or a registry: extract it and run `docker build --build-arg DIST=prebuilt -t crm:<release> .` in the extracted directory, which holds the `Dockerfile`, its `.dockerignore`, `dist/` and `config.example.yml`; the archive has to be built for the server's platform (`pnpm build --target linux-x64 --tar`). `.env` is not carried into the image, so pass its settings as container environment variables.

Bind-mount the complete runtime configuration read-only at `/app/config.yml` and the persistent storage at `/app/storage`, writable by the image's `node` user (UID 1000), and run the container with `init: true`. The image has no pnpm: run application commands as `node dist/cli/index.js <command>`, for example `docker run --rm -v ./config.yml:/app/config.yml:ro <image> node dist/cli/index.js config check` before the first start. Validate the Compose file before starting. For a configuration file replacement, recreate the container so the process reads the new file. Keep the image, config, storage, and proxy changes separately identifiable.

## Verify the deployed application

Use the intended public origin for browser checks, including its scheme, host and base path. Follow [browser API compatibility](../nocobase-app-development/references/frontend/frontend-dev.md#browser-api-compatibility): UUID generation and copying text must work on ordinary HTTP, while other HTTPS-only capabilities must explain their requirements to users when unavailable. Verify those HTTP-compatible actions and unavailable-feature messages on an origin where `window.isSecureContext` is `false`; localhost and HTTPS can mask incompatibilities. Exercise actions through completion and inspect browser errors; a loaded page or healthy server is insufficient.

When the application offers copy buttons, include [copy and clipboard verification](../nocobase-app-development/references/frontend/frontend-dev.md#copy-and-clipboard-actions): copy from a representative page or dialog and paste into a normal input on HTTP. A success toast alone is not evidence that the clipboard changed.

Collect evidence for each item:

1. The process or container reports ready. The application answers `GET <APP_BASE_PATH>/api/healthz` with a JSON object whose `ok` is `true` (alongside the app name and base path).
2. The public URL, base path, static assets, page refresh, API requests, cookies, and WebSocket connections work through the real reverse proxy.
3. The configured administrator can sign in, and a normal user has the expected server-side permissions.
4. The application can read and write a known record in the intended database; the database is not an unexpected empty instance.
5. Upload and download a file if the application uses file storage.
6. Exercise one representative business operation end to end and confirm its expected result.
7. Restart the service or recreate the container and confirm records, files, configuration, and enabled runtime behavior remain available, including that scheduled jobs keep firing.

Record the exact artifact or image digest, configuration revision, database migration result, logs checked, and verification time.

## API documentation in production

A deployed application serves its OpenAPI document at `<APP_BASE_PATH>/api/swagger` and Swagger UI at `<APP_BASE_PATH>/api/swagger/docs`, with the Swagger UI files bundled in the application; nothing is fetched from a CDN, so it works without outbound network access. Only a signed-in session or a valid API key may read them, and anything else gets `401` with reason `API_DOCS_UNAUTHENTICATED`; an application without the authentication plugin registers no access check and answers `404`. Verify with `curl -H "x-api-key: <key>" https://<host><APP_BASE_PATH>/api/swagger` using a key the user supplies, and expect `401` without it.

There is no setting that makes the documentation public, and none that turns it off. It describes the endpoints' shape, not data, and every reader is already someone who can call those endpoints. Where policy requires that only some users see it, block `<APP_BASE_PATH>/api/swagger` and everything below it at the reverse proxy for the users or networks that should not reach it; do not change the application or its plugins for this.

## Update, rollback, and recovery

Before an update, review migration and configuration differences and take a backup. A code rollback creates or selects an older runtime; it does not undo database migrations, business writes, or a historical configuration snapshot. Confirm database compatibility before switching back.

For recovery, stop the affected service, prepare the code and runtime matching the backup, restore the database, files, configuration, stable secrets, and persistent mounts, then start and verify. Restore external databases and object storage to a coordinated point in time.

## Report the result

Return a concise deployment report with these sections:

- Target and deployment mode.
- Source revision and artifact or image digest.
- Configuration, database, migration and seed status.
- Representative business verification.
- Restart or recovery verification.
- Logs and checks performed.
- Unresolved risks, skipped checks, and required follow-up.

Never claim a production deployment, migration, rollback, or recovery was completed when it was only planned, accepted by an API, or tested locally.
