If you find this add-on useful, please star it on GitHub — stars show appreciation and help maintainers know their work matters.
Sync a production MySQL database into your local DDEV project — modelled on Drush’s sql-sync, for any plain SSH-accessible server (a VPS running Laravel, WordPress, Drupal, …), not just hosted platforms.
ddev sql-sync @prod @self:
<tmp>/ddev-dumps/) with a completion marker carrying its byte size. See Dump strategies below.~/sql-sync) and verify it against the completion marker before anything is imported. The remote temp files are deleted after a successful import (kept if the import fails, so the same dump can be fetched again with --no-dump).ddev import-db into this project (you are asked to confirm first; the current local database is replaced).The add-on only ever reads from the server (a dump). It never writes to a remote database, and @self is the only supported target.
ddev add-on get atj4me/ddev-sql-sync
ddev restart
.ddev/sql-sync/aliases.ymlThis file is the whole configuration. Installing the add-on copies aliases.example.yml to aliases.yml if you don’t have one yet; your aliases.yml is never overwritten by add-on updates and never removed when the add-on is removed.
local:
dir: ~/sql-sync # where downloaded dumps are kept
prod:
host: myserver # an ~/.ssh/config alias or a hostname
user: deploy # optional; ssh target becomes user@host
root: /var/www/example # the app's path on the server
tmp: /tmp # optional; temp folder on the server
Define as many aliases as you like (prod, staging, …) and pick one per run: ddev sql-sync @staging @self.
Optional per-alias keys:
| Key | Purpose |
|---|---|
platform: |
laravel, wordpress, drupal or php. Resolved per alias: this key, else the project’s DDEV type from .ddev/config.yaml (laravel/wordpress/drupal* map to themselves), else php — DDEV’s generic type, an unrecognised type, and no type at all all land on the generic php platform. Usually unset. |
options: |
Extra ssh options, e.g. options: -i ~/.ssh/prod_key. |
jump: |
Optional jump host (bastion) passed to ssh -J (ProxyJump) on every connection, e.g. jump: bastion.example.com. Prefer ProxyJump in ~/.ssh/config — that is the durable place, and every tool benefits. This key is only for one-off syncs where you don’t want to touch your ssh config. |
dump: |
A command on the server whose stdout is the dump (e.g. dump: drush sql-dump). Replaces both dump strategies below — works for any platform. |
database: |
Explicit database credentials (see below). |
sanitize: |
Path (relative to the project root) to your own SQL file to sanitize with after import. |
Normally no credentials are configured at all: at dump time, the add-on reads them from the app’s own config on the server — .env (Laravel), wp-config.php (WordPress), or sites/default/settings.php (Drupal). SSH authentication stays your own keys/~/.ssh/config; nothing is stored by the add-on.
For setups where that can’t work (managed database hosts, split credentials), an alias can pin values explicitly:
prod:
# ...
database:
name: example_db
username: example_user
password: secret
host: 127.0.0.1 # from the app server's perspective
port: 3306
Any field left out falls back to the app’s config, field by field — you can pin just the password, or just host/port for an RDS-style endpoint.
Treat a filled-in
password:as a secret. Prefer app-config extraction when it works.aliases.ymllives under.ddev/, which many projects gitignore (check yours), andchmod 600 .ddev/sql-sync/aliases.ymlis a good idea if you use the block. The password is passed to mysqldump via theMYSQL_PWDenvironment variable only — never as a-pargument and never echoed.
Make sure you can reach the server non-interactively before the first run:
ssh your-server-alias 'echo ok'
If that prompts for a password, set up a key (ssh-copy-id) and, if you use a short alias like myserver, a matching entry in your ~/.ssh/config. This is your own machine’s SSH setup — the add-on doesn’t manage keys.
ddev sql-sync @prod @self # source, then target — like cp / rsync / drush sql-sync
ddev sql-sync @prod @self -y # skip the confirmation prompt
ddev sql-sync @prod @self --dry-run # preflight checks only — nothing is dumped or imported
| Flag | Meaning |
|---|---|
--sanitize / --no-sanitize |
Sanitize after import (default on) / skip it. Skipping prints a loud warning: the local DB then holds real production emails and password hashes. |
--dump-dir=<path> |
Where the dump is downloaded to (overrides local.dir). |
--no-dump |
Don’t create a new dump; rsync the dump an earlier --no-sync run left on the server. Its completion marker must be present and match the downloaded size, or the run refuses to import. |
--no-sync |
Only create the dump on the server (with its completion marker); don’t download, import, or sanitize. A later --no-dump run fetches it. |
--dry-run |
Preflight only: check ssh connectivity, platform, dump strategy, credentials (the password is never shown), database size and disk space, then print the plan. Takes no dump and imports nothing. See “Testing safely” for one small footnote. |
-y, --yes |
Don’t ask for confirmation. |
-h, --help |
Show help. |
Dumps land at <dump-dir>/<alias>.sql (or <alias>.sql.gz when the dump is gzipped) and are kept after import (handy for re-importing). They contain production data — delete them when you no longer need them: rm ~/sql-sync/*.sql.
The dump strategy per alias, in order:
dump: override — if the alias sets one, it always wins.~/bin, ~/.local/bin and /usr/local/bin, and Drush in the app’s vendor/bin:
wp db export (WP-CLI)drush sql-dumpschema:dump is structure-only), so Laravel always uses strategy 3.database: block merged over the app’s config).The run tells you which strategy it used: using the app's own export: wp db export, using the app's own export: drush sql-dump, using the alias's dump: command, or using mysqldump.
Platform notes:
.env app — credentials from <root>/.env. Bedrock-style WordPress also fits here (set platform: laravel).DB_* defines in <root>/wp-config.php (or one level up, as WordPress allows).php type, and the terminal default when nothing else matches. No app-native export exists, so it always uses mysqldump; credentials come from <root>/.env if the app has one, merged under the alias’s database: block — for a generic app that block is the expected primary source. No bundled sanitize: a sanitize: file is honoured, otherwise the run reports that no sanitize ran.$databases entry from settings.php with a small PHP snippet on the server (best-effort: a settings file that pulls credentials from the environment may not yield them — then use a dump: override such as dump: drush sql-dump, or pin a database: block).Runs after import, on the local database only:
users.email → user<id>@example.test, users.password → bcrypt of password, remember_token → NULL.<prefix>users.user_email → user<id>@example.test, <prefix>users.user_pass → MD5('password') (WordPress accepts MD5 at login and rehashes transparently). The prefix is read from the server’s wp-config.php — a custom $table_prefix is honoured, not assumed to be wp_ (on --no-dump runs it is detected from the imported tables instead).ddev drush sql-sanitize when the local project has Drush (Drupal hashes passwords in PHP, so there is deliberately no bundled SQL sanitize for it).sanitize: file in the alias, when set, is used instead of the built-in sanitize on every platform — including Drupal, where it wins over drush sql-sanitize (explicit per-alias config wins, as with dump: and database:).If sanitize is on (the default) but nothing could be applied — no matching table, no Drush, no sanitize: file — the run imports and then stops with a prominent error and a non-zero exit: it never reports success over an unsanitized database. Pass --no-sanitize to accept an unsanitized import explicitly.
sql-syncMirrored deliberately: the two-position sql-sync @source @target form with @self as the local site, site aliases in a YAML file, the confirmation prompt, and --no-dump / --no-sync with their Drush meanings.
Deviations, also deliberate:
sql-sanitize command you must remember to run). --no-sanitize opts out.@self as target in v1 — syncing to a remote is out of scope by design; the tool never writes to a remote server.New server, or just cautious? Work up in four steps:
ddev sql-sync @prod @self --dry-run — preflight only. It connects over ssh (BatchMode, short timeout, so it fails fast instead of hanging), detects the platform and dump strategy on the server, resolves the credentials (database name and username are shown; the password never is), estimates the database size from information_schema, and checks free space in the server’s tmp folder and your local dump folder. You get a verdict per check plus the plan — then it stops. No dump is taken and nothing is imported. One footnote: resolving Drupal credentials during preflight briefly creates a small temp file (mode 600) in the server’s tmp folder and deletes it again within the same step — it is the extractor for settings.php, not a dump.ddev sql-sync @prod @self --no-sync — a real dump, created on the server in <tmp>/ddev-dumps/ with a completion marker. Nothing is downloaded — the point of this step is proving the server side works end to end and leaving a known-good dump in place.ddev sql-sync @prod @self --no-dump — fetch the dump step 2 left behind (its completion marker is checked against the downloaded size before anything is imported), then import and sanitize it — without touching the server’s database again.ddev sql-sync @prod @self — the full run.What a real run writes on the server: a dump file and a small completion marker in <tmp>/ddev-dumps/ inside the alias’s tmp folder (the folder is created mode 700, the dump mode 600), both removed after a successful import. If the import fails, both are kept so a --no-dump run can fetch the same dump; if the dump itself fails, its partial file is removed by the remote side. One guard protects small servers: before dumping, a run estimates the database size and aborts if the tmp filesystem has less free space than that — whenever the size can be estimated. When it cannot (for example a dump: command whose credentials cannot be resolved, or no mysql client on the server), the run says so and skips the check rather than pretending it happened — a dump filling the disk is the one realistic way a sync could hurt a small server, so an un-estimable database deserves a second look at the server’s free space before a full run.
<tmp>/ddev-dumps/ (removed after a successful import; a failed dump removes its own partial file) — plus whatever a dump: command you configured chooses to do.ddev add-on remove sql-sync
This removes the command and the bundled files. Your aliases.yml and any downloaded dumps are left in place — delete them manually if you want them gone.
If you find this add-on useful, please star it on GitHub — stars show appreciation and help maintainers know their work matters.