Skip to content

Services ​

Built-in services ​

CommandDescription
lerd service start <name>Start a service (auto-installs on first use)
lerd service stop <name>Stop a service container
lerd service restart <name>Restart a service container; refreshes the quadlet first so config edits land
lerd service status <name>Show systemd unit status
lerd service listAll services with status, version, and an Update column
lerd service search [query]Browse every service preset, bundled and store alike; install a hit with lerd service preset <name>
lerd service update <name> [tag]Pull a newer image and restart; tag selects an explicit upgrade target
lerd service update --allApply the safe update to every installed service that has one, one at a time, reporting each failure without stopping the rest
lerd service migrate <name> <version>SQL dump + restore for cross-version moves (mysql, mariadb, postgres); <version> is a preset version label such as 18
lerd service rollback <name>Swap back to the previously-running image (toggles)
lerd service pin <name>Pin a service so it is never auto-stopped
lerd service unpin <name>Unpin a service so it can be auto-stopped when unused
lerd service expose <name> <host:container>Publish an extra port on a built-in service
lerd service expose <name> <host:container> --removeRemove a previously exposed port
lerd service port <name> <port>Move a service's primary published host port (e.g. free 3306 for a host server)
lerd service port <name> <port> --container <cport>Move a specific mapping of a multi-port service (e.g. Mailpit's 8025 UI)
lerd service port <name> --resetReset a service to its preset default published port

Available services: mysql (8.4 LTS canonical, 9.7 LTS / 5.7 alternates), redis (7-alpine), postgres (16 canonical with PostGIS, 17 / 18 alternates), meilisearch (v1.42), rustfs (S3-compatible), mailpit (SMTP catcher).

Default services are defined as YAML presets with default: true in the lerd binary. Adding or replacing a default service is a YAML edit, not a code change. Each preset declares its own update_strategy (patch / minor / rolling), whether track_latest should auto-bump fresh installs to the current upstream, whether allow_major_upgrade lets the cross-strategy upgrade button cross numeric majors, and where the engine records the version that wrote its data (data_version_file) so a data dir that outlives its config still gets a server that can open it. See Service updates for the full update / upgrade / migrate / rollback flow.

lerd service list shows the version (derived from the image tag) and an Update column with green / amber / violet badges:

╭─────────────┬─────────┬────────┬────────╮
│ Service     │ Version │ Status │ Update │
├─────────────┼─────────┼────────┼────────┤
│ mailpit     │ latest  │ active │        │
│ meilisearch │ v1.42.1 │ active │        │
│ mysql       │ v8.4.9  │ active │        │
│ postgres    │ v16     │ active │        │
│ redis       │ v7.4.8  │ active │        │
│ rustfs      │ latest  │ active │        │
╰─────────────┴─────────┴────────┴────────╯

The Web UI, the TUI, and lerd status display the same labels. Services pinned to rolling tags (latest, main) show the tag verbatim. Services where an update is available show → <new-tag>; cross-strategy upgrades show ⇧ <new-tag> in amber.

An interrupted first start ​

Commands that run in a site, lerd php, lerd composer, console commands and the shims, make sure the services the site's .env names are up and accepting connections first. A service that has answered once is trusted for the rest of that run, so later commands do not ask it again: lerd notes the run by systemd's invocation id on Linux and by the container's start time and process id on macOS, and probes again once the service has been restarted, woken from idle-suspend or started for the first time. On macOS the running checks a command makes share a single podman call rather than one VM round trip per service. A php run that only asks PHP about itself, -v, -m, -i, --ini, --ri <ext> or -l on files, runs no project code, so it starts no services and does not wake the site's workers from idle-suspend.

A database engine initialises its data directory the first time it starts, and a start stopped halfway through (a lerd stop or a reboot at the wrong moment) leaves files the engine then refuses to open on every later start. lerd remembers which data directories it created and that have not come up since, and when such a service fails to become ready it moves the unfinished directory aside as <name>.incomplete-first-start-<time> under ~/.local/share/lerd/data, then starts the service once more on a clean one. A data directory that has ever come up, or that lerd did not create, is never touched.

Exposing extra ports on bundled services ​

Bundled services publish a fixed set of ports by default. Use lerd service expose to bind additional host ports without recompiling or replacing the service. This works for any service lerd ships as a preset, both the default-stack ones (MySQL, PostgreSQL, Redis) and the optional ones you install on demand (Gotenberg, MongoDB, Elasticsearch, and so on). Only genuinely custom services you define yourself are excluded, since those declare their ports in their own YAML.

bash
# Expose MySQL on an extra port (e.g. for a second GUI client using a different port)
lerd service expose mysql 13306:3306

# Remove the extra port
lerd service expose mysql --remove 13306:3306

Extra port mappings are persisted in ~/.config/lerd/config.yaml under services.<name>.extra_ports and are applied automatically every time the service starts. If the service is already running when you run expose, it is restarted immediately to apply the change.

You can also edit ~/.config/lerd/config.yaml directly:

yaml
services:
  mysql:
    extra_ports:
      - "13306:3306"

Then apply with lerd service restart mysql.

You can also manage extra ports from the dashboard: open a service and switch to the Ports tab, then add or remove mappings alongside the published port. The CLI, dashboard, MCP and TUI all route through the same logic, so a change made on one surface shows up on the others.

Moving a service's published host port ​

Each service publishes on a default host port (MySQL 3306, PostgreSQL 5432, Redis 6379, and so on). Every member of a service family shares that one canonical port rather than pre-spacing itself, so a single database of any family lands on the familiar port. When lerd writes a service's quadlet and the port can't be bound, or another installed same-family service already holds it, it shifts the service to the next free port and records it, so the container comes up cleanly instead of failing to bind. The decision is made purely from port availability and what other lerd services already claim: lerd never inspects host files, sockets, or installed packages. It applies to every service, not just databases, and to every published mapping, not just the primary one. A port a service is already recorded on is checked the same way on Linux: something else can take it while the service is down, so rather than fail to bind on the next start, the service moves to a free port and records that. A service whose container is up is never moved, since the listener holding its port is its own. On macOS a recorded port is not re-checked, because the Podman VM can keep a service's own port bound after it stops, and moving it then would leave the sites using it behind; if something else really has taken it, move the service with lerd service port.

Move a port yourself, or undo an automatic shift, with lerd service port:

bash
# Publish lerd-mysql on 3307 so a host-installed MySQL can keep 3306
lerd service port mysql 3307

# Go back to the preset default
lerd service port mysql --reset   # or: lerd service port mysql 0

The container-internal port never changes, so containerized apps (which reach the service by name over the lerd network) are unaffected. Only host clients pointed at the old published port need to follow. Sites that connect over the published loopback port have their .env regenerated automatically when the port moves: host-proxy sites, and every site under the native runtime, whose PHP runs on the host and reaches services the same way. Their env names 127.0.0.1 rather than the service, so lerd finds them through the services lerd env last wired into them, which it records in the site registry; a site last wired by an older lerd is found once lerd env has run in it again. A site that is paused when the port moves is skipped at that moment and picks up the new port when it is next unpaused.

Some services publish more than one host port: Mailpit exposes SMTP on 1025 and its web UI on 8025, RustFS the S3 API on 9000 and the console on 9001, Selenium the WebDriver on 4444 and the noVNC view on 7900. lerd service port <name> <port> moves the primary (first) mapping. To move any other published port, name the mapping by its container-internal port with --container:

bash
# Move Mailpit's web UI off 8025 to 8026 (SMTP on 1025 is untouched)
lerd service port mailpit 8026 --container 8025

# Put it back
lerd service port mailpit --reset --container 8025

The dashboard link for a service always follows the port its dashboard is served on, so moving Mailpit's UI port re-points the dashboard and the "open dashboard" iframe automatically.

The Mailpit, Meilisearch and RustFS views are served through the dashboard's own origin rather than framed from their ports, which is what lets them wear lerd's theme: it follows the light and dark you picked, on the surfaces of the palette you picked, and Mailpit's header takes the raised surface instead of Bootstrap's brand blue. Meilisearch's mini-dashboard has no dark mode of its own and paints from a fixed palette, so its colours are mapped onto the theme's as they are read: its page lands on the dashboard's page colour, its panels on the card colour and its pink on your accent. The RustFS console has a light and a dark of its own, so it is handed the mode to open in and its surfaces take the theme's colours. Their own addresses keep working exactly as before, so http://localhost:8025, http://localhost:7700 and http://localhost:9001 and anything pointing at them are untouched.

The chosen ports are persisted in ~/.config/lerd/config.yaml and reapplied on every start: the primary under services.<name>.published_port, any other mapping under services.<name>.published_ports keyed by container port. Once a port is set, automatically or with lerd service port, it sticks: lerd never moves it again on its own, not even back to the default when that frees up later. Change it only with lerd service port.

Removing a service leaves its recorded ports in config.yaml, so reinstalling it later lands back on the same ports rather than picking new ones. While it is gone those ports are free again: another service can be moved onto one, and the automatic shift can hand one out. If that happens, the reinstall passes through the ownership guard like a fresh install and takes a free port instead of publishing on top of whoever holds it now.

Every published port can also be moved from the dashboard: a service's Ports tab lists one editable host-port field per published port (primary and secondary alike), each with a reset-to-default. The TUI shows the current published and extra ports read-only; editing stays in the CLI, dashboard and MCP.

Known limitation

The shift is decided at quadlet-write time, from whether the port can be bound right then. A host server that is installed but stopped at that moment leaves its port looking free, so lerd may take it and clash when that server next starts (for example at boot). This is the deliberate trade for not inspecting the host: a host database is usually running, and the failure is loud. Recover by moving lerd onto a free port with lerd service port <name> <port>.

Which services the Overview shows ​

The service cards fill as many columns as the Overview pane has room for, not the window: two until the pane is 48rem wide, three until 64rem, four beyond. A tiled window with the site list open keeps two wide cards rather than three or four narrow ones.

The site Overview treats a service as in use when the project lists it in .lerd.yaml, when the env file names its container (lerd-redis), or when the site registry records that lerd env last wired it. Native sites and host-proxy sites reach services at 127.0.0.1, so their env files name the loopback address; the registry record is what keeps those services on the Overview and out of the suggestion row. Container sites are read from the same record.

A recorded service stays listed while its quadlet is installed, whether the container is running or stopped, and a custom service is included the same way. A service that is only recorded, not declared, has a declare button on its card rather than a remove button: declare it first, then remove it. Removing a service from the Overview is remembered, so the registry entry left behind does not put it back, and a service whose quadlet is gone leaves no badge behind. A site last wired by an older lerd has no record yet: run lerd env in the project once and the Overview lists the services that command wired.


Using a service you run on the host ​

Moving lerd's published port lets a host-installed server keep its own. The step past that is pointing a project at your server instead of lerd's container: the database you already have, with your data and your users, while lerd keeps managing everything else.

It takes two things, both in the project's personal, gitignored .env.lerd_override: the connection values that point at your server, and the reserved LERD_EXTERNAL_SERVICES key that tells lerd to stay out of the way.

dotenv
# .env.lerd_override: this project uses the MySQL installed on the machine
DB_HOST=host.containers.internal
DB_PORT=3306
DB_DATABASE=myapp
DB_USERNAME=myapp
DB_PASSWORD=secret

LERD_EXTERNAL_SERVICES=mysql

Run lerd env and those values are written into .env, last, over anything lerd computed. For a service named in LERD_EXTERNAL_SERVICES lerd still writes the connection variables your framework reads, but it does not start the container and does not create the project database or S3 bucket. The key is comma or space separated, so LERD_EXTERNAL_SERVICES=mysql, redis opts both out, and it is consumed by lerd rather than written into .env. The output names what it skipped:

Updating existing .env...
  Detected mysql        — applying lerd connection values
   mysql externally managed (.env.lerd_override) — not starting it
  Applying 5 override(s) from .env.lerd_override
Done.

Opting out does not stop a lerd service that is already running. Leave it (the two coexist once their ports differ, see above) or shut it down with lerd service stop mysql.

Use host.containers.internal as the host, never 127.0.0.1 or localhost. The app runs inside the PHP-FPM container, where loopback is the container itself; host.containers.internal is an entry lerd maintains that points at an address it has probed and found routable back to your machine. lerd doctor prints the one in force under Container → Host connectivity.

A site on the native runtime is the exception: its PHP runs on your machine, not in a container, so it reaches your server at 127.0.0.1 like any other local program, and host.containers.internal does not resolve there.

What the host server has to allow ​

A container is not on your machine's loopback, and what that costs you depends on the platform.

macOS. gvproxy maps host.containers.internal to 192.168.127.254 and hands the connection to the host's loopback, so a server listening on 127.0.0.1 with 'user'@'localhost' grants accepts it with nothing changed.

Linux. The connection arrives from a real, non-loopback address, so a distro package left at its defaults refuses it. On Ubuntu, mysql-server ships bind-address = 127.0.0.1 in /etc/mysql/mysql.conf.d/mysqld.cnf and grants only for localhost, which is exactly the combination that produces a refused connection from a lerd site. Three things need attention:

  1. Listen past loopback. MySQL and MariaDB: set bind-address = 0.0.0.0 and restart the server. PostgreSQL: listen_addresses = '*' in postgresql.conf. Redis: comment out bind 127.0.0.1 or add the address the container reaches.
  2. Grant from somewhere other than localhost. CREATE USER 'myapp'@'%' rather than 'myapp'@'localhost'; PostgreSQL needs a matching host line in pg_hba.conf. The address the server actually sees is your machine's own address on one of its interfaces, and which one it is depends on the podman network setup, so % is the practical choice on a development machine. If you would rather pin it, make one failed attempt and read it back out of the rejection: MySQL answers Access denied for user 'myapp'@'192.168.122.139', and that address is the one to grant.
  3. Let it through the firewall. ufw and firewalld both drop the port by default once they are enabled.

Binding wider than loopback

bind-address = 0.0.0.0 exposes the server to every network the machine is on, not just to containers. On a laptop that joins untrusted networks, keep a firewall rule that allows the port only from the podman subnet, or bind to that bridge address specifically instead of to everything.

What still points at lerd's container ​

The lerd db:* commands resolve their target from the service, not from DB_HOST, so db:shell, db:import, db:export and the snapshot commands keep talking to lerd-mysql even while your site reads and writes your own server. Use your own mysql or psql client for a host-run database. Everything else, the site's .env, migrations, queue workers, and the app itself, goes to the host server.


Service credentials ​

Two sets of hostnames

Services run as Podman containers on the lerd network. Two hostnames apply depending on where you're connecting from:

  • From host tools (e.g. TablePlus, Redis CLI): use 127.0.0.1
  • From your Laravel app (PHP-FPM runs inside the lerd network): use container hostnames (e.g. lerd-mysql)

lerd service start <name> prints the correct .env variables to paste into your project.

ServiceDefault versionHost (host tools)Host (Laravel .env)PortUserPasswordDB
MySQL8.4 LTS (mysql:8.4)127.0.0.1lerd-mysql3306rootlerdlerd
PostgreSQL16 + PostGIS 3.5127.0.0.1lerd-postgres5432postgreslerdlerd
Redis7-alpine127.0.0.1lerd-redis6379---
Meilisearchv1.42127.0.0.1lerd-meilisearch7700--per-site index prefix
RustFSlatest127.0.0.1lerd-rustfs9000lerdlerdpasswordper-site bucket
Mailpit SMTPlatest127.0.0.1lerd-mailpit1025---

Additional UIs:

  • RustFS console: http://127.0.0.1:9001
  • Mailpit web UI: http://127.0.0.1:8025

Mailpit notifications ​

Captured emails can pop a notification with the subject and sender; clicking the notification opens the captured message in the Mailpit overlay. This is one of several notification kinds the dashboard supports, see Notifications for the full list (worker failures, finished service operations, service updates, dumps) and how to configure them under System → Notifications.

Spam scoring ​

Mailpit can report an Apache SpamAssassin score for every message it catches, once there is a SpamAssassin to ask. Install the preset and the catcher picks it up:

bash
lerd service install spamassassin

The scoring endpoint is wired by discovery, not by configuration: mailpit is regenerated to point at the new service when it starts and back to no scoring at all when it is removed, so there is nothing to undo. The score shows on each message in the Mailpit UI.

Which template built a message ​

While the debug window is capturing, lerd stamps outgoing Laravel mail with an X-Lerd-View header naming the Blade templates the message was rendered from, and the debug window's Mail lens lists them under the recipients. The header travels with the message, so it is also there in Mailpit's own Headers tab. Mail sent from a queue worker carries it only when worker capture is on, since a worker otherwise reports nothing but the jobs it runs.

Meilisearch, per-site indexes ​

Every site shares the one Meilisearch, so lerd gives each site its own index prefix, named like its database: SCOUT_PREFIX=<site>_ on Laravel, MEILISEARCH_PREFIX=<site>_ on Symfony with the Meilisearch bundle. Two projects that both index a users model then write to app_one_users and app_two_users rather than to one index, and switching between them needs no reimport. A site whose indexes were built before the prefix was set finds them empty once, so import them again: on Laravel run php artisan scout:import 'App\Models\User' once per searchable model, swapping in each model's class, and on Symfony bin/console meilisearch:import.

RustFS, per-site buckets ​

Mail sent through PHP's own mail() reaches Mailpit too, without any project configuration. The FPM image's sendmail is BusyBox's, which talks to 127.0.0.1:25 and finds nothing listening inside the container, so lerd writes a sendmail_path pointing at the mail catcher it runs and mounts it into every PHP container. That covers the frameworks that send through mail() rather than SMTP, Drupal and WordPress among them, which would otherwise report that mail could not be sent with nothing to show for it. A sendmail_path you set yourself in the shared or per-version php.ini wins, since lerd's file loads before both.

RustFS is an S3-compatible object storage service (a drop-in replacement for MinIO). A site's bucket is ensured at every point the site and the service are brought together: lerd link when .lerd.yaml lists rustfs, lerd env when it detects rustfs is needed (via FILESYSTEM_DISK=s3 or AWS_ENDPOINT in .env), and any install or reinstall of the service itself, which walks every already linked site. The bucket is looked up before it is created, so the repeats cost a lookup. In each case lerd:

  1. Creates a bucket named after the site handle, sanitised to match the S3 naming rules (lowercase, digits, hyphens, dots only, max 63 chars). Underscores in the handle are rewritten as hyphens, so admin_astrolov becomes bucket admin-astrolov.
  2. Sets the bucket to public access (suitable for local development)
  3. Writes the correct .env values:
ini
FILESYSTEM_DISK=s3
AWS_ACCESS_KEY_ID=lerd
AWS_SECRET_ACCESS_KEY=lerdpassword
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=my-project
AWS_URL=http://localhost:9000/my-project
AWS_ENDPOINT=http://lerd-rustfs:9000
AWS_USE_PATH_STYLE_ENDPOINT=true

If a historical AWS_BUCKET value with underscores (or other S3-invalid characters) is present from an earlier lerd run or Sail import, lerd env will sanitise it in place on the next run.

AWS_URL points to the public bucket URL (browser-reachable). AWS_ENDPOINT is the internal container address used by PHP.

Serving a service on its own domain ​

An app reaches a service by container name, lerd-rustfs, which resolves only inside the podman network. That is fine until something hands the browser a URL built from it. An S3 presigned URL carries its host inside the signature, so rewriting the host afterwards produces SignatureDoesNotMatch; the app and the browser have to agree on one name before the URL is signed. AWS_URL cannot solve it either, because stock Laravel signs against the endpoint, not the public base.

RustFS ships with one: its preset declares domain: rustfs, so an install that has it takes rustfs.test on the next start and the sites using it are repointed in the same pass. That is deliberate rather than opt-in, because a service whose URLs reach a browser is broken without a name both sides resolve, and a fix nobody runs is not a fix.

bash
lerd service domain rustfs storage.test  # choose a different name
lerd service domain rustfs               # show the current domain
lerd service domain rustfs --remove      # stop serving it there

Removing it is recorded, so the preset's default is not handed back on the next start; naming one by hand clears that again. Any preset can declare a default the same way, which is why the value lives in the store and reaches every install without a release.

The domain is a name both sides resolve: nginx serves it over HTTPS with a certificate from lerd's own CA, and the hosts file mounted into every PHP container points it at nginx, so the app signs for the same host the browser opens. A bare name is qualified with lerd's TLD, so storage means storage.test, and a domain outside that TLD is refused because nothing else resolves on both sides. The service stays reachable at lerd-<name> on the podman network throughout.

A service exposing more than one port declares which one its domain serves, so Mailpit's mailpit.test would land on its web UI rather than on the SMTP port that happens to be listed first. Override it with --port when the preset says nothing:

bash
lerd service domain rustfs console.rustfs.test --port 9001

A port the service does not expose is refused rather than written into a vhost that answers nothing.

A subdomain of a site works just as well if you prefer the storage to sit under the app it belongs to, lerd service domain rustfs rustfs.myapp.test. lerd runs one RustFS for every site, so the default is a name of its own rather than one borrowed from a project, and per-site subdomains share a namespace with grouped sites and worktree domains; but nothing stops you naming it that way.

The domain is taken during lerd install as well as at start, so an update reaches a machine that never sees a lerd start between releases.

Setting or removing a domain sweeps the sites that use the service and rewrites their env files right away, so the change reaches the projects instead of waiting for each to run lerd env itself. That matters most on removal: without the sweep every project would be left pointing at a name that no longer resolves.

Once a service has a domain, lerd env writes it into the sites that use the service: the container URL and the localhost published-port URL both become the domain, and the port the scheme now implies is dropped. Values that are not URLs are left alone, so a bare REDIS_HOST=lerd-redis keeps pointing at the container. A container URL only moves when it names the port the domain serves, or no port, so a service listening on a second port, like a mock server beside its admin API, keeps that one on the container.

ini
AWS_ENDPOINT=https://rustfs.test
AWS_URL=https://rustfs.test/my-project

Uploading to the service from the browser ​

A presigned upload is issued by the app but sent by the browser, and it goes to a different origin than the page it was issued to. The browser asks the service for permission first, and sends nothing at all unless the answer names that page's origin. Server-side uploads and plain <img> or download links never ask, so they were never affected.

RustFS's preset asks for the preflight to be answered, so an install that has its domain gets it. For a service whose preset says nothing, or one of your own:

bash
lerd service domain rustfs --cors      # answer preflights on the domain
lerd service domain rustfs --no-cors   # stop answering them

The origin is reflected rather than answered with *: the vhost matches the request's Origin against the hosts lerd serves and echoes it back, so https://myapp.test is answered and a page on the open internet is not. A wildcard would let any site the browser happens to have open read the service over JS. An origin lerd does not serve leaves the header off entirely, which the browser refuses exactly as it would with nothing configured.

Whatever the service says about CORS itself is dropped before lerd's answer is added. An object store that sends its own Access-Control-Allow-Origin would otherwise leave two on the response, and a browser rejects that outright rather than picking one. If you would rather configure CORS on the bucket, turn lerd's off with --no-cors and the service's own headers pass through untouched.

The preflight answer reflects the headers the browser asked for rather than replying *, which Safari does not accept, and ETag is exposed because a multipart upload confirms each part by the ETag it comes back with.

If a bucket a site points at is not there, lerd site:doctor reports it under Bucket with a fix that creates it. Such a site serves every page fine and fails on the first upload, which is exactly the kind of thing that reads as an application bug. The check is driven by the service preset rather than by any framework: the preset names the .env key holding the entity a site owns (owner_env: AWS_BUCKET for RustFS), and only a project whose env also points at the lerd service is measured against it, so one configured for real AWS is left alone.

lerd env writes the .env even when it could not create the database or bucket behind it, and then exits non-zero naming what is missing. It used to warn in passing and exit 0, which left a file claiming storage that was never created.

Migrating from MinIO to RustFS ​

RustFS exposes the same S3 API as MinIO with the same default credentials, no application changes are needed after migration.

Automatic prompt during lerd update

If lerd detects an existing MinIO data directory (~/.local/share/lerd/data/minio) during lerd update, it will offer to migrate automatically:

==> MinIO detected, migrate to RustFS? [y/N]

Answering y runs the full migration in-place. The update continues regardless of your answer.

Manual migration

bash
lerd minio:migrate

This command:

  1. Stops the lerd-minio container (if running)
  2. Removes the MinIO quadlet so it no longer auto-starts
  3. Copies ~/.local/share/lerd/data/minio/ to ~/.local/share/lerd/data/rustfs/
  4. Updates ~/.config/lerd/config.yaml: removes the minio entry and adds rustfs
  5. Installs and starts the lerd-rustfs service

The original MinIO data directory is not deleted. Verify the migration works, then remove it manually:

bash
rm -rf ~/.local/share/lerd/data/minio

More ​

  • Service updates: the Update / Upgrade / Migrate / Rollback flow, update_strategy / track_latest / allow_major_upgrade configuration, and recovery from failed migrations.
  • Service presets: one-command installers for phpMyAdmin, pgAdmin, MongoDB, alternate MySQL / MariaDB versions, Selenium, and Stripe Mock.
  • Custom services: YAML schema for your own OCI-based services, with env injection, placeholders, dependencies, and worked examples (Soketi, Stripe).

Released under the MIT License.