Skip to content

Database

Database commands work with any project type: Laravel, Symfony, NestJS, Next.js, or any other framework. lerd automatically detects which database service to use through a resolution chain described below.

Commands

CommandDescription
lerd db:create [name]Create a database and a <name>_testing database
lerd db:import [-s service] [-d name] [--fresh] <file.sql>Import a SQL dump
lerd db:export [-s service] [-d name] [-o file.sql]Export a database to a SQL dump
lerd db:shell [-s service] [-d name]Open an interactive MySQL or PostgreSQL shell
lerd db:extension list|add <name>List or create the extensions an engine offers
lerd db:snapshot [name] [-A]Create a named, restorable snapshot of a database
lerd db:snapshots [--all]List stored snapshots
lerd db:restore <name> [-A] [-f]Restore a database from a stored snapshot
lerd db:snapshot:rm <name> [-A]Delete a stored snapshot
lerd db:snapshot:keep <name> [--off]Keep an automatic snapshot for good, exempt from retention
lerd db:snapshot:auto status|on|off|siteConfigure scheduled snapshots, globally or per site
lerd db create [name]Same as db:create (subcommand form)
lerd db import [-s service] [-d name] <file.sql>Same as db:import (subcommand form)
lerd db export [-s service] [-d name]Same as db:export (subcommand form)
lerd db shell [-s service] [-d name]Same as db:shell (subcommand form)
lerd db snapshot [name]Same as db:snapshot (subcommand form)
lerd db snapshotsSame as db:snapshots (subcommand form)
lerd db restore <name>Same as db:restore (subcommand form)
lerd db snapshot:rm <name>Same as db:snapshot:rm (subcommand form)
lerd db snapshot:keep <name>Same as db:snapshot:keep (subcommand form)
lerd db snapshot:auto ...Same as db:snapshot:auto (subcommand form)

Flags

FlagShortDescription
--service <name>-sTarget a specific lerd service (e.g. mysql, postgres, mysql-5-7)
--database <name>-dOverride the database name
--output <file>-oOutput file for db:export (default: <database>.sql)
--all-databases-ASnapshot or restore every database in the service at once
--force-fSkip the db:restore confirmation prompt
--freshEmpty the database before loading, so the dump replaces it (db:import)
--allList snapshots across every database on the service (db:snapshots)
--offPut a kept snapshot back under retention (db:snapshot:keep)
--every <duration>Schedule interval for db:snapshot:auto on (e.g. 6h)
--keep <n>Automatic snapshots kept per database, -1 for no limit (db:snapshot:auto on)
--keep-for <duration>Also drop automatic snapshots older than this (db:snapshot:auto on)
--selection <mode>opt-in (default) covers no site until one is included, opt-out covers every site until one is excluded (db:snapshot:auto on)

A named snapshot (lerd db:snapshot nightly) gets a UTC timestamp appended to its name, e.g. nightly-20260719-135558, so taking the same name twice never collides. Reference the full stamped name shown by db:snapshots when restoring or removing it.


Databases tab (web UI)

