Local runtime instructions
Requirements:
- Docker compose + Docker
- For Mac, OrbStack is recommended instead of Docker Desktop
- mise
- add trust to the project folder with
mise trust - install repo-managed tools with
mise install - activate mise in your shell (for example
eval "$(mise activate bash)"or your shell equivalent) so repo-managed tools win onPATH dev:initensures thepnpmversion pinned inpackage.jsonis installed for the active mise-managed Node toolchain when missing
- add trust to the project folder with
Steps
-
Initialize the local stack (first run, or whenever you want to rebuild local bootstrap state)
mise run dev:initdev:initis the first half of the local startup flow. It:- creates
.envfrom.env.dockerwhen.envis missing - stops immediately until
.envcontains non-emptyDC_SUPERADMIN_EMAILandDC_SUPERADMIN_PASSWORDvalues - prepares the local Postgres bind-mount folders so
medusa-dbcan bootstrap while the database process still runs as thepostgresuser - runs
mise run dev:install - starts shared resources
- runs Medusa DB migrations before starting
medusa-be - creates the Medusa admin user from
DC_SUPERADMIN_EMAIL/DC_SUPERADMIN_PASSWORD - runs
mise run dev:medusa:seed
If you also use the Zane-targeted helper scripts, copy
.envto.env.zaneonce and keep Zane-specific values there. - creates
-
Bring the full local stack to steady state
mise run devdevis the normal incremental/start-or-heal path after initialization. It runs in order:- resources (
medusa-db,medusa-valkey,medusa-minio,medusa-meilisearch) - Meilisearch key provisioning
- Medusa DB migrations
medusa-ben1
Common follow-up tasks stay on the public
misesurface:- rerun first-time bootstrap from the top:
mise run dev:init - non-destructive restart from a stopped stack:
mise run dev:fresh - stop the stack:
mise run dev:down - destructive reset that also removes named volumes:
mise run dev:down:volumes - enable the optional operator profile:
mise run dev:operator - rerun Postgres bootstrap:
mise run dev:postgres:bootstrap - verify Postgres bootstrap idempotency:
mise run dev:postgres:bootstrap:verify - verify hardened Postgres grants:
mise run dev:postgres:grants:verify
During
devstartup,.envhandling is opinionated:-
if
DC_MEILISEARCH_BACKEND_API_KEY/DC_N1_NEXT_PUBLIC_MEILISEARCH_API_KEYare empty, values are written -
if existing values differ, you are prompted to
overrideorkeep -
Meilisearch key policy/provisioning is owned by
apps/new-engine-ctl meili-api-credentials; onlymise run devperforms.envsync logic -
mise run devcalls provisioning against host URLhttp://127.0.0.1:7700by default (override viaMISE_DEV_MEILI_URL) soDC_MEILISEARCH_HOSTcan remain container-internal (http://medusa-meilisearch:7700) -
Postgres role bootstrap (
medusa_app,medusa_dev,zane_operator) runs automatically frommedusa-dbstartup via/usr/local/bin/run-postgres-with-bootstrap.sh -
MinIO bootstrap now runs inside
medusa-miniostartup (idempotent): it ensuresDC_MINIO_BUCKETexists, enforces public object reads, and provisions a non-root Medusa runtime key with bucket-scoped permissions limited to the Medusa bucket -
Meilisearch now starts through an in-image bootstrap wrapper (idempotent, swarm-safe) before serving traffic
-
medusa-miniouses dedicated MinIO bootstrap env (MINIO_ROOT_*+MINIO_MEDUSA_*) to avoid deprecated MinIO server env collisions -
medusa-meilisearchcontinues to use shared Medusa env plus service-specific Meili env -
medusa-dbnow ownszane_operatorrole/template bootstrap and derives that target from the canonicalDC_ZANE_OPERATOR_*DB settings -
medusa-dbhealth now means both Postgres readiness and completed role/template bootstrap; operator startup waits on that full convergence -
mise run dev:operatorreruns Postgres bootstrap in the runningmedusa-dbcontainer with the currentDC_ZANE_OPERATOR_*values before startingzane-operator, so enabling or rotating operator DB creds does not require a DB restart -
Default
mise run devdoes not require operator credentials; operator flow is explicit opt-in and requires:DC_ZANE_OPERATOR_API_AUTH_TOKEN=<replace-with-long-random-token>DC_ZANE_OPERATOR_PGPASSWORD=<replace-with-strong-db-password>DC_ZANE_OPERATOR_DB_PREVIEW_APP_PASSWORD_SECRET=<replace-with-long-random-secret>
To exercise the deploy-wrapper endpoints locally as well, also set:
DC_ZANE_OPERATOR_ZANE_BASE_URL=<upstream-zane-url>DC_ZANE_OPERATOR_ZANE_CONNECT_BASE_URL=<optional-container-reachable-upstream-url>DC_ZANE_OPERATOR_ZANE_CONNECT_HOST_HEADER=<optional-host-header-for-connect-url>DC_ZANE_OPERATOR_ZANE_USERNAME=<upstream-zane-username>DC_ZANE_OPERATOR_ZANE_PASSWORD=<upstream-zane-password>
First-time upstream ZaneOps setup assumptions for local deploy testing:
DC_ZANE_OPERATOR_ZANE_BASE_URLmust point at the upstream ZaneOps UI/API root you actually log into, for examplehttp://localhost:3000DC_ZANE_OPERATOR_ZANE_CONNECT_BASE_URL/...HOST_HEADERshould stay empty by default; they are only needed when the deployedzane-operatorcannot reach the public Zane hostname directly, such as this local Docker-based Zane stackDC_ZANE_OPERATOR_ZANE_USERNAME/DC_ZANE_OPERATOR_ZANE_PASSWORDare the login credentials for that ZaneOps instance;zane-operatoruses session + CSRF login upstream, not a direct Zane token- create one canonical Zane project and note its slug; local CI-style deploy tests use that slug as
ZANE_PROJECT_SLUG - each Zane project gets a protected
productionenvironment by default; preview clones in this repo always use that environment as the base - service names in that Zane project must match
apps/new-engine-ctl/config/stack-manifest.yamlcurrently:medusa-db,medusa-valkey,medusa-minio,medusa-meilisearch,medusa-be,payload,n1 - preview environments are derived in CI script space as
pr-<number>by default - preview teardown is explicit in this repo's CI flow; do not rely on built-in Zane preview auto-teardown for these cloned environments
When you run the deploy scripts manually later, export:
ZANE_OPERATOR_BASE_URL=http://localhost:8082ZANE_OPERATOR_API_TOKEN=<same value as DC_ZANE_OPERATOR_API_AUTH_TOKEN>ZANE_PROJECT_SLUG=<your-zane-project-slug>ZANE_PRODUCTION_ENVIRONMENT_NAME=production
If your Postgres volume already existed before this change, rerun Postgres bootstrap once after setting the operator password:
mise run dev:postgres:bootstrapOptional idempotency check (runs bootstrap twice):
mise run dev:postgres:bootstrap:verifyOptional grant hardening check (read-only verification):
mise run dev:postgres:grants:verify- Existing DB migration note: bootstrap includes idempotent legacy-object migration from
publicschema intoDC_MEDUSA_APP_DB_SCHEMA(defaultmedusa), with conflict fail-fast if same object already exists in target schema. - Medusa BE DB connection is derived from
DC_MEDUSA_APP_DB_*; keep those on app credentials (medusa_app), not superuser credentials MEDUSA_DATABASE_SCHEMA/DATABASE_SCHEMAare derived fromDC_MEDUSA_APP_DB_SCHEMAand must stay aligned with app schema grantsmedusa-dbstarts with-c file_copy_method=clone; zane-operator preview cloning usesCREATE DATABASE ... STRATEGY=FILE_COPY
- resources (
Cloud predeploy note
zane_operator role/template bootstrap is now owned by medusa-db, not zane-operator.
Operational consequence:
- rotating
zane-operatorDB credentials requires updating both:medusa-dbbootstrap envszane-operatorruntime envs
- then redeploy
medusa-dbfirst so it can reconcile the role/template state before redeployingzane-operator
GitHub approval requirement
Main-lane deploys now resolve downtime risk once after affected-service filtering.
If any affected service is marked with ci.zane.downtime_risk: true in apps/new-engine-ctl/config/stack-manifest.yaml, the workflow expects a GitHub environment named zaneops-main-downtime.
To make the workflow actually pause for human approval:
- create the
zaneops-main-downtimeenvironment in GitHub - configure required reviewers on that environment
Without required reviewers, the approval job still runs, but it will not enforce a real manual pause on its own.
Manual live .env updates (not automated)
When DB env wiring changes, apply these actions manually on the live .env file:
-
Open live
.env. -
Set medusa-be DB connection values to APP credentials (
medusa_app-style account), not superuser credentials. -
Ensure DB host, port, and database match the compose service defaults used in your environment.
-
Keep operator-only credentials separate from app credentials.
-
Restart services that consume
.env. -
Validate with one read and one write operation from medusa-be.
-
Migrate database (if needed)
- (optional)
medusaschema needs to exist, which it should, unless it was manually dropped
mise run dev:medusa:migrate - (optional)
-
Create user for medusa admin (if needed)
EMAIL=[some@email.com] PASSWORD=[PASSWORD] mise run dev:medusa:user:create -
Prepare file storage (automatic in compose; manual fallback only if needed)
mise run dev:medusa:minio:init -
Seed initial data (only first time)
- seeded data also add regions that are required to be set
- optionally this step can be skipped, but you need to manually add at least 1 region in medusa BE settings page
mise run dev:medusa:seed -
Create & set PUBLISHABLE_API_KEY for Store front (only first time)
- Restart services (commands below)
- Go to localhost:9000/app
- Login via user created in previous step
- Go to settings -> Publishable API Keys
- Create or copy existing key
- Update DC_N1_NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY in .env
- Restart services
mise run dev:down mise run dev
8b. Verify scoped Meilisearch keys (manual helper only)
* local via mise:
shell mise run dev:meili:verify
* dev:meili:verify remains a thin helper because the current contract check is “compare provided key values to the active CLI-managed outputs”
* local .env sync still uses scripts/dev/provision-meili-keys.sh, but that helper is now just a thin wrapper over apps/new-engine-ctl meili-api-credentials
* main-lane Meili API credential reconciliation now happens inside deploy orchestration, not in a standalone main prepare phase
* the remaining scripts/dev/* Meili commands are helper surfaces, not policy owners
* provider policy lives in apps/new-engine-ctl/config/stack-inputs.yaml, not in shell scripts
- Explore local envs
- N1 FE should be available at:
- Medusa BE should be available at:
- Minio console should be available at:
- localhost:9003
- https://admin.minio.localhost
- credentials:
DC_MINIO_ROOT_USER/DC_MINIO_ROOT_PASSWORD(defaults:minioadmin/minioadmin) - Medusa runtime should use
DC_MINIO_ACCESS_KEY/DC_MINIO_SECRET_KEY(non-root identity; object-level policy only)
- credentials:
- Meilisearch console should be available at:
- localhost:7700
- https://admin.meilisearch.localhost
- credentials:
DC_MEILISEARCH_MASTER_KEY - backend key:
DC_MEILISEARCH_BACKEND_API_KEY(scoped, server-only) - frontend key:
DC_N1_NEXT_PUBLIC_MEILISEARCH_API_KEY(read-only search key, never master) - (optional) if plugin was disabled before adding products:
mise run dev:medusa:meili:reseed
- credentials:
- zane-operator should be available at:
- localhost:8082/healthz
- optional local-stack Caddy route: https://admin.zane-operator.localhost/healthz
- bearer token:
DC_ZANE_OPERATOR_API_AUTH_TOKEN
- bearer token:
- Redis compatible ValKey storage can be connected at
localhost:6379- password:
DC_VALKEY_PASSWORD(default in.env:valkey_dev_change_me)
- password:
- Postgres DB can be connected at
localhost:5432- default credentials:
root/root - adminer can be accessed on localhost:8081
- default credentials:
- (1) Caddyfile currently works inside of docker, and SSL cert is not exposed to host system,
Admin for Medusa BE fails to connect websockets for Vite server due to SSL errors when visiting
https://admin.medusa.localhost/app.
- Optional steps
- Due to Server side rendering on FE and Client side rendering on BE for images, BE images are broken unless you
edit hosts file on your host machine and add
127.0.0.1 medusa-miniorecord- the issue is described here: curl/issues/11104
Local Postgres 18 Upgrade (Safe Path)
Postgres Docker image 18+ changed its default PGDATA layout. This repo now pins Postgres via
docker/development/postgres/Dockerfile (currently postgres:18.1-alpine) and stores cluster files in
./.docker_data/db18 (mounted to /var/lib/postgresql, with PGDATA=/var/lib/postgresql/18/docker).
If you already have local data in ./.docker_data/db from Postgres <18, run:
mise run dev:postgres18:migrate
What the migration script does:
- stops
medusa-db(if running) - creates a physical backup archive of
./.docker_data/db - creates a logical SQL backup using
pg_dumpall - restores into a fresh Postgres 18 data dir at
./.docker_data/db18 - leaves old data untouched for rollback
After migration:
mise run dev
Rollback path:
- keep using
./.docker_data/dbwith a Postgres 17 image - restore from the generated SQL/tar backups in
./.docker_data/backups/postgres18-migration
When migration looks good and you want to keep only the PG18 state:
mise run dev:postgres18:verify
mise run dev:postgres18:finalize
mise run dev:postgres18:verify checks that old cluster DBs/roles exist in the new cluster and compares structure, sequence values, and per-table row counts.
If old ./.docker_data/db cannot be started, it falls back to verifying against the newest
./.docker_data/backups/postgres18-migration/pg_dumpall_*.sql backup.
mise run dev:postgres18:finalize runs the same verification and then deletes ./.docker_data/db and ./.docker_data/backups/postgres18-migration.
Testing Production Build Locally
To test the production Docker build locally:
mise run dev:prod
Use mise run dev:prod:no-cache when you need to bypass Docker layer cache.
This builds a production-optimized image and starts the container. Access the admin at:
- https://admin.medusa.localhost/app (requires HTTPS for session cookies)
Note: Production mode uses secure cookies, so you must access via HTTPS (Caddy) rather than http://localhost:9000.
Note: mise run dev:prod now starts medusa-be, waits for health, regenerates apps/n1/src/data/static/categories.ts from Medusa data, then builds the n1 prod image.
ZaneOps Deploy Setup
Active Zane deployment for this repo is no longer driven by a checked-in swarm compose file. The supported setup is:
- create one canonical Zane project with the service names from
apps/new-engine-ctl/config/stack-manifest.yaml - configure the shared production-environment variables and per-service env blocks described in
apps/zane-operator/README.md; the bootstrap helper derives that contract fromapps/new-engine-ctl/config/stack-manifest.yamlandapps/new-engine-ctl/config/stack-inputs.yaml - let CI orchestrate preview/main deploys through
zane-operator - config ownership is split intentionally:
stack-manifest.yaml= service graph/deploy topologystack-inputs.yaml= runtime/env/provider materialization and lane runtime policy
- deploy-to-verify runtime-provider handoff is generic via
runtime_provider_outputs_json(plusruntime_provider_output_keys_csv), not Meili-only output fields
For first-time local Zane setup, follow:
apps/zane-operator/README.md
Use .env for local compose/runtime and .env.zane for Zane-targeted helper scripts such as:
- local CI-parity lane helpers over CTL + deployed
zane-operator:mise run dev:zane:mainmise run dev:zane:preview
- manual bootstrap helpers for the local Zane plane:
scripts/dev/setup-zane-project.shscripts/dev/refresh-zane-template-db.shmise run dev:zane:project:syncmise run dev:zane:template-db:sync
The lane helpers and bootstrap helpers are different surfaces:
dev:zane:main/dev:zane:previeware staged local wrappers that build CTL and call the active lane commands against a running deployedzane-operator; they do not authenticate directly to upstream Zane- the bootstrap helpers authenticate to upstream Zane in shell, capture normalized inspect JSON, call the CTL bootstrap plan surface, and execute only the resulting local/manual transport
- those bootstrap helpers also reconcile the repo-owned shared env contract and managed public URL set defined by the CTL/bootstrap inputs rather than copying ambient
.env.zaneservice URLs into Zane
Planning for those rare-use local Zane bootstrap helpers exists in CTL:
node apps/new-engine-ctl/dist/cli.js bootstrap zane-project plan --inspect-json /tmp/zane-project-inspect.jsonnode apps/new-engine-ctl/dist/cli.js bootstrap preview-template-db plan --inspect-json /tmp/zane-template-db-inspect.json
Those planning commands are read-only and intentionally separate from the active CI deploy surface. For the manual local Zane plane, shell remains the runtime entrypoint for upstream Zane auth/session handling and raw API reads; CTL consumes normalized inspect JSON so the repo-owned plan stays centralized without teaching CI-oriented CTL paths to talk to upstream Zane directly.
For lane helpers, prefer .env.zane or explicit script flags over ad hoc exported shell vars. The scripts derive or validate the operator endpoint and token from their own inputs rather than treating exported ZANE_OPERATOR_BASE_URL alone as the documented contract.
For Zane-targeted helpers, managed public service URLs are derived from the route contract rather than copied from ambient .env.zane frontend URL values.
mise run dev:zane:template-db:sync is also the first-time setup path for the preview template DB: it creates the configured template DB when missing and refreshes it from the chosen source DB when it already exists.
WrSearch
✨ Your new, shiny Nx workspace is almost ready ✨.
Learn more about this workspace setup and its capabilities
or run npx nx graph to visually explore what was created. Now, let's get you up to speed!
Finish your CI setup
Click here to finish setting up your workspace!
Generate a library
npx nx g @nx/js:lib packages/pkg1 --publishable --importPath=@my-org/pkg1
Run tasks
To build the library use:
npx nx build pkg1
To run any task with Nx use:
npx nx <target> <project-name>
These targets are
either inferred automatically
or defined in the project.json or package.json files.
More about running tasks in the docs »
Versioning and releasing
To version and release the library use
npx nx release
Pass --dry-run to see what would happen without actually releasing the library.
Keep TypeScript project references up to date
Nx automatically updates
TypeScript project references in tsconfig.json
files to ensure they remain accurate based on your project dependencies (import or require statements). This sync is
automatically done when running tasks such as build or typecheck, which require updated references to function
correctly.
To manually trigger the process to sync the project graph dependencies information to the TypeScript project references, run the following command:
npx nx sync
You can enforce that the TypeScript project references are always in the correct state when running in CI by adding a step to your CI job configuration that runs the following command:
npx nx sync:check
Install Nx Console
Nx Console is an editor extension that enriches your developer experience. It lets you run tasks, generate code, and improves code autocompletion in your IDE. It is available for VSCode and IntelliJ.
Useful links
Learn more:
- Learn more about this workspace setup
- Learn about Nx on CI
- Releasing Packages with Nx release
- What are Nx plugins?
And join the Nx community:
ZCLI (Zerops zCLI)
https://github.com/zeropsio/zcli
curl -L https://zerops.io/zcli/install.sh | sh