Production stack
There are two ways to run Manablox in production:
- A project of your own, written by
manablox create --preset docker. It has its own Dockerfile and compose file with exactly the plugins you picked, premium ones included, and a proxy in front. This is the usual way; see Use it as a dependency. - The published CMS images,
ghcr.io/manablox/cms-apiandghcr.io/manablox/cms-admin, with the compose file of the CMS repository (docker/compose.ymlin manablox-cms). They carry the core and the free plugins: fields, workflows, webhooks and the license plugin. The premium plugins are not in them; an instance that needs the website or AI plugin is a project of its own.
The rest of this page describes the second. The environment variables are the same in both, because every config reads them the same way.
cd docker # in a checkout of manablox-cms# docker/.env: the passwords, AUTH_SECRET, PUBLIC_URL, PUBLIC_API_URL, CORS_ORIGINSIMAGE_PREFIX=ghcr.io/manablox IMAGE_TAG=0.50.0 docker compose pullIMAGE_PREFIX=ghcr.io/manablox IMAGE_TAG=0.50.0 docker compose up -dWithout IMAGE_PREFIX compose builds the images from the checkout instead. Compose
brings up Postgres 18, Valkey, a one-shot migrate service, the management API, the
public API and the admin. To run on SQLite instead, drop the postgres service, set
DATABASE_URL=file:/data/db/manablox.db and mount one volume at /data/db into
migrate and both APIs; see Database. Every hostname,
secret and origin comes from the environment; nothing is baked into an image. The
variables are listed in Environment variables; the ones
a production instance must set are POSTGRES_PASSWORD, DATABASE_OWNER_PASSWORD,
DATABASE_APP_PASSWORD, AUTH_SECRET, PUBLIC_URL, PUBLIC_API_URL, CORS_ORIGINS, and
for the public API the space it serves (PUBLIC_SPACE, passed on as MANABLOX_SPACE).
Postgres runs with two roles: manablox_owner owns the schema and runs the migrations,
manablox_app is what the CMS logs in as and cannot change tables or delete audit
entries. An init script creates both on the first start; to split a database that runs
with one role, see Database roles.
| Service | What | Port on the host |
|---|---|---|
postgres | Postgres 18 | internal |
valkey | The shared cache and job queue | internal |
migrate | One-shot: applies the migrations as the owner role, grants the app role, then exits; both APIs wait for it | none |
api | The management API (cms-api) | API_PORT, 3000 |
public-api | The public API, same image, node apps/public-api/dist/main.js, listening on 3100 in the container | PUBLIC_API_PORT, 3001 |
admin | The admin (cms-admin), static files behind nginx that proxies the management API to api | ADMIN_PORT, 3002 |
For the premium plugins, set MANABLOX_LICENSE_KEYS for the management API and let it
reach the license server over outbound HTTPS; see
Premium plugin licenses.
Put a TLS-terminating proxy or a CDN in front; the admin needs a secure origin for push
notifications and for framing a frontend in the visual editor. Your own website is not
part of this file: build it as its own image. See
A Vite + Vue SSR frontend,
and point it at public-api. Spaces designed in the admin are served by the site process
of the website plugin; see Running the site process for its image,
its variables and HTTPS on the domains editors add.
Images
Every Manablox repository builds its own images and pushes them to
ghcr.io/manablox/<image>, tagged with the version (0.50.0, 0.50) and latest. See
CI and releases.
| Image | Built in | Runtime | Port |
|---|---|---|---|
cms-api | manablox-cms | Node 24, non-root; the management API, the public API and the migrations | 3000 |
cms-admin | manablox-cms | nginx, the admin | 80 |
site | manablox-plugin-website | Node 24; the site process of the website plugin | 3200 |
license-server | manablox-license-portal | Node 24; the license server of the premium plugins | 3100 |
license-portal | manablox-license-portal | nginx, the customer portal | 80 |
dev-docs | manablox-dev-docs | nginx, these pages (dev.manablox.io) | 80 |
user-docs | manablox-user-docs | nginx, the user guide (docs.manablox.io) | 80 |
website | manablox-website | nginx, the marketing website | 80 |
cms-apiis built fromdocker/Dockerfile.apiof the CMS repository in stages: the build stage builds the two API apps and the packages they use todistwith tsdown and points each package at itsdist, as the npm tarballs do. The runtime layer holds a production-only install plus that built code, runsnode apps/api/dist/main.jsas a non-root user, carries no sources, tsx or other build tooling, and declares aHEALTHCHECKagainst/readyz. Thepublic-apiservice runsnode apps/public-api/dist/main.jsfrom the same image and themigrateservicenode apps/api/dist/migrate.js: one image, three entrypoints. The management API loads fields, the license plugin, workflows and webhooks; switch a feature off per instance or space with itsfeatures.plugins.<id>control. The public API loads only the field types.cms-admin: the admin SPA with a history fallback. Hashed assets are immutable;index.htmlis never cached, so a deploy cannot pin clients to a stale bundle. The build’s.gzcopies are sent as they are (gzip_static); everything else is compressed on the fly. The admin calls the management API on its own origin, so nginx proxies it toMANAGEMENT_API_URL(defaulthttp://api:3000, an origin without a path or trailing slash; nginx fills it in when the container starts):/rpc,/api,/media,/upload,/transfer,/realtime(the live event stream, unbuffered, with WebSocket upgrades passed through),/plugins(plugin server routes) and/admin/plugins.jsonwith/admin/plugins/(the admin plugin bundles). Request bodies pass through unbuffered and without an nginx size limit; the API enforces its own. nginx sendsX-Forwarded-For,X-Forwarded-ProtoandX-Forwarded-Host, so add the admin container toTRUSTED_PROXIESfor client addresses (see Client addresses), and keep the admin’s origin inCORS_ORIGINS. SetMANAGEMENT_API_URLwhen the management API is not the compose serviceapi, for exampledocker run -e MANAGEMENT_API_URL=http://cms.internal:3000 ghcr.io/manablox/cms-admin:0.50.0. While the API is unreachable nginx answers 502 on those paths.site: the site process of the website plugin; see Running the site process.license-serverandlicense-portal: the license server of the premium plugins and its customer portal, not part of an instance. See Premium plugin licenses.dev-docs,user-docsandwebsite: static sites behind nginx. They talk to no Manablox instance. The site URL and base path are build arguments (for these pagesDOCS_SITE_URLandDOCS_BASE_PATH); they only reach canonical links and the sitemap.
In a checkout of the CMS repository, pnpm docker:build builds its two images:
pnpm docker:build # ghcr.io/manablox/cms-api:0.50.0 and ghcr.io/manablox/cms-admin:0.50.0pnpm docker:build --tag test admin # one image, another tagpnpm docker:build --prefix my.registry/me --pushThe other repositories have the same pnpm docker:build for theirs.
Scaling by role, health checks, backups and the security checklist are in Operations; how the images and the npm packages are published is in CI and releases.