Each database engine's detail page in the web UI (Services → pick MySQL, MariaDB, PostgreSQL or MongoDB) opens on a Databases tab that shows the databases inside that engine as a grid of cards, each with its on-disk size. It surfaces the same operations as the CLI without leaving the browser:

  • Create a database inline from the field above the grid. Names accepted here are limited to letters, digits, underscores and dashes, up to 64 characters, which covers every name lerd generates and keeps the value safe to use as both a path segment and a SQL identifier.
  • Export a database to a .sql dump, or import a dump into one, from the card. An import reports itself while it runs: the card shows the dump's name with a progress bar, then a spinner once the last byte is in and the engine is still replaying it, and finally either a confirmation that fades away or the engine's own error, so a dump that fails halfway says why instead of quietly stopping. The daemon pipes the upload straight into the engine instead of reading the whole request into a temp file first, so the bar tracks what the engine has actually swallowed and a multi-gigabyte dump never lands on disk twice. A load that the engine accepted while still complaining ends on an amber warning with the error count, because psql exits 0 even when every statement in a dump failed and a silent green tick over a half-empty database is worse than no feedback at all. Opening the warning lists every distinct complaint with how often the engine made it, in the order it hit them, so a dump replayed over a populated schema shows all of what it tripped over rather than the first few.
  • Snapshots are managed on the card of the database they belong to: take a snapshot, restore one (with a confirmation, since a restore overwrites the current data), delete one (also confirmed), or download one as a plain .sql dump. Taking, restoring and deleting all report what they are doing in the snapshots modal and leave a confirmation or the engine's error behind, so a slow restore of a large database is visibly working rather than apparently frozen. A snapshot is keyed on the engine and database it was taken from, never on a site, so it lives with the database rather than on the site page. A named snapshot gets a UTC timestamp appended (nightly-20260719-135558), so repeated snapshots of one name never collide; the list shows the parsed time and sorts newest first.
  • Copy connection string builds a ready-to-paste DSN for that specific database, which works whether or not an admin UI is installed.
  • Open in the admin UI appears on the card when an admin tool is installed for the engine, and opens it straight to this database when the tool supports a per-database URL (phpMyAdmin and Adminer for MySQL/MariaDB, Mongo Express for MongoDB). pgAdmin has no such URL, so it opens at its root.
  • The linked site, when a site owns the database, is shown as a link on the card that jumps to that site. Which database a site owns is read through that site's framework definition: the env file it declares, in the format it declares, at the keys it declares, so a WordPress site is matched on DB_NAME in wp-config.php and a Magento site on db.connection.default.dbname in app/etc/env.php, the same as a Laravel site is on DB_DATABASE in .env. A <name>_testing database links to the same site as <name>. A worktree's isolated database is shown under the branch's own domain (staging.astrolov.test for the staging branch of astrolov.test), so it reads as that branch's data rather than as a stray database of the parent site, and the link still opens the parent site's page.
  • A <name>_testing database shares the card of the <name> database it tests, rather than taking a second card of its own for what is usually an empty database. The card header carries an App/Testing segment, and the name, size, linked site and every action below it act on whichever half is selected, so an export, an import, a snapshot or a drop always applies to the database currently shown. Dropping the app half offers to take its testing database along, in a checkbox that names that database outright and starts ticked, since the two were created together and the half left behind belongs to nothing and points at nothing. Both go in a single request that either drops the pair or reports which half it could not, rather than two drops fired from the browser. The testing half is offered nothing of its own, being nobody's pair. A _testing database whose matching database does not exist keeps an ordinary card of its own.

The same "open in the admin tool" affordance is on the database service card in a site's own overview (a database-icon button), so from a site you can jump straight into that site's database in phpMyAdmin, Adminer or Mongo Express.

Which operations an engine offers follows what its preset declares: each card button (export, import, snapshots, drop) and the create field appear only when the engine's preset declares the matching action, so an engine that can list and drop but not dump shows exactly that. A document engine like MongoDB lists its databases and exposes the connection string and admin link, and gains the rest the moment its published preset declares the actions, with no lerd release. A stopped engine shows a prompt to start it rather than an empty grid. A running one shows a loading line while its databases are on their way, since reading an engine that has not answered yet as a stopped one would ask you to start something that is already up, and a request that never lands says so with a retry instead.

Snapshots tab (web UI)

Next to the Databases tab, every database engine carries a Snapshots tab: the engine's whole snapshot history as one table rather than a card at a time, newest first, with the site and database each snapshot came from, when it was taken, its size, and what retention will do with it (nothing for one you took by hand, a date for an automatic one, kept for good for one you pinned).

The card above the table states the schedule at a glance: how often it runs (a count and a unit), how many snapshots it keeps per database, any maximum age, whether it is opt-in or opt-out, how many of the engine's databases are covered, and what its snapshots cost on disk. Changing any of it opens the snapshot settings dialog, so there is one place the schedule is edited rather than a second set of controls per engine.

Two filters narrow the table, by site and by when it was taken (last 24 hours, 7 days, 30 days, or older than 30), and a long history is paged twenty rows at a time. Each row carries four actions: keep an automatic snapshot (or put it back under retention), download it as a dump, restore it, and delete it. Restore and delete both ask first, naming what they are about to overwrite or remove.

A checkbox in front of each row, and one in the table header, select snapshots for removal together. The header checkbox covers the rows on screen, never one the filters or the paging are hiding, while what you picked on another page stays picked, which is what the removal count reflects.

The site column names the site that owns the database, so a snapshot taken by hand and one taken by the schedule read as the same project rather than as a domain and a name.

The tab's header says how many of the engine's databases are on the schedule and opens the snapshot settings dialog, the same one reachable from any database's snapshots dialog and from System → Snapshots: the schedule, whether databases are opted in or opted out by default, how many snapshots to keep per database, and an optional maximum age. It is one global policy, so it reads the same wherever it is opened.

