Beta . Ortha CMS is in public beta. Every plan is free until it ends.
Features Ortha CMS on GitHub

Deployment

Getting Ortha onto your own infrastructure and keeping it there.

Documents 0.13.1 Updated Edit this page Report a problem

On this page

A deployment is one Node process + PostgreSQL.

  • The process serves the API under /api and 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.js
orthacms build flagBuilds
(none)Server, then admin (an app with no apps/admin/index.html is API-only)
--serverServer only
--adminAdmin bundle only
  • Migrate as its own step, never at boot: migrations are not safe to run concurrently from several replicas.
  • orthacms migrate compiles the server first — it cannot run against a tree that does not compile.
  • orthacms start refuses, 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)
VariableWhy it must be right
NODE_ENVOnly production marks the session cookie Secure. Values other than development/test/production refuse the boot
TRUST_PROXYUnset behind a proxy, the login rate limit is one bucket for everyone
ALLOWED_ORIGINSDefault is http://localhost:4200; any other browser origin gets 403 Origin not allowed on writes
API_DOCStrue 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.target

The 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-For from the proxy and TRUST_PROXY in 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 up

Storage

The default media root is local disk — lost when a container is replaced. Mount a volume or use object storage: media storage.

Release order

  1. Bump every @orthacms/* package to one version, npm ci
  2. orthacms build
  3. orthacms migrate
  4. Start the new version
  5. 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.