Pull a project's production database, files and setup commands into a local DDEV environment — and leave the production data behind while doing it.
One command replaces the sequence every developer otherwise repeats by hand:
dump the remote database over ssh, import it, rsync the uploads, rewrite the
local environment, anonymise what came across, and run the framework's own
post-import commands. What those commands are is decided by a profile per
project type, so the tool knows that TYPO3 wants vendor/bin/typo3 cache:flush where Laravel wants php artisan optimize:clear.
Published as a release candidate. v0.1.0-rc.1 is the first published
version: the engine, the profiles, the container image and the add-on
packaging all exist and are installable, and what has not happened yet is
written down rather than implied. No real sync against a real remote host has
been run through the add-on, and profiles/VERIFIED.md names every profile
variant whose commands no run has touched. Installing from the published
release is exercised: the add-on suite installs this release into a real
DDEV project on every run, starts it, and removes it again.
cd /your/project
ddev add-on get NoiXdev/ddev-syncNo .ddev/.env lines are needed: the add-on names the image it shipped with.
To work on the add-on itself, install from a checkout and point it at a local
image instead. The two lines go into .ddev/.env.local, the git-ignored file
for machine-specific keys:
docker build -t ddev-sync:dev /path/to/ddev-sync
cd /your/project
ddev add-on get /path/to/ddev-sync
printf 'DDEV_SYNC_IMAGE=ddev-sync\nDDEV_SYNC_VERSION=dev\n' >> .ddev/.env.localFor a checkout install the order matters. A pre-start hook runs on every
project start and reads the image, so the image has to be built and
.ddev/.env.local has to name it before the first ddev restart. Get it
wrong and the whole project refuses to start, not just the sidecar. A published
install has nothing to build or name, so none of this applies to it.
Then load your key, restart, and let init ask for the target:
ddev auth ssh
ddev restart
ddev sync initinit asks for a name, where the remote is and which stage it is, writes that
one target into .ddev/ddev-sync.yaml - the commented template the install
put there - and finishes by running doctor against it. It only ever writes
that file, and it reads the answers from a terminal, so there are no flags to
supply them with: to set a target up without one, edit the template by hand
and run ddev sync --target <name> doctor yourself.
However you installed it, the add-on needs DDEV's ssh agent: if
ddev-ssh-agent is listed in omit_containers anywhere, no project with the
add-on installed will start, and the hook says what to remove.
doctor is the proof that installation worked, and what to look for is the
web container and ssh agent rows being OK. Without a reachable remote its
ssh connection row will fail, and that is expected: it says nothing about the
installation. An unknown host is trusted the first time it is seen, without
asking, and remembered after that; a host whose key has changed is refused,
and doctor says so on the ssh connection row. The exception is a jump host:
its key is not covered, so a target behind one fails on the bastion's key until
ssh has been told about it. See SECURITY.md for why and for the
workaround.
The user documentation (NoiXdev/noix-docs, in its ddev-sync section) walks
through installation, the first sync and removal.
The add-on declares DDEV v1.25.0 as its floor, which is what it has been exercised against: only macOS with Docker Desktop and DDEV v1.25.4. Linux, Colima, OrbStack and rootless Docker have not been tried, and no real sync against a real remote host has yet been run through the add-on.
The published image is built for both linux/amd64 and linux/arm64. On
arm64 the same four checks run as on amd64: the image starts with no network,
carries every binary the engine runs locally, finds its profiles where they
are installed, and reports the version it was released as. No sync has been
run on arm64 at all.
| Step | What happens |
|---|---|
db |
mysqldump over ssh, streamed, imported into the DDEV database, and the local .env pointed at it |
files |
rsync of the paths the profile declares — uploads, fileadmin, storage/app/public |
s3 |
rclone between two S3 endpoints, for projects that keep media in a bucket |
sanitize |
Truncates sessions and queues, anonymises user tables, resets passwords, creates a local admin |
setup |
The framework's own commands: composer install, migrations, cache rebuilds |
Nothing is pushed, ever. The remote is only ever read.
Profiles ship for TYPO3 (9 through 13), Laravel, Symfony, WordPress, Drupal and Shopware 6, plus optional features for Horizon, Filament, Livewire, Inertia, node builds, Symfony search reindexing and TYPO3's processed-files handling.
A generic fallback covers DDEV project types with no dedicated profile; it
syncs the database and files but leaves the framework-specific work alone,
and it needs credentials of its own because there is no convention to read
them from. They are secrets, so they belong in .ddev/ddev-sync/local.yaml
rather than the project file — inside the directory the add-on's shipped
.gitignore already covers, so there is nothing to check yourself. A project
that still keeps them in the old .ddev/ddev-sync.local.yaml continues to
work — that path is still read — but should move them to the new one; that
file sits one level up, outside the directory the .gitignore covers.
The local database calls speak MariaDB's client protocol. The test suites
exercise them end to end against a MariaDB 11 server, and they have been
measured to connect against DDEV's MariaDB 10.8, 10.11 and 11.8 images. A DDEV
project on MySQL 8.0 is not supported: its accounts use an authentication
plugin this image's client cannot load, so the connection is refused whatever
TLS does. MySQL 5.x connects but is not otherwise covered here, and PostgreSQL
is not addressed at all. Nothing here reads DDEV's own database.type from
.ddev/config.yaml, so an unsupported project fails at the first local call
rather than up front.
Which of them have actually been run against a real installation — rather
than only reasoned about — is recorded per variant in
profiles/VERIFIED.md. Read that file before trusting
a profile with a production database. It also records, per rule, what has no
run-time evidence.
ddev sync --target=production --dry-run
Plan for target 'production' typo3@12
acme - deploy@web01.example:22 /srv/app/
Nothing will be executed.
1 preflight remote ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 -p 22 deploy@web01.example [built-in]
2 db.dump remote mysqldump --single-transaction $TYPO3_DB_NAME [profile:typo3]
3 db.download - streamed as part of db.dump into .ddev/.downloads/dump.sql [built-in]
4 db.import local mysql -h ddev-acme-db db < .ddev/.downloads/dump.sql [built-in]
5 files.sync rsync rsync -a --info=progress2 -e ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 -p 22 deploy@web01.example:/srv/app/public/fileadmin/ /var/www/html/public/fileadmin/ [profile:typo3]
6 files.sync rsync rsync -a --info=progress2 -e ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 -p 22 deploy@web01.example:/srv/app/public/uploads/ /var/www/html/public/uploads/ [profile:typo3]
7 sanitize sql 10 rules: clear-be-sessions, clear-scheduler-tasks, clear-fe-sessions, clear-sys-log, anonymize-fe-users, reset-fe-passwords, anonymize-be-users, reset-be-passwords, local-admin, flush-cache [profile:typo3]
8 setup web composer install --no-interaction [profile:typo3]
9 setup web vendor/bin/typo3 extension:setup [profile:typo3]
10 setup web vendor/bin/typo3 cache:flush [profile:typo3]
Sources: [built-in] [profile:typo3]
That is not an illustration: it is tests/cli/golden/typo3-12-full.txt, a
snapshot the test suite checks, so a change to a profile shows up as a diff
there rather than as a surprise on someone's machine. Every line names the
layer it came from, and --dry-run walks the same plan object the executor
would — so what you read is what would run.
db.import drops and recreates the local database. The sanitize step
rewrites user tables in place. Both are the point of the tool, and both are
irreversible locally, so ddev sync doctor exists to tell you whether a sync
will work before one moves a byte.
Sanitize rules are what keep production data from living on a laptop. They
are per project type, and a project can add its own — but a rule that was
never verified against a real installation can silently do nothing, which is
why profiles/VERIFIED.md is part of this repository rather than an
afterthought.
uv sync
uv run pytest # unit suite; the gate is 100 % line and branch coverage
uv run ruff check . && uv run ruff format --check .
uv run mypy # strict, over src, tests and toolsTwo further suites exist and are excluded from the default run:
uv run pytest -m integration # a real pull against containers
uv run pytest -m matrix --no-cov # installs real frameworks and syncs against themThe matrix is what turns a profile's declared commands from a claim into evidence. A cell installs the real framework, seeds a stand-in production database from that installation's own schema, and runs a whole sync against it with the shipped image. It takes about twenty minutes for six cells.
User-facing documentation lives in NoiXdev/noix-docs; this README stays
short on purpose.
MIT © noix.dev