Which databases are covered is set per database, and the quickest way is the clock next to a database's size on the Databases grid: it is lit while the schedule covers that database, dimmed while it does not, and clicking it includes or excludes it in one go. The same switch, with a line saying when the schedule last ran and next runs, is in that database's snapshots dialog. A database no linked site points at carries neither, since the schedule resolves its targets through sites.

Clicking that switch records an explicit yes or no for the database, so changing the selection mode afterwards leaves it where you put it. lerd db:snapshot:auto site <name> default hands one back to the policy.

The whole tab is loopback only. Reaching the dashboard over the LAN, even as a remote-control client with valid credentials, the tab is not offered at all and every database endpoint behind it answers 403, because this surface both reads a database out in full and drops or overwrites it, and the raw .env view is already held to the same rule for carrying the credentials to it. Drive databases from the machine lerd runs on.

Which databases an engine advertises, their sizes, and the commands behind every operation all come from the introspect.entities block declared in the engine's service preset, so a newly added engine works here as soon as its preset ships the declarations. The size is the data you put there, not the engine's own overhead: every postgres database inherits roughly 7.5 MB of system catalogs from template1, so that baseline is netted off and an empty database reads as empty, the same as it does on MySQL.

Services that hold something other than databases declare it the same way: a preset with a buckets or keyspaces entity gets an overview tab on its service page, rendered as the same card grid as databases, with the declared columns on each card, an inline create when the preset declares one, per-row actions with a confirmation in front of anything declared destructive, and the owning site linked on the card when the preset declares which .env key claims a row. A declared export downloads the entity as one file (a RustFS bucket comes down as a .tar.gz of its objects) and a declared import loads such an archive back, accepting the compressed and the plain form alike, with the same streamed upload progress database imports have. That tab is held to the same loopback-only rule as the Databases tab, and to the same start, loading and retry states.

Service and database resolution

Every db command resolves which service to target and which database to use through the following chain (first match wins):

  1. --service flag: explicit override, e.g. lerd db:shell --service postgres
  2. .lerd.yaml db: block: declared in the project root, works even on unlinked sites
  3. Framework definition: lerd detects the framework and reads the env file the definition declares, in the format it declares, at the keys it declares: Symfony's DATABASE_URL in .env.local, WordPress's DB_NAME and DB_HOST in wp-config.php, Magento's dotted keys in app/etc/env.php
  4. .env key inference: reads DB_CONNECTION, DB_TYPE, TYPEORM_CONNECTION, DATABASE_URL, or DB_PORT from .env
  5. Error: with instructions listing all options above

The --database flag overrides the database name at any resolution level.

.lerd.yaml db: block

Add a db: block to .lerd.yaml to set a persistent default for the project. Useful for non-PHP projects that don't have a lerd framework definition.

yaml
db:
  service: postgres
  database: myapp

Supported .env keys

When falling back to .env inference, lerd checks the following keys in order to determine the database type:

