A Laravel package that adds a db:refresh-from-prod Artisan command, which replaces the local database with a copy of the production database. Production can be reached over SSH (a server you can log into) or directly on a Laravel Cloud MySQL database's public endpoint. Both MySQL and SQLite local connections are supported; the command picks the strategy from the local connection's driver and the configured source.
- Dumps the current local database to
storage/backups/as a safety net. - Opens an SSH tunnel to the production server.
- Dumps the production database through the tunnel, streaming to disk with a progress bar.
- Drops and recreates the local database (honoring the connection's charset and collation).
- Imports the production dump, streamed with a progress bar.
Same as above, minus the tunnel: mysqldump connects straight to the database's public endpoint over TLS. The dump runs with --single-transaction (consistent, no locking) and --no-tablespaces (Laravel Cloud database users are not granted the PROCESS privilege that mysqldump otherwise requires).
The database is only reachable while the cluster's public endpoint is open. Set LARAVEL_CLOUD_DB_CLUSTER_ID (the db-... id in the cluster's dashboard URL) and the command manages it for you:
- If the endpoint is closed, it asks as soon as it starts whether to open it for this sync, opens it just before the production dump, and waits until it accepts a connection.
- When the sync finishes — or fails — it asks whether to close it again. Say no if another sync is still using it.
- If the endpoint was already open when the command started, it is left open and you are not asked, since someone else may be relying on it.
- On Ctrl-C it closes an endpoint it opened without asking, rather than leave production reachable.
The API token is LARAVEL_CLOUD_TOKEN, or else each token the cloud CLI has stored (~/.config/cloud/config.json) is tried in turn. Without a cluster id, open the endpoint yourself first — "Org > Resources > Databases > ... > Edit settings > Enable public endpoint". Credentials come from "View credentials" on the same menu.
- Snapshots the current local database file to
storage/backups/as a safety net (viasqlite3 .backup, which is WAL-safe). - Takes a consistent snapshot of the production database file on the server with
sqlite3 .backup. - Downloads the snapshot over
scpand removes the remote temp file. - Swaps the snapshot in as the local database file, clearing any stale
-wal/-shmsidecars.
A passkey only works on the domain it was registered on, and two-factor secrets are encrypted with the app key, so the copies production holds usually can't sign you in locally. As soon as it starts, the command lists the local passkeys and the users with two-factor set up, and asks whether to keep each (a question with nothing to keep is skipped; --no-interaction keeps both). After the import it puts what you kept back onto the production users with the same email:
- Passkeys production doesn't have (matched by
credential_id) are inserted for that user. A passkey whose user now has a different id is still restored, with a warning: libraries such aslaravel/passkeysderive the WebAuthn user handle from the id, so it may need registering again. - Two-factor columns (
two_factor_secret,two_factor_recovery_codes,two_factor_confirmed_at) are put back only for users who had two-factor set up locally, so two-factor others enable in production still comes through. - Users production doesn't have are skipped with a warning. Missing tables or columns are skipped silently, so apps without passkeys or Fortify are unaffected.
Turn it off with DB_SYNC_PRESERVE_LOCAL_AUTH=false, or publish the config to change the table and column names under preserve_local_auth.
The command refuses to run unless APP_ENV=local.
- PHP 8.2+
- Laravel 10, 11, or 12
- For MySQL:
mysqlandmysqldumplocally; for SQLite:sqlite3locally and on the production server - For the
sshsource:sshon your PATH (pluslsoffor the MySQL tunnel,scpfor SQLite) and SSH access to the production server (key-based auth recommended) - For the
cloudsource: a Laravel Cloud MySQL database with its public endpoint enabled, and amysqldumpthat understands--ssl-mode(Oracle MySQL 5.7+; see the SSL note below for MariaDB)
composer require abigah/db-sync-from-prod --devOptionally publish the config:
php artisan vendor:publish --tag=db-sync-from-prod-configDB_SYNC_SOURCE selects how production is reached: ssh (default) or cloud. The rest of the variables depend on that choice and on whether your local connection is MySQL or SQLite.
SSH settings are shared by both drivers.
DB_SYNC_SOURCE=ssh
# Shared
PROD_SSH_HOST=prod.example.com
PROD_SSH_USER=deploy
PROD_SSH_PORT=22
# MySQL
PROD_DB_HOST=127.0.0.1
PROD_DB_PORT=3306
PROD_DB_USERNAME=app
PROD_DB_PASSWORD=secret
PROD_DB_DATABASE=app_production
# SQLite
PROD_DB_PATH=/var/www/app/current/database/database.sqliteTake these from "View credentials" in the Laravel Cloud dashboard. Note that PROD_DB_HOST here is the public endpoint you connect to directly, not a host as seen from a production server.
DB_SYNC_SOURCE=cloud
PROD_DB_HOST=db-xxxxxxxx.ca-central-1.db.laravel.cloud
PROD_DB_PORT=3306
PROD_DB_USERNAME=xxxxxxxxxxxxxxxx
PROD_DB_PASSWORD=secret
PROD_DB_DATABASE=production| Variable | Source | Driver | Default | Description |
|---|---|---|---|---|
DB_SYNC_SOURCE |
— | both | ssh |
Where production lives: ssh or cloud. |
PROD_SSH_HOST |
ssh | both | — | SSH host of the production server. |
PROD_SSH_USER |
ssh | both | — | SSH user on the production server. |
PROD_SSH_PORT |
ssh | both | 22 |
SSH port. |
PROD_DB_HOST |
both | mysql | 127.0.0.1 (ssh only) |
DB host: as seen from the production server (ssh), or the public endpoint (cloud). |
PROD_DB_PORT |
both | mysql | 3306 |
DB port. |
PROD_DB_USERNAME |
both | mysql | root (ssh only) |
DB username on production. |
PROD_DB_PASSWORD |
both | mysql | `` (empty) | DB password on production. |
PROD_DB_DATABASE |
both | mysql | — | Name of the production database. |
PROD_DB_SSL_MODE |
cloud | mysql | REQUIRED |
--ssl-mode passed to mysqldump. Leave empty to omit it. |
PROD_DB_SSL_CA |
cloud | mysql | — | CA bundle to verify the server certificate against. |
PROD_DB_PATH |
ssh | sqlite | — | Absolute path to the production SQLite file on the server. |
LARAVEL_CLOUD_DB_CLUSTER_ID |
cloud | mysql | — | Cluster whose public endpoint the command offers to open and close. |
LARAVEL_CLOUD_TOKEN |
cloud | mysql | the cloud CLI's tokens |
Laravel Cloud API token used to open and close the endpoint. |
DB_SYNC_LOCAL_CONNECTION |
both | both | config('database.default') |
Local connection (from config/database.php) to replace. |
DB_SYNC_BACKUP_DIR |
both | both | storage/backups |
Where local and production dumps are written. |
DB_SYNC_PRESERVE_LOCAL_AUTH |
both | both | true |
Put local passkeys and two-factor settings back after the refresh. |
The cloud source requires a MySQL local connection; Laravel Cloud does not host SQLite databases.
Laravel Cloud requires TLS, so the dump runs with --ssl-mode=REQUIRED by default: the connection is encrypted, but the certificate is not verified. To verify it, point PROD_DB_SSL_CA at a CA bundle (/etc/ssl/certs/ca-certificates.crt on Linux, $(brew --prefix)/etc/ca-certificates/cert.pem on macOS). Avoid VERIFY_IDENTITY — the endpoint hostname does not match the certificate.
MariaDB's mysqldump does not understand --ssl-mode. If you're using it, set PROD_DB_SSL_MODE= (empty) to drop the flag; the client negotiates TLS on its own.
The backup directory is created on first run and seeded with a .gitignore that ignores its own contents, so dumps don't accidentally end up in version control.
php artisan db:refresh-from-prodYou will be shown which local database is about to be replaced and asked to confirm. The command aborts unless APP_ENV=local.
| Option | Description |
|---|---|
--source=ssh|cloud |
Override the configured source for this run. |
--dump=PATH |
Use an existing dump/snapshot file instead of pulling a fresh one (skips the production download). |
--skip-local-backup |
Skip backing up the local database before replacing it. |
The mysqldump progress bar is driven by an estimate from information_schema.tables (SUM(DATA_LENGTH + INDEX_LENGTH)), which reports on-disk storage, not dump size. The SQL dump is usually smaller than storage for InnoDB tables — indexes aren't dumped, and text compresses differently than on-disk pages. Expect the bar to finish before reaching 100% and then jump to done. The reported final byte count is exact.
If information_schema isn't reachable (e.g. the DB user lacks SELECT on it, or the connection times out), the estimate is skipped and the dump runs without a progress bar.
MIT