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 at the end one command tears it all down.
You need Docker, Node.js 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:latest
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:latest
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
d6s --version
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 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
Resources 14 records in 10 resources → directus/default/data
Your record counts may differ slightly; a fresh instance carries a handful of configuration records (the admin role, its policy, the settings row) 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 files deterministically: no timestamps, no reshuffling, nothing but your change. This is the property that makes the files reviewable, and it means a pull that finds nothing new leaves your working tree clean. Commit the field.
git add directus/
git commit -m "Add articles.summary"
Preview against the target
The target instance is still empty. Ask what pushing the committed files would do to it:
d6s sync diff --to target
● Comparing committed files with target — http://localhost:8056 (merge — additive, no deletions)
● Schema — 1 change: 1 added, 0 modified, 0 deleted
+ collection articles (3 fields)
● Data — no changes to import.
Read the plan: one collection to add, and the mode line tells you up front that merge, the default, never deletes anything. If your data section lists a few configuration 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 data changes to import.
The "schema hash verified" line is the push double-checking that the target's schema ended up matching the plan it previewed, rather than assuming it did.
Open the target Studio at http://localhost:8056: the articles collection is there, fields and settings intact. That's the whole loop. Change on one instance, pull to files, review in git, push to another.
If a push imports configuration records, it also writes directus/default/id_map.json, which remembers which target record each committed record became. Commit it when it changes; How It Works explains why.
Push again
d6s sync push --to target
● Pushing to target — http://localhost:8056 (merge — additive, no deletions)
◇ target already matches the committed files — schema and data 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 files you commit to git, using the Directus CLI.
How It Works
The mental model behind Environment Sync, including files as the source of truth, record identity across instances, how a push applies, and the safety rules every command follows.