Migration between instances
“Migration” can mean several different things for a CairnCMS deployment, and the right procedure depends on which one you mean. Cloning a production instance to a development environment is a different operation from promoting a development schema to staging, and both are different from changing database vendors.
This page lays out the migration shapes, the right procedure for each, and the surfaces that have to move together to end up with a working instance on the other side.
Pick the migration shape
Section titled “Pick the migration shape”Three common shapes:
- Full instance clone. Move everything — schema, content, users, roles, files, secrets — to a new home. New host, managed-database provider, copy of production for offline analysis, that kind of thing. The procedure is a database dump plus a file-storage copy.
- Structure promotion. Move the data model and the role/permission setup from one environment to another, without content. Dev to staging, staging to production, branch to mainline. Schema-as-code and config-as-code handle this together.
- Content move. Move some or all collection rows between environments where the schema already matches. Can use a database-level dump of specific tables or the HTTP API for selective transfer.
The list of caveats, including cross-vendor compatibility, cross-version compatibility, and file-row consistency, apply to all three but matters most for the first.
Full instance clone
Section titled “Full instance clone”The complete state of a deployment lives in three places: the database, the file-storage backend, and the configuration. To clone a deployment you have to copy all three.
1. Take a coordinated snapshot
Section titled “1. Take a coordinated snapshot”Pause writes, or accept some inconsistency, and capture:
- A database dump (
pg_dump,mysqldump,sqlite3 .backup). See Backups for the per-vendor commands. - A copy of the storage backend’s contents — the actual bytes for any files in
directus_files. For local-disk storage, anrsyncof the upload root. For S3 / GCS / Azure Blob, a bucket-to-bucket copy with the cloud provider’s tooling. For Cloudinary, an asset export.
These two captures should describe the same point in time. If your deployment has heavy concurrent writes, take both during a brief maintenance window. For lightly-loaded deployments, capturing them in sequence is usually fine. Files added between the two captures show up as missing-byte errors that you can resolve manually.
2. Restore on the target
Section titled “2. Restore on the target”On the destination:
- Restore the database dump into a fresh database. Whether the target database has to be precreated depends on the vendor and tool:
pg_restore --dbname=<name>expects the database to exist (usepg_restore --createagainst the postgres database to create it as part of the restore);mysql < dump.sqlsimilarly expects the schema to exist unless the dump containsCREATE DATABASE; SQLite creates the file on the fly. Check thevendor-specific commands in Backups before running. - Restore the file bytes into the new storage backend. The destination’s
STORAGE_LOCATIONSand per-driver paths must match what the original deployment used, becausedirectus_filesrows reference files bystorage(the location name) andfilename_disk(the path within that location). If you change storage backends, see Changing storage backends below.
3. Bring up CairnCMS pointed at the restored state
Section titled “3. Bring up CairnCMS pointed at the restored state”Configure the destination’s .env (or secret store) to point at the restored database and storage backend. The minimum configuration that has to match the original:
SECRET— the signing secret for tokens and sessions. Keep it identical to the source if you want active sessions and outstanding refresh tokens to remain valid across the migration. Changing it invalidates every existing access and refresh token.KEY— the unique instance identifier. Required at startup and reported as the service ID in health checks. Carry the source’sKEYover so the migrated instance presents the same identity that downstream consumers and operators expect.STORAGE_LOCATIONSand per-driver settings — must match the locations referenced bydirectus_files.storage.PUBLIC_URL— sets the externally-reachable URL for asset links and email templates. Update this for the new home.
Bring up the new instance, run cairncms bootstrap once to verify migrations are at the expected version, and the clone is online.
4. Switch traffic
Section titled “4. Switch traffic”DNS or load-balancer change to point at the new instance, with the old one kept running until you are sure the new one is healthy. The old instance’s database becomes a fallback you can repoint at if the new one needs an emergency rollback.
Structure promotion
Section titled “Structure promotion”When you only want to move the data model and the role/permission setup use the snapshot/apply pattern. This is the right shape for dev → staging → production promotion.
The procedure pairs two existing CairnCMS workflows:
# On the source (dev)cairncms schema snapshot ./schema.yamlcairncms config snapshot ./config
# Commit both to source control, open a PR, review the diffs
# On the target (staging or production), after mergecairncms schema apply ./schema.yamlcairncms config apply ./configThe two commands run in that order: schema first (so collections, fields, and relations exist), then config (so permissions can reference the now-present collections). See Schema as code and Config as code for the full reference on each.
For environments that need automated promotion, run the same two commands as steps in the deploy pipeline. Prefer --dry-run against the production database in CI on every PR, so the eventual deploy diff is visible before merge.
Content move
Section titled “Content move”Snapshots never contain content; database dumps always do. For moving content between two instances that already share a schema, the choices:
Selective table dump
Section titled “Selective table dump”Dump and restore just the user-defined tables, leaving each side’s own users, roles, sessions, and activity log untouched:
# Postgres example: dump only specific tablespg_dump --data-only --table=articles --table=authors \ --host=<source-host> --username=<user> --dbname=<source-db> > content.sql
psql --host=<target-host> --username=<user> --dbname=<target-db> < content.sqlThe same shape works for mysqldump --tables or sqlite3 .dump. Use --data-only (or vendor equivalent) to skip schema, since the target already has the schema in place.
This is the cheapest content move, but it does not handle file relations cleanly: a row in articles that references directus_files.id = '...' will point at a file that does not exist on the target unless you also move the matching directus_files row and the underlying bytes.
HTTP API replay
Section titled “HTTP API replay”For finer control such as selective filtering, transformation, and partial moves, the HTTP API works well. Read items from the source with the SDK or fetch, then create them on the target. The SDK is the easiest tool for this; see Clients for the usage pattern.
The tradeoff is performance and consistency. The API enforces validation and permissions on every write, which is correct but slower than a bulk SQL load. For migrations of millions of rows, the table-dump approach is usually faster.
Cross-vendor migration
Section titled “Cross-vendor migration”Moving from one database vendor to another (Postgres ↔ MySQL ↔ SQLite) is the hardest variant. Three layers of incompatibility:
- Column types and defaults are not strictly portable. A Postgres
timestamp with time zoneis not a MySQLDATETIME; SQLite’s loose typing will accept either but loses precision. - The schema-as-code snapshot stamps the source vendor. The HTTP
/schema/diffendpoint refuses cross-vendor application unless?force=trueis set; the CLI does not enforce this but the resulting database may have subtly wrong types. - A native database dump is vendor-specific. A Postgres dump cannot be loaded into MySQL.
The only reliable approach is to recreate the schema natively on the target and replay content through the HTTP API:
- Stand up the target instance with the right vendor.
- Apply the schema snapshot from the source against the target. The CLI
cairncms schema applydoes not gate on the snapshot’s stamped vendor, so it will run against a different-vendor target as-is. Spot-check critical fields for type drift; correct any that did not translate. (If you go through the HTTP/schema/diffendpoint instead, it will refuse the cross-vendor snapshot unless you pass?force=true— that bypass is HTTP-only.) - Apply config-as-code against the target.
- Replay content through the HTTP API or a vendor-specific data tool that handles the type mapping.
- Move file bytes.
Treat cross-vendor migration as a project rather than a procedure. Plan a non-prod rehearsal on representative data first.
Cross-version migration
Section titled “Cross-version migration”A migration between deployments running different CairnCMS versions is two operations stacked: a version upgrade and a host move. Decide the order:
- Upgrade first, then move. Upgrade the source to match the target, take a fresh dump and snapshot, then move. Lower risk, since the upgrade happens on the host that already works.
- Move first, then upgrade. Clone to the new host on the source’s old version, then upgrade in place. Useful when the old host is on a deprecated platform and you are also using the move to escape it.
The HTTP /schema/diff endpoint refuses snapshots from a different platform version unless ?force=true. The CLI does not enforce this. Treat a version mismatch as a hint to align versions first, not a routine --force flag.
For major-version moves, also see the extension-rebuild step in Upgrades.
File bytes
Section titled “File bytes”Every move that includes content has to move the file bytes too. The cheapest mistake to make is moving the database without the storage backend — the new instance comes up, lists items, and serves 404s for every file because the bytes are not where directus_files.filename_disk says they are.
Two specific consistency rules to keep in mind:
- The
directus_files.storagevalue must name a configured storage location on the target. Keeping the sameSTORAGE_LOCATIONSvalue across the move is the simplest path. - The
filename_diskpath is relative to the storage location’s root. Renaming files during the move breaks every reference. Move bytes verbatim.
Changing storage backends
Section titled “Changing storage backends”When the move also changes storage backends, say, from local disk to S3, there is a third step. After the database is restored:
- Reupload the file bytes into the new backend.
- Update
directus_files.storageand, if the path scheme is different,filename_diskto match.
This is database-side work, not a snapshot operation. A custom migration (Custom migrations) is a clean place to put a one-time backfill, since the migration runs as part of the next bootstrap and is recorded in directus_migrations.
Sequencing
Section titled “Sequencing”For any non-trivial migration, the order of operations matters. The conservative sequence:
- Bring up the target’s database and storage backends, empty.
- Restore the database dump (full clone) or apply the schema snapshot (structure promotion).
- Restore file bytes if applicable.
- Apply config-as-code if applicable.
- Configure secrets to match the source where required (
KEY,SECRET,STORAGE_*). - Bring up the CairnCMS process. Verify the API responds, the admin app loads, and a sample item-fetch succeeds.
- Validate end-to-end: log in as a known user, fetch a known asset, run a flow, check the activity log.
- Switch traffic.
Stop at step 7 if anything fails to validate. The previous instance is still serving so there is no urgency to flip the switch on a half-working migration.
Where to go next
Section titled “Where to go next”- Schema as code — the snapshot/apply mechanism for the data model.
- Config as code — the snapshot/apply mechanism for roles and permissions.
- Backups — per-vendor dump and restore commands, the same machinery used for full-instance migration.
- Upgrades — the version-bump procedure that pairs with cross-version migrations.