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.
npx skills add https://github.com/nocobase/nocobase --skill nocobase-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.
Before building or changing a server, record:
dist, dist.tar.gz, or a Docker image.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.
When the user asks to deploy this application to an existing NocoBase Studio, read Deploy to Studio 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.
In the application project root, use the project's lockfile and declared pnpm version:
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:
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:
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.
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.
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 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.
Prepare the complete runtime configuration before starting the service. At minimum review:
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.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.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. Install and register the commercial @nocobase/app-plugin-jobs-redis Server plugin on every instance before selecting Redis; define the named jobs configuration with adapter: redis. Queue Redis is a separate capability and is unchanged.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.Redis Queue support requires installation and explicit Server registration of @nocobase/app-plugin-queue-redis/server. The OSS templates declare Memory only. Add the plugin's typed Redis configuration to the existing queue section before selecting it; Jobs configuration is independent. Unselected legacy Redis entries do not prevent Memory startup, but selecting Redis without its implementation fails explicitly.
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.For every required commercial backend package, automatically probe its metadata at https://pkg-v3.nocobase.com/ using existing credentials and command-local global/scoped registry overrides, check runtime/peer compatibility and install/register an authorized version without prompting. Follow the installed App development Skill's references/plugins.md commercial package workflow. If access is unavailable, leave a brief conversation notice, distinguish authorization denial from credentials/network/version issues, and continue independent preparation without waiting for the user. Keep dependent deployment or scaling pending and existing working configuration intact; do not select an unregistered Redis adapter or claim Memory satisfies multi-instance requirements.
When upgrading an application generated from an older template, add adapter: redis explicitly to every Redis Jobs entry in config.yml that previously relied on the template's TypeScript defaults. Templates now provide only Memory defaults; jobs.redis.connection alone does not identify a Redis backend. Preserve existing connection, namespace and scope values, and install and register the commercial Server plugin before selecting those configurations.
The reverse proxy must preserve the public Host and protocol headers, forward cookies, and support WebSocket Upgrade and Connection headers.
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.
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:
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.
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.
Use the intended public origin for browser checks, including its scheme, host and base path. Follow 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: 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:
GET <APP_BASE_PATH>/api/healthz with a JSON object whose ok is true (alongside the app name and base path).Record the exact artifact or image digest, configuration revision, database migration result, logs checked, and verification time.
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.
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.
Return a concise deployment report with these sections:
Never claim a production deployment, migration, rollback, or recovery was completed when it was only planned, accepted by an API, or tested locally.
Take nocobase/nocobase-deployment from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.
The instructions reference npm, npx, docker.
Without those the skill loads but fails at the first command.