Skip to content

Upgrades

CairnCMS follows semantic versioning. Most upgrades are routine: pull the new image, restart the container, and the database catches up automatically on startup. The work that does need attention is the small amount of structural and operational care around an upgrade, including taking a backup, reading the changelog, choosing the right window, and rebuilding extensions when a major version moves the host range.

This page covers the upgrade procedure, the rollback path, and the considerations specific to multi-instance and major-version upgrades.

CairnCMS uses semver. For a given version MAJOR.MINOR.PATCH:

  • Patch (1.2.31.2.4) — bug fixes and security patches. No schema changes that require a code change on your side, no breaking API or extension contract changes.
  • Minor (1.2.31.3.0) — new features, additive changes. The HTTP API, SDK, and extension contracts stay backwards-compatible. Database schema changes happen here, but always through migrations that run automatically.
  • Major (1.x2.x) — breaking changes. Possible API contract changes, extension SDK changes, and migrations that require operator attention. Major upgrades are documented in dedicated migration notes alongside the release.

The version is stamped into every container image tag (cairncms/cairncms:1.2.3) and reported by cairncms --version on the CLI. The running platform reports its version through the /server/info API endpoint.

Three things every time, no exceptions:

  1. Take a backup. A full database dump and, if files have changed since the last backup, a copy of the storage volume. See Backups.
  2. Read the changelog for every version between yours and the target. Even within a single minor range, you might have skipped a release that introduced a configuration default change or a deprecated environment variable.
  3. Test in a non-production environment first if you can. A staging instance restored from production data, run through the upgrade, is the cheapest way to surface upgrade-time problems before they reach users.

For major-version upgrades, also:

  • Audit your extensions. Their cairncms:extension.host semver range in package.json declares which CairnCMS versions they are compatible with. An extension built for ^1.0.0 will not load against 2.x until it is rebuilt with a compatible range.
  • Review breaking-change notes for any deprecated environment variables, removed flags, or schema changes that need manual intervention.

The procedure is the same for any patch or minor version, and most major versions:

Terminal window
# Pull the new tag
docker pull cairncms/cairncms:1.3.0
# Update your compose file or deployment manifest to reference 1.3.0
# Stop the running container, start the new one
docker compose up -d cairncms

The image’s default CMD runs cairncms bootstrap on startup. Bootstrap ensures system tables exist and applies pending migrations. After migrations complete, cairncms start boots the API.

For a multi-host or orchestrated deployment, the equivalent step is whatever your platform does to rotate the running version: a kubectl rollout restart, an ECS service update, a Fly deploy, and so on.

If you run CairnCMS directly on a host:

Terminal window
npm install -g cairncms@1.3.0
cairncms bootstrap
# Restart your process supervisor (systemd, PM2, etc.)

cairncms bootstrap is idempotent meaning it’s safe to run on every upgrade. It applies pending migrations and flushes the schema cache.

On a deploy that bumps the CairnCMS version:

  1. Bootstrap reads the database’s directus_migrations table to determine which migrations have already run.
  2. It applies any newer migrations the upgraded version ships, in version order, and records each one as it completes.
  3. After all pending migrations are applied, it flushes schema and permission caches so the next request sees the new structure.
  4. cairncms start boots the API. On startup, start itself only validates that all known migrations have been applied; it does not run migrations.

For most deployments, this means “pull the new image, restart” is the entire upgrade. The platform handles the rest.

Behind a load balancer with multiple CairnCMS instances, the migration step needs to run exactly once, not once per instance. The migrations runner does not coordinate across instances. It reads directus_migrations, applies whatever has not been applied yet, and inserts the completion rows. Two instances running bootstrap at the same time will both read the same pending list and both try to apply the same migrations, which races at best and corrupts state at worst.

The remedy is to keep the migration step out of the steady-state pod startup:

  1. Run cairncms bootstrap once against the database, typically as a one-shot job or init container, and wait for it to complete.
  2. Once migrations are done, roll the fleet onto the new image. The new instances start, observe that all migrations are already applied, and serve traffic.

In Kubernetes, the cleanest shape is a Job (or init container) that runs cairncms bootstrap before the rolling update of the main Deployment. The Job blocks the rollout if migrations fail, which surfaces the problem before any user traffic hits the new version.

For a single-host Docker Compose deployment that scales to one CairnCMS replica, the default image CMD (which runs bootstrap then start) is fine. There is no second runner to race against. The split job pattern only matters once you have more than one instance.

Rollback is the reverse of the upgrade with one extra step.

If the database has not been changed (no new migrations applied), rollback is just an image rollback:

Terminal window
docker pull cairncms/cairncms:1.2.3
docker compose up -d cairncms

If migrations did run, roll those back first:

Terminal window
# One step at a time — each invocation reverses one migration
cairncms database migrate:down
cairncms database migrate:down
# ...until you are at the target version's expected migration state

migrate:down is destructive. The migration’s down() is responsible for reversing the schema change, but data added after the migration ran can still be lost (a column that was added by the migration and populated since cannot be restored after the column is dropped). When in doubt, restore from the backup taken before the upgrade rather than relying on down migrations.

A major version is, by policy, allowed to ship migrations that do not have a clean down path. Treat major-version rollback as a restore-from-backup operation, not a step-down operation. The pre-upgrade backup is the source of truth; reapply it to a 1.x instance on the previous image tag, redirect traffic, and investigate the upgrade failure offline.

This is also why the pre-upgrade backup is non-negotiable. Without it, a major-version rollback may not be possible.

Custom migrations placed in EXTENSIONS_PATH/migrations are interleaved with platform migrations by version timestamp at runtime. This has two consequences for upgrades:

  • Newer custom migrations run during the upgrade. If you have added a custom migration since the last deploy, it runs as part of bootstrap alongside any new platform migrations.
  • A custom migration with a timestamp earlier than the last applied platform migration will not run. The migrations runner only applies versions newer than the latest completed one. Pick custom-migration timestamps that put them after any platform migrations they depend on.

See Custom migrations for the file format and CLI commands.

Each extension’s package.json declares a host range:

{
"cairncms:extension": {
"host": "^1.0.0"
}
}

This field is informational. CairnCMS surfaces it in extension metadata so operators and tooling know which platform versions an extension was built against, but the loader does not enforce the range. An extension whose declared host excludes the running platform version still loads.

In practice, that means extension compatibility is your responsibility to manage during upgrades. For minor and patch upgrades within an extension’s declared range, the extension keeps working without intervention because the SDK contract is stable. For major upgrades, where the SDK contract may change:

  1. Update each extension’s host range to include the new major.
  2. Rebuild against the new SDK version with npm run build (which calls cairncms-extension build).
  3. Run the extension’s tests, if any, against the upgraded SDK.
  4. Reinstall or redeploy the extension alongside the platform upgrade.

If you skip the rebuild on a major upgrade, the extension may still load, but it can fail at runtime when it calls SDK helpers whose contracts have changed. Treating the rebuild as part of the major-upgrade procedure is the safe default.

Bundles use the same host field; rebuilding a bundle rebuilds all of its entries.

  • Backups — the prerequisite for any upgrade you would actually be willing to roll back from.
  • Deployment — covers the bootstrap-and-start pattern in more detail, including the Kubernetes init-container shape.
  • Custom migrations — the file format, version naming, and CLI commands for migrations you write yourself.