A deployment is one Node process + PostgreSQL.
- The process serves the API under
/apiand the built admin as static files — one origin. - Background work (outbox, webhooks, mail) runs on timers in the same process, against queues in Postgres.
- No broker, worker, cache or search cluster.
Build, migrate, run
npm ci # includes devDependencies: @orthacms/cli, tsc, vite
npx orthacms build # server → dist/server, admin → dist/admin
npx orthacms migrate # release step, once, before the new version starts
npx orthacms start # runs dist/server/src/main.jsorthacms build flag | Builds |
|---|---|
| (none) | Server, then admin (an app with no apps/admin/index.html is API-only) |
--server | Server only |
--admin | Admin bundle only |
- Migrate as its own step, never at boot: migrations are not safe to run concurrently from several replicas.
orthacms migratecompiles the server first — it cannot run against a tree that does not compile.orthacms startrefuses, naming the missing file, if you have not built.- Inside the monorepo the equivalent is
npx nx run server:db:migrate. See the CLI.
Start from the app root
staticDir is join(process.cwd(), 'dist/admin'). Started elsewhere, the API works and the admin is a 404. The boot log warns when no index.html is found. Set WORKDIR (Docker) or WorkingDirectory= (systemd) to the app root.
Required environment
NODE_ENV=production # Secure cookie, API reference off
DATABASE_URL=postgresql://… # required
TRUST_PROXY=1 # behind one reverse proxy
ALLOWED_ORIGINS=https://cms.example.com # the admin's origin
MEDIA_LOCAL_ROOT=/data/media # a persistent volume (or configure object storage)| Variable | Why it must be right |
|---|---|
NODE_ENV | Only production marks the session cookie Secure. Values other than development/test/production refuse the boot |
TRUST_PROXY | Unset behind a proxy, the login rate limit is one bucket for everyone |
ALLOWED_ORIGINS | Default is http://localhost:4200; any other browser origin gets 403 Origin not allowed on writes |
API_DOCS | true publishes /reference and GraphiQL from production |
Everything else: configuration. Variables exported in the environment override a .env shipped in the image.
Docker
# Dockerfile, at the app root
FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npx orthacms build
ENV NODE_ENV=production
EXPOSE 3000
CMD ["npx", "orthacms", "start"]NODE_ENV=production is set after npm ci, so the devDependencies the CLI needs (orthacms migrate rebuilds) are installed.
# docker-compose.yml — Postgres, a one-shot migration, the app
services:
db:
image: postgres:17-alpine
environment:
POSTGRES_USER: orthacms
POSTGRES_PASSWORD: change-me
POSTGRES_DB: orthacms
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U orthacms']
interval: 5s
retries: 10
migrate:
build: .
command: ['npx', 'orthacms', 'migrate']
environment:
DATABASE_URL: postgresql://orthacms:change-me@db:5432/orthacms
depends_on:
db:
condition: service_healthy
app:
build: .
ports:
- '127.0.0.1:3000:3000'
environment:
DATABASE_URL: postgresql://orthacms:change-me@db:5432/orthacms
TRUST_PROXY: '1'
ALLOWED_ORIGINS: https://cms.example.com
MEDIA_LOCAL_ROOT: /data/media
ORTHACMS_ROOT_ADMIN_EMAIL: ops@example.com
ORTHACMS_ROOT_ADMIN_PASSWORD: change-me-too
volumes:
- media:/data/media
depends_on:
migrate:
condition: service_completed_successfully
volumes:
pgdata:
media:systemd
# /etc/systemd/system/orthacms.service
[Service]
WorkingDirectory=/srv/my-cms
EnvironmentFile=/etc/orthacms.env
ExecStart=/usr/bin/npx orthacms start
Restart=on-failure
User=orthacms
[Install]
WantedBy=multi-user.targetThe reverse proxy
Serve the admin and the API from one origin. The session cookie is httpOnly + SameSite=lax; split origins give a sign-in that “succeeds” and does not stick.
server {
server_name cms.example.com;
client_max_body_size 50m; # ≥ MEDIA_MAX_UPLOAD_BYTES; nginx's default is 1m
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}X-Forwarded-Forfrom the proxy andTRUST_PROXYin the app — both are needed.- Without the body-size line, most image uploads get a 413 that never reaches Ortha.
Health checks
There is no health endpoint. Probe GET / (the admin’s index.html), or add a plugin if you need a database check.
curl -fsS -o /dev/null https://cms.example.com/ && echo upStorage
The default media root is local disk — lost when a container is replaced. Mount a volume or use object storage: media storage.
Release order
- Bump every
@orthacms/*package to one version,npm ci orthacms buildorthacms migrate- Start the new version
- Stop the old one
Migrations are forward-only. A rollback is the previous app version on an already-migrated database — fine for additive migrations, not for destructive ones. Read the SQL first. See upgrading.
Read next
- Configuration — every environment variable
- Security — what is and is not defended
- Database and migrations — plugin order, what grows
- The CLI reference — every command and flag