Quickstart
The fastest way to trust Environment Sync is to watch it work somewhere mistakes are free. In this guide, you stand up two throwaway Directus instances with Docker, make a change on one, and move it to the other through git. Nothing here touches a real project, and one command tears it all down at the end.
You need Docker, Node.js 22 or later with npm, and git.
Start two instances
Save this as docker-compose.yml in an empty directory:
services:
source-db:
image: postgres:16-alpine
environment:
POSTGRES_USER: "directus"
POSTGRES_PASSWORD: "directus"
POSTGRES_DB: "directus"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U directus"]
interval: 5s
retries: 12
source:
image: directus/directus:12.2.0
ports:
- 8055:8055
environment:
SECRET: "quickstart-source-secret"
DB_CLIENT: "pg"
DB_HOST: "source-db"
DB_PORT: "5432"
DB_DATABASE: "directus"
DB_USER: "directus"
DB_PASSWORD: "directus"
ADMIN_EMAIL: "admin@example.com"
ADMIN_PASSWORD: "quickstart"
ADMIN_TOKEN: "source-token"
depends_on:
source-db:
condition: service_healthy
target-db:
image: postgres:16-alpine
environment:
POSTGRES_USER: "directus"
POSTGRES_PASSWORD: "directus"
POSTGRES_DB: "directus"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U directus"]
interval: 5s
retries: 12
target:
image: directus/directus:12.2.0
ports:
- 8056:8055
environment:
SECRET: "quickstart-target-secret"
DB_CLIENT: "pg"
DB_HOST: "target-db"
DB_PORT: "5432"
DB_DATABASE: "directus"
DB_USER: "directus"
DB_PASSWORD: "directus"
ADMIN_EMAIL: "admin@example.com"
ADMIN_PASSWORD: "quickstart"
ADMIN_TOKEN: "target-token"
depends_on:
target-db:
condition: service_healthy
docker compose up -d
After a minute, http://localhost:8055 (the source) and http://localhost:8056 (the target) both serve a fresh Data Studio. Log in with admin@example.com / quickstart. The ADMIN_TOKEN values are static admin tokens the CLI will use.
Install the CLI
npm install -g @directus/cli@12
d6s --version
Pinning the package to major version 12 selects the current Environment Sync CLI instead of the deprecated 9.x package previously published under this name. The package installs two commands that do the same thing: directus-cli and the short alias d6s. These docs use d6s.
Add a profile for each instance
A profile pairs a name with an instance URL. Run this in the same directory as the compose file:
d6s profile add source --url http://localhost:8055 --token source-token
◇ Saved profile "source" → http://localhost:8055
◇ Saved a token for "source" to the credential store.
d6s profile add target --url http://localhost:8056 --token target-token
That first command created directus.config.json in the current directory:
{
"profiles": {
"source": { "url": "http://localhost:8055", "auth": { "type": "token" } },
"target": { "url": "http://localhost:8056", "auth": { "type": "token" } }
}
}
Notice what is not in it: the tokens. URLs are project configuration you commit; credentials go to ~/.directus/credentials.json, readable only by you. Confirm both connections work:
d6s profile test source
◇ Authenticated to http://localhost:8055 as Admin User (Administrator).
Make something to sync
In the source Studio at http://localhost:8055, create a collection called articles with two fields: title and status. This plays the part of a day's modeling work on a development instance.
Pull it into sync files
Make the directory a git repository, then pull:
git init
d6s sync pull --from source
◇ Pulled from source — http://localhost:8055
Schema 1 collection → directus/default/schema
Configuration 14 records across 11 collections → directus/default/data
Your record counts may differ slightly; a fresh instance carries a handful of configuration records (the admin role, its policy, and the settings record) even before you touch it.
Look at what appeared:
directus/default/
schema/ # one JSON file per collection, plus metadata.json
data/ # one JSON file per configuration resource, plus metadata.json
Open the articles file under schema/. It's your collection, readable as JSON: the fields you just created, their types, their interface settings. This is what reviewers will see in pull requests. Commit it:
git add directus/ directus.config.json
git commit -m "Baseline from source"
Change something and pull again
Back in the source Studio, add one more field to articles: summary. Then:
d6s sync pull --from source
git diff
The diff touches one file, and inside it, only the new summary field. The CLI writes sync files deterministically: no timestamps, no reshuffling, nothing but your change. This makes the sync files reviewable and means repeating the pull while the source is unchanged leaves your working tree clean. Commit the field.
git add directus/
git commit -m "Add articles.summary"
Preview against the target
The target still has only its initial Directus setup and does not have articles. Ask what pushing the sync files would do to it:
d6s sync diff --to target
● Comparing ./directus/default with target — http://localhost:8056 (merge — creates and updates records, never deletes)
● Schema — 1 change: 1 added, 0 modified, 0 deleted
+ collection articles (3 fields)
● Configuration — no changes to push.
Read the plan: one collection to add, and the mode line tells you up front that merge, the default, never deletes anything. If the Configuration section lists a few records instead of "no changes", that's fine; fresh instances can differ slightly in their defaults. diff applied nothing; it's always safe to run.
Push
d6s sync push --to target
The push prints the same plan, then asks before applying: Apply 1 schema change to target — http://localhost:8056?. Confirm it:
● Schema applied.
◇ Push complete. Applied 1 schema change to http://localhost:8056; schema hash verified. No configuration changes to push.
The "schema hash verified" line means the server confirmed that the target still matched the schema hash used to build the plan before applying it. If someone changes the target between planning and apply, the push stops instead of applying a stale plan.
Open the target Studio at http://localhost:8056: the articles collection is there, fields and settings intact. That's the whole loop. Change one instance, pull to sync files, review in git, and push to another instance.
If a push imports configuration records, it also writes directus/default/id_map.json, which records the target record that corresponds to each source record. Commit it when it changes; How It Works explains why.
Push again
d6s sync push --to target
● Pushing ./directus/default to target — http://localhost:8056 (merge — creates and updates records, never deletes)
◇ target — http://localhost:8056 already matches ./directus/default — schema and configuration match; nothing to push.
Nothing to confirm, nothing applied. The CLI checked the target and proved it matches; it didn't assume. Re-running a completed push is safe, which is exactly what automation needs.
Clean up
docker compose down
Both instances and their databases are gone.
Where to go next
- How It Works: the mental model behind what you just did, and the safety rules around deletions and record identity.
- Common Workflows: the same loop applied to real situations, including adopting sync on an existing project.
- Reference: every flag the commands take, including how to scope a pull to specific collections or resources.
Get once-a-month release notes & real‑world code tips...no fluff. 🐰
Overview
Move schema and configuration between Directus instances through sync files you commit to git, using the Directus CLI.
How It Works
The mental model behind Environment Sync, including sync files as the source of truth, record identity across instances, how a push applies, and the safety rules every command follows.