Deployment
CairnCMS is a Node.js application packaged as a Docker image. The recommended deployment shape is to run the official image alongside your database and (optionally) Redis, with the platform you already use for everything else: Docker Compose on a single host, Kubernetes, ECS, Fly, Railway, Render, or any other container runtime.
This page covers how the image is built, what the cairncms init scaffold gives you, where the scaffold ends and production decisions begin, and the patterns for running CairnCMS at scale.
The official image
Section titled “The official image”The CairnCMS image is published on two registries:
cairncms/cairncmson Docker Hubghcr.io/cairncms/cairncmson GitHub Container Registry
Tags follow the project’s release versions (for example, cairncms/cairncms:1.0.0). A latest tag tracks the latest stable release. Multi-arch images are published for linux/amd64 and linux/arm64.
The image is based on node:22-alpine, runs as the unprivileged node user, and exposes port 8055. It contains:
- the compiled API
- the built admin app, served by the API
- a CLI bin (
cairncms) for migrations, bootstrap, user management, and config workflows
By default the image expects state at three paths inside the container:
/cairncms/database— SQLite file location, when using SQLite/cairncms/uploads— the local-disk storage backend/cairncms/extensions— custom extensions, migrations, and email templates
The default CMD runs cairncms bootstrap (which ensures system tables exist and applies pending migrations), then cairncms start to serve requests.
The init scaffold
Section titled “The init scaffold”The fastest way to a working local stack is the scaffold:
npx cairncms init my-cmsThis creates my-cms/cairncms/ with three files relevant to deployment:
docker-compose.yml— three services: PostgreSQL with PostGIS, Redis, and CairnCMS. Mounts volumes for uploads, extensions, snapshots, and config-as-code..env— random secrets, a default admin account, and the database connection details. Gitignored.README.md— quick reference for the stack.
The scaffold is for local development and content modeling. It is not a production-ready template. See the next section for what to change before deploying.
From scaffold to production
Section titled “From scaffold to production”The scaffold makes choices that work locally but should be revisited for any environment beyond a developer laptop:
Database image architecture
Section titled “Database image architecture”The scaffold’s database service uses postgis/postgis:16-3.4-alpine, which is linux/amd64 only. On ARM hosts (AWS Graviton, ARM-based Kubernetes nodes, Apple Silicon production), pull a multi-arch image instead. Plain postgres:16-alpine is multi-arch and works for any project that does not use geometry fields.
Secret management
Section titled “Secret management”The scaffold writes random secrets to a local .env file. For production:
- Source
KEY,SECRET,DB_PASSWORD, and any provider credentials from a secret store (your platform’s secret manager, HashiCorp Vault, AWS Secrets Manager, or similar). - Do not commit
.envto source control. The scaffold gitignores it; preserve that. - Rotate
SECRETcarefully. Rotating it invalidates all existing sessions, which is sometimes the goal but should be intentional.
Public URL
Section titled “Public URL”The scaffold defaults PUBLIC_URL to http://localhost:<port>. Set it to the externally-reachable URL — https://cms.example.com, including scheme — before any deployment. Asset URLs, redirect targets, and email links are all built from this value.
The image does not terminate TLS. Run a reverse proxy (Caddy, Traefik, nginx) or a managed load balancer in front of CairnCMS to handle HTTPS. Set IP_TRUST_PROXY=true (the default) so CairnCMS reads X-Forwarded-For for the real client IP.
Cookie security
Section titled “Cookie security”For production over HTTPS:
REFRESH_TOKEN_COOKIE_SECURE=trueREFRESH_TOKEN_COOKIE_SAME_SITE=strictThe scaffold defaults to false/lax for local dev. Override these explicitly in your production environment.
Persistent volumes
Section titled “Persistent volumes”The scaffold’s cairncms service mounts four directories:
./uploads— file bytes for the local storage backend./extensions— custom extensions, migrations, and email templates./snapshots— schema and config snapshot output (used bycairncms schemaandcairncms configCLI commands)./config— config-as-code working directory
In production, these need to be backed by durable storage:
- For uploads, prefer a remote storage backend (S3, GCS, Azure Blob, Cloudinary) over a mounted volume. Files outlive container instances; the storage backend is the durability surface, not the volume.
- For extensions, the directory should be part of your container image or pulled in at build time, not edited in production.
- For snapshots and config, these are output paths used by CLI workflows. Mount them only on the host where you run those commands.
Running without Docker
Section titled “Running without Docker”If you prefer to run CairnCMS directly on a host:
- Install Node.js 20 (or newer) on the host.
- Install the CairnCMS package:
npm install -g cairncms(or use the source tree if you’re building from this repo). Thecairncmspackage is the one that registers thecairncmsCLI binary. - Provide the configuration through a
.envfile or environment variables (see Configuration). - Run
cairncms bootstraponce to install system tables and apply migrations. - Run
cairncms start(typically under a process supervisor like systemd or PM2).
The Docker image is a packaging convenience, not a hard requirement.
The bootstrap and start pattern
Section titled “The bootstrap and start pattern”CairnCMS separates initialization from serving:
cairncms bootstrapensures the database has the system tables, applies pending migrations, and (on first run) creates a default admin user. Idempotent — safe to run on every deploy.cairncms startboots the API process. Does not run migrations on its own; it only validates that all known migrations have been applied and warns if any are missing.
The official Docker image runs bootstrap then start from its CMD, so a freshly-deployed container ends up with up-to-date schema and a running server. For platforms that expect a long-running process, this is fine. For platforms that want a separate migration step, run cairncms bootstrap as a one-shot job before scaling up the long-running service.
For Kubernetes specifically, an init container running cairncms bootstrap followed by the main container running cairncms start is the cleanest split. The init container can also act as a sanity check that fails the deploy if migrations cannot be applied.
Reverse proxy notes
Section titled “Reverse proxy notes”A typical proxy configuration:
- Forward
/adminand the API endpoints (/items,/files,/auth, etc.) to CairnCMS. - Set
Host,X-Forwarded-Proto, andX-Forwarded-Forheaders. - Cache static asset routes (
/admin/assets/*) aggressively at the proxy.
Sample Caddy snippet:
cms.example.com { reverse_proxy cairncms:8055}Sample nginx snippet:
server { listen 443 ssl http2; server_name cms.example.com; location / { proxy_pass http://cairncms:8055; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }}Resource sizing
Section titled “Resource sizing”CairnCMS itself is light. The platform’s resource needs are dominated by:
- the database (sized to your dataset and concurrency)
- file storage (sized to your asset volume)
- transformation throughput, when serving on-the-fly image transforms
A small project (single instance, modest traffic, S3-backed storage, Postgres) runs comfortably on 1 vCPU and 1 GB RAM for the CairnCMS container. Image transformations spike CPU and memory on demand; a heavy public-facing site benefits from caching transformed assets at the CDN layer. The ASSETS_TRANSFORM_* configuration variables control concurrency limits — see Configuration.
Multi-instance deployments
Section titled “Multi-instance deployments”To run CairnCMS behind a load balancer with more than one instance:
- Database: a single shared instance; CairnCMS does not expect to be the only writer, but it is the source of truth for its system tables.
- Storage: use a remote backend (S3-compatible, GCS, Azure, or Cloudinary). Local-disk storage does not survive container restarts and cannot be shared across instances.
- Cache and rate limiter: switch to Redis-backed stores so limits and cached responses are shared across instances. See Configuration for
CACHE_REDIS,RATE_LIMITER_REDIS, andMESSENGER_REDIS. - Messenger: required for cross-instance event coordination. Set
MESSENGER_STORE=redisand configureMESSENGER_REDISso all instances see the same events.
Without these, multi-instance deployments work for read-heavy workloads but exhibit drift on writes, i.e. items created on instance A may not appear in cached responses from instance B until the cache TTL elapses.
Where to go next
Section titled “Where to go next”- Configuration is the reference for every environment variable mentioned here.
- Security hardening covers HTTPS, secrets, rate-limiting, and other production-safety topics.
- Backups and Upgrades cover ongoing operational tasks.