KeyFrameworks
DB_CONNECTIONLaravel (mysql, pgsql, etc.)
DB_TYPETypeORM / NestJS (postgres, mysql, etc.)
TYPEORM_CONNECTIONTypeORM CLI
DATABASE_URLPrisma, Drizzle, Symfony, Next.js (postgresql://..., mysql://...)
DB_PORTLast resort: 5432 for postgres, 3306/3307 for mysql

The database name is resolved from DB_DATABASE, TYPEORM_DATABASE, or the path component of DATABASE_URL (Prisma's ?schema=public suffix is stripped automatically).


lerd db:create name resolution

Name is resolved in this order (first match wins):

  1. Explicit [name] argument
  2. Database name from the resolution chain above
  3. Project name derived from the registered site name (or directory name)

A <name>_testing database is always created alongside the main one. If a database already exists the command reports it instead of failing.


Snapshots

Snapshots are named, restorable point-in-time copies of a database, stored inside lerd's own data directory. Use one as a safety net before a risky migration, a branch switch, or any destructive experiment, then roll back in a single command. An engine supports snapshots when its preset declares both an export and an import action, which the bundled MySQL and PostgreSQL presets do; store engines gain them through a store publish.

bash
lerd db:snapshot pre-migration       # snapshot the current project database
lerd db:snapshot                     # name omitted: auto-named snapshot-<timestamp>
lerd db:snapshots                    # list snapshots for this database
lerd db:restore pre-migration        # restore it (prompts for confirmation)
lerd db:snapshot:rm pre-migration    # delete it

Snapshots live under ~/.local/share/lerd/snapshots/<service>/, one directory per snapshot holding a gzipped SQL dump and a meta.json sidecar. They are scoped to a (service, database) pair, so two projects can both keep a snapshot called pre-migration without colliding. The same service-and-database resolution chain as every other db command applies, so from inside a project directory the snapshot commands just work.

Restoring

lerd db:restore <name> is destructive. A per-database restore drops and recreates the target database before loading the dump, so the restore is clean with no leftover tables. It prompts for confirmation; pass --force to skip the prompt (required when running non-interactively, e.g. in a script).

All databases

Pass --all-databases (-A) to snapshot or restore every database in the service at once instead of a single one:

bash
lerd db:snapshot --service mysql --all-databases nightly
lerd db:restore --service mysql --all-databases nightly

An all-databases restore drops and recreates every database contained in the snapshot, but leaves databases that aren't in the snapshot untouched.

Snapshots before a data wipe

lerd service remove <name> --purge and lerd service reinstall <name> --reset-data rename the data dir aside, and a renamed directory only reads back under the image that wrote it, which after a reinstall on another version is an image you no longer have. So before either wipes anything, lerd snapshots every database on the service, the same all-databases snapshot db:snapshot -A takes:

bash
lerd db:snapshots --service mysql --all      # pre-remove-<ts> / pre-reset-data-<ts> are listed here
lerd db:restore --service mysql -A pre-reset-data-20260809-141500

The name says where it came from, so it is still recognisable weeks later. Services that declare no export action, and services with no data dir yet, have nothing to snapshot and are skipped.

The snapshot has to come off a running engine, so lerd starts the service if it is stopped. If the snapshot cannot be taken the operation stops before touching anything, rather than wiping without one. Pass --no-snapshot to go ahead anyway, for an engine that will not come up or data you know is disposable.

Reserved names

db:snapshot rejects names that look like command verbs (list, rm, delete, restore, …), so lerd db snapshot list errors with a hint instead of silently creating a snapshot literally named "list". Use lerd db:snapshots to list.

Automatic snapshots

lerd can take these same snapshots on a schedule, so a migrate:fresh on the wrong database is a restore away rather than a lost afternoon. The schedule ships on, in opt-in mode, which means it dumps nothing until you name a database: turning it on is one click per database rather than a setting to discover.

bash
lerd db:snapshot:auto status                     # the policy, and when each database was last taken
lerd db:snapshot:auto on --every 24h --keep 7    # daily, keeping the last 7 per database
lerd db:snapshot:auto off                        # stop the schedule entirely

An automatic snapshot is an ordinary snapshot: same directory, same meta.json, restored and deleted with the same commands. It is only marked as automatic, which is what puts it under retention. Retention never touches a snapshot you took by hand, so a pre-migration you made yourself is safe whatever the schedule does.

The watcher checks every hour whether the schedule is due, and skips a database whose engine is stopped rather than starting containers behind your back; the next check picks it up once the engine is running again. Two sites sharing one database are snapshotted once, not twice.

Which sites are covered

The schedule works either way round, set by its selection mode:

  • Opt in (the default) snapshots nothing until you name the databases you want.
  • Opt out snapshots every site's database, and you exclude the ones you don't want.
bash
lerd db:snapshot:auto on --selection opt-in     # nothing until a site opts in
lerd db:snapshot:auto on --selection opt-out    # everything except what opts out

A site follows that mode unless you say otherwise: under opt-out you exclude a noisy site, under opt-in you include the two that matter. The schedule's own switch still gates everything, so turning it off stops every site, including one that opted in.

bash
lerd db:snapshot:auto site on         # this site, always (run from the project directory)
lerd db:snapshot:auto site shop off   # that site, never
lerd db:snapshot:auto site shop default

Knowing it ran

A finished run raises a Snapshots taken notification naming how many databases it took and on how many sites, in its own category so it can be muted on its own (System → Notifications). It stays quiet when a run takes nothing, and nothing is announced when a run starts.

Retention, and keeping one for good

--keep <n> keeps the last n automatic snapshots per database; --keep-for <duration> also drops any older than that. Whichever fires first wins. db:snapshots shows what each snapshot's fate is in a RETENTION column: manual for one you took, auto, ~3d for one the count rule will drop in about three days (an estimate, because it moves with the schedule), auto, 5h for an exact age cutoff, and auto, kept for one that is never dropped.

To rescue an automatic snapshot from retention:

bash
lerd db:snapshot:keep auto-20260903-101500          # keep it for good
lerd db:snapshot:keep auto-20260903-101500 --off    # back under retention

A kept snapshot takes no slot in the rolling window, so pinning one does not shrink the set of automatic snapshots you keep.

The same policy is in the web UI under System → Snapshots and on each database engine's Snapshots tab, in the TUI's Settings pane and on a site's toggles, and over MCP through the db tool's auto, auto_set and snapshot_keep actions.

What an export carries

Every export lerd produces, from the CLI, the web UI download and the MCP tool, carries the same flags a snapshot already used, so handing a colleague a dump and having them import it through lerd gives them what you have.

It drops each object before recreating it, so the load replaces what they had rather than colliding with it. mysqldump writes DROP TABLE on its own; pg_dump only does it when asked, so lerd passes --clean --if-exists.

It carries stored procedures, functions and events. pg_dump writes functions either way, but mysqldump leaves routines and events out unless asked, so a mysql or mariadb dump taken without --routines --events hands over a database that looks complete and is not.

Two things a dump cannot carry across on its own. Dropping only covers the objects the dump contains, so a table the other machine has and yours does not survives the import. And an extension your database uses has to exist in their engine too, so a dump from postgres-pgvector will not load into plain postgres.

Dumps from a managed provider

A dump taken from a hosted Postgres or MySQL carries statements about that host's own roles: ALTER ... OWNER TO, GRANT, REVOKE and ALTER DEFAULT PRIVILEGES on postgres, and a DEFINER clause on every view, trigger and routine on the mysql families. lerd's engines run a single admin role, so none of it can apply here. On postgres each one lands as an error you cannot act on, and on mysql it is worse: the object is created and only fails when something uses it, with ERROR 1449: The user specified as a definer does not exist.

lerd filters those out on the way in, which is what --no-owner --no-privileges does at dump time, except you do not have to have thought of it when you took the dump. A bare CREATE SCHEMA is made conditional for the same reason: every database lerd creates already has a public schema, so the statement can only ever fail, and the database is identical either way. What it skipped is reported next to the errors, since a dump lerd quietly rewrote should never read as one that arrived clean. Row data is never touched: a postgres COPY block is passed through byte for byte between its header and its closing \., and on mysql only DDL lines are rewritten, so a value that happens to contain the word survives.

Ownership is the smaller half of what goes wrong. The rest comes from loading into a database that already has the objects, which no filter can fix: the schema, tables, sequences, indexes and constraints all collide, and then the rows land on populated tables as duplicate keys and foreign key violations. Tick Empty the database first in the import dialog, or pass --fresh on the CLI, and the database is dropped and recreated before the dump loads, so it replaces what was there rather than fighting it.

Extensions

A postgres engine declares in its service preset which extensions its image can create and which types each provides, so lerd never carries a list of extension names in its own code. Two things follow from that declaration.

An extension marked as always-on is created wherever lerd creates a database, so a project on postgres-pgvector gets vector in both its app and _testing databases, and a database that is dropped and recreated, by an import with Empty the database first or by a snapshot restore, comes back with it rather than missing what the site was built on. A database that already exists is topped up the same way, so a site set up before its engine declared an extension picks it up rather than staying behind.

The rest are created only when an imported dump reaches for one, matched on the type names the preset declares. That is how a dump holding a public.geometry column brings PostGIS with it on the default engine without every database on the machine paying for it: an empty postgres database is about 7.5 MB, and PostGIS doubles it, while vector costs a third of a megabyte, which is why one waits and the other does not. What was created is reported next to the import, since a database should never gain something without saying so.

lerd db:extension list shows what the engine offers against what the database already has, and lerd db:extension add <name> creates one when you want it before any dump arrives.

Compressed and custom-format dumps

A .sql.gz imports like a .sql. The engine clients read plain SQL, so lerd looks at the head of the upload and decompresses it on the way in rather than handing gzip bytes to psql, which would report a couple of encoding errors, load nothing and still exit clean.

A custom-format archive (pg_dump -Fc) is not a SQL file at all, and psql says so: the import fails with "The input is a PostgreSQL custom-format dump. Use the pg_restore command-line client". Export it as plain SQL, or --format=plain, and it imports.

Imports that finish with errors

psql exits 0 whether a dump loaded cleanly or every statement in it failed, so lerd db:import, lerd db:restore and a cross-version service migrate count what the engine wrote and end on a warning instead of "import complete" when it complained. On the terminal the warning spells out the first few complaints with their counts and folds the rest into a tally, which is usually enough to name the cause on sight; the web UI lists them all: a flood of invalid command \N, or of backslash commands are restricted on PostgreSQL 18, means a COPY block had no table to load into, so the failure is further up in whatever stopped that table from being created. Both phrasings are the same thing and both fold into a single line in the report, so the cascade never crowds out its cause.

Large dumps and max_allowed_packet

A big restore that dies partway with "Lost connection to MySQL server during query" is almost always a single SQL statement exceeding max_allowed_packet, which is enforced on both the client and the server. lerd db:import, lerd db:restore, and a cross-version service migrate all raise the client ceiling to 1G automatically, so the client is never the bottleneck, and the bundled MySQL config ships a max_allowed_packet of 256M on the server. If a dump has an even larger single statement, raise the server ceiling in the service Config tab (or the zz-*.cnf tuning file) under [mysqld] and run lerd service restart <name>:

ini
[mysqld]
max_allowed_packet = 1G

When you restore with an external client instead (a GUI, a manual mysql call), raise the packet size there too, either with mysql --max-allowed-packet=1G or a matching [client] entry in the same tuning file.


Picking a database for a Laravel project

The database for a Laravel project is configured through .lerd.yaml and applied to .env when lerd env runs (which the lerd init wizard calls automatically). The supported choices are:

ChoiceService.env keys written
sqlitenone (local file)DB_CONNECTION=sqlite, DB_DATABASE=database/database.sqlite
mysqllerd-mysql (Podman)DB_CONNECTION=mysql, DB_HOST=lerd-mysql, DB_PORT=3306, DB_DATABASE=<project>, DB_USERNAME=root, DB_PASSWORD=lerd
postgreslerd-postgres (Podman)DB_CONNECTION=pgsql, DB_HOST=lerd-postgres, DB_PORT=5432, DB_DATABASE=<project>, DB_USERNAME=postgres, DB_PASSWORD=lerd

Installed family alternates are valid picks too: mariadb / mariadb-10-11, mysql-5-7, postgres-pgvector / postgres-17, etc. They go through the same env-write + database-create flow as the built-ins, using the host and port from their preset. Install one first with lerd service preset <name>, then list it in .lerd.yaml under services: or pick it in the lerd init wizard.

For SQLite, the database/database.sqlite file is created automatically if it doesn't exist. No service is started.

Because no service is started, the commands on this page do not apply to a SQLite project: they drive a database engine running in a container, and SQLite is a file your framework opens directly. Running one there refuses with a message naming the file it found rather than trying to treat the path as a database name. Back a SQLite project up by copying the file, and use your framework's own tooling (php artisan migrate:fresh, for example) to reset it. To move the project onto an engine these commands do cover, run lerd db:move --to mysql, or point a single command at a service you already have with --service.

For MySQL or PostgreSQL (and their family alternates), the matching lerd-<service> container is started if it isn't already, and the project database (plus a _testing variant) is created via lerd db:create.

You can change the choice at any time by editing the services: list in .lerd.yaml and re-running lerd env, or by running lerd init --fresh and picking a different database in the wizard.


Using a database you run on the host

A project can use the MySQL, MariaDB or PostgreSQL already installed on the machine instead of lerd's container, keeping your existing data and users while lerd manages the rest. The connection values and the LERD_EXTERNAL_SERVICES opt-out go in the project's .env.lerd_override; the host server also has to accept a connection that does not come from its own loopback, which on Linux means changing what it listens on and who it grants to. See Using a service you run on the host.

Note that the lerd db:* commands on this page resolve their target from the service rather than from DB_HOST, so they keep talking to lerd's container. Use your own mysql or psql client against a host-run server.


Moving sites between services

lerd service migrate <service> <version> upgrades one service in place (e.g. postgres from 16 to 18): the service keeps its name, so every site on it follows automatically and no .env changes. Use that when you want to move everyone off a major version at once. See Service updates.

lerd db:move is the other half: when you run two services of the same family side by side (e.g. the canonical postgres and an installed postgres-18 alternate), it moves selected sites from one to the other and repoints their .env. For each site it dumps the database from the source, creates and restores it on the target, then rewrites the site's .env DB_HOST/DB_PORT (the same code path as lerd env, so host-proxy sites get loopback host + published port). The source data is left intact as a safety net.

Run it without flags for an interactive wizard:

bash
lerd db:move
# ? Move databases from which service?  postgres (3 sites)
# ? Move to which service?              postgres-18
# ? Which sites?                        [x] shop  [x] blog  [ ] api

Or script it:

bash
lerd db:move --from postgres --to postgres-18 --all      # every site on postgres
lerd db:move --from postgres --to postgres-18 --site shop --site blog
lerd db:move --from postgres --to postgres-18 --all --force   # skip the confirmation prompt

Both services must already be installed and in the same family (mysqlmysql-5-7, postgrespostgres-18, etc.); cross-family moves are rejected. A site's current service is detected from its .lerd.yaml services:/db: entry, falling back to the lerd-<service> hostname in .env. The target's _testing database is recreated empty by the env step; only the primary database is copied. Because the source data is preserved, clean it up by hand once you're happy with the move (drop the old databases via lerd db:shell --service <source>, or reinstall/remove the old service).

The repoint reuses lerd env, so the site needs a detectable framework (Laravel, Symfony, etc.); if the env step fails the .lerd.yaml change is rolled back so the site stays on its original service.


Non-PHP projects

For projects without a lerd framework definition (NestJS, Next.js, Go, etc.), db commands work without any lerd-specific configuration if the project's .env uses a recognised key:

bash
# NestJS / TypeORM, DB_TYPE is sufficient
lerd db:shell

# Next.js / Prisma, DATABASE_URL is sufficient
lerd db:shell

# No .env at all, use --service
lerd db:shell --service postgres --database myapp

# Or declare it once in .lerd.yaml
# db:
#   service: postgres
#   database: myapp
lerd db:shell

Client tools for external databases and IDEs

The db:* commands work against lerd's own service containers. When you need to dump or query a database that lives outside lerd, for example a managed cluster on DigitalOcean, or you want to point an IDE like PhpStorm at a real mysqldump executable, lerd exposes the client tools that already ship inside its database images as host shims.

A service declares which tools it exposes in its YAML, so the set grows with the store. Today: mysql and mariadb expose mysql and mysqldump (mariadb backed by the mariadb/mariadb-dump binaries); postgres and its pgvector/timescaledb variants expose psql, pg_dump, pg_dumpall, pg_restore; redis exposes redis-cli; valkey exposes valkey-cli; and mongo exposes mongosh, mongodump, mongorestore, mongoexport, mongoimport. Each becomes a shim on your PATH in ~/.local/share/lerd/bin.

When you install a service, lerd installs its shims. If you do not already have the tool on your system there is nothing to shadow, so the shim is installed automatically. If you do already have the tool installed, lerd asks first (default no) because the shim sits ahead of your own binary on PATH. Removing a service removes its shims. A tool added to a service in the store reaches an already-installed service on the next lerd update, without a reinstall.

The shims pass every argument straight through, so targeting an external host is just a matter of supplying your own connection flags:

bash
# Dump a managed database to a file in the current directory
mysqldump -h db.example.com -P 25060 -u doadmin -p yourdb > dump.sql

# Same for postgres
pg_dump -h db.example.com -p 25060 -U doadmin -d yourdb > dump.sql

Each shim runs its tool inside a throwaway container, so a loopback host (127.0.0.1, localhost, ::1) is not automatically read the same way: if its port matches one your own lerd services actually publish, the shim recognises it and routes to that service internally instead of trying (and failing) to reach the host's own loopback from inside the container. psql -h 127.0.0.1 -p 5433 mydb, the natural way to hit the port lerd service start postgres-timescaledb printed, works the same way psql mydb does. A loopback host whose port matches nothing lerd owns, or any other host, is left untouched.

Each tool runs in a throwaway container spun from the service's image, so nothing touches your running database container. Your home directory is mounted read-write, so the tool can read a CA cert and write its output anywhere under it, whether you use a shell redirect (> dump.sql), the tool's own --result-file/-f flag, or an IDE that fills one in. Output files are owned by you, not root.

Managed databases usually require TLS. Keep the CA file you pass with a flag like --ssl-ca somewhere under your home directory so the tool can read it:

bash
mysqldump -h db.example.com -P 25060 -u doadmin -p --ssl-ca=ca.crt yourdb > dump.sql

When you give no host, the tool connects to a local lerd database with its admin credentials, so pg_dump mydb or mysqldump mydb just works. If you run it from a project directory, it targets that project's own database service, read from the project's DB_HOST, so a mariadb-backed project routes to your mariadb container rather than the default mysql one. Outside a project, or when the project's database is a different family than the tool, it falls back to the family's default service. Passing -h an external host turns all of this off and the shim forwards everything untouched; passing -h a loopback host that names one of lerd's own published ports routes the same way the hostless case does, for the reason above. A connection URI (postgresql://user@host/db) or a libpq conninfo string (host=… dbname=…) names its host too, so it counts as an explicit target and passes through exactly as written, with lerd's own credentials kept out of it. That holds even when the URI spells a loopback host, since its host and port live inside the string rather than in flags lerd can rewrite: reach a lerd service that way and use the container name, or drop the URI for flags. For scripted local dumps lerd db:export is still the tidier option; the raw shim is there for external databases and IDEs.

Run from inside a git worktree, the shim reads that checkout's own env file rather than the parent site's, so a branch with an isolated database dumps from its own schema even when the worktree lives inside the parent's directory. A worktree whose env was never rewritten keeps using the parent site's.

To point an IDE at a tool, use its shim path, for example ~/.local/share/lerd/bin/mysqldump. IDEs validate the path by running the tool with --version first, and a bare --version or --help is always forwarded exactly as given, without the local-database default and without starting the service behind it.

The connection in a JetBrains project

An IDE cannot work out a site's database on its own. The site's .env names the container and its internal port, lerd-postgres-pgvector:5432, which is correct inside the network and unusable from the machine, where the same engine answers on 127.0.0.1 and whatever port it was published on. No format is shared across IDEs for a project to declare a database either, so the connection normally gets typed by hand, and typed wrong the moment two engines of one family push the second onto a shifted port.

JetBrains keeps its data sources in <project>/.idea/dataSources.xml, so lerd maintains one entry there, named <database> (lerd), scoped to the project's own database rather than the whole server, which on a busy machine holds every other project's databases too.

It is written on lerd link, refreshed by lerd env and by the group commands, and removed by lerd unlink. An entry lerd owns whose database the project no longer uses is dropped rather than left behind, which is what keeps a site that moved onto a group's shared database from carrying two connections. The port is read fresh every time, so an engine that moved is picked up rather than remembered wrong.

A grouped secondary sharing the main site's database gets the main's database, because sharing rewrites the secondary's own DB_DATABASE, which is what the connection is resolved from.

A project linked before this existed picks it up the next time either command runs there, which is also how a connection catches up with an engine that moved to another port.

lerd only ever touches a project that already has a .idea directory, because that directory existing is what says the project is open in a JetBrains IDE; it never creates one. Inside the file it owns exactly one entry, keyed on a stable identifier derived from the project path, so your own data sources are left byte for byte as they were. The URL carries no credentials. The user goes into dataSources.local.xml, which is where the IDE reads it from, and the password is left to JetBrains' own credential store, which lerd cannot write to, so the first connection asks for it once. It is the fixed local one the site's .env already spells out.

Set ide_data_source: false in ~/.config/lerd/config.yaml to leave IDE files alone entirely. IDEs validate the path by running the tool with --version first, and a bare --version or --help is always forwarded exactly as given, without the local-database default and without starting the service behind it.

Managing shims

List the shims your installed services expose and whether each is installed:

bash
lerd shims

Add or remove an individual shim, for example if you declined it at install time and later want it, or you would rather keep your own binary on PATH:

bash
lerd shims remove mysqldump   # take lerd's shim off your PATH
lerd shims add mysqldump      # put it back

The same per-tool toggles are on each database service's Tools tab in the web UI. When two services of the same family are installed (say mysql and mariadb, which both provide mysqldump), one owns the shim and runs it; the others show that tool disabled on their Tools tab so it is managed in one place.

Whether a tool reads as installed is answered by the shim dir itself rather than by the choice you made, so a tool counts as on only while its shim is really there. If the name is already taken in ~/.local/share/lerd/bin by a file lerd did not write, it is left alone and adding the shim fails with that path instead of being recorded as on, both from the command and from the toggle in the web UI. Remove the file and add the shim again.

Recovering after a service reinstall

lerd service reinstall <name> walks every active site that depends on the service and recreates the database it expects via CREATE DATABASE IF NOT EXISTS, whether or not the data was wiped; --reset-data wipes the data dir first (rename-aside, recoverable). Database name resolution is the same as lerd env: .lerd.yaml db.database first, then .env DB_DATABASE, then a name derived from the site name.

The DBs come back empty. To get the contents back, restore the snapshot the reinstall took before wiping: lerd db:restore --service <name> -A pre-reset-data-<timestamp> (see snapshots before a data wipe).

The previous data also lives next door as ~/.local/share/lerd/data/<name>.pre-remove-<timestamp>, but that directory is only readable by the image that wrote it. It is the fallback when the reinstall stayed on the same version, or when you passed --no-snapshot: stop the service, rename the aside dir back over the new data dir, and start the service again.

If you only want to recreate a single missing database without wiping the whole server, use lerd db:create against the live service instead.

Released under the MIT License.