Shared local development infrastructure for all your projects, as Docker
Compose services gated behind profiles. Start only what you need; every
service joins one shared network (lds-network) so your application containers can
talk to them.
| Group | Profile | Service(s) | Host port(s) |
|---|---|---|---|
| MySQL | `mysql` | `mysql` (8.4, binlog ON for CDC) | 4400 |
| MariaDB | `mariadb` | `mariadb` (11.8, utf8mb4 defaults — also the ERPNext database) | 4406 |
| SQL Server | `mssql` | SQL Server 2025 Developer (free for dev) | 4407 |
| Oracle | `oracle` | Oracle Database Free 23ai (via the `gvenzl` mirror, no account needed) | 4408 (+4409 EM Express) |
| PostgreSQL | `postgres` | `postgres` (16, `wal_level=logical` for CDC) | 4401 |
| MongoDB | `mongo` | `mongo` (7, single-node replica set `rs0`, CDC-ready) | 4402 |
| Redis | `redis` | `redis` (7) | 4403 |
| Valkey | `valkey` | `valkey` (Redis-compatible) | 4405 |
| Memcached | `memcached` | `memcached` (1.6) | 4404 |
| Proxy/DNS | `proxy` | `proxy` (nginx-proxy edge router), `dns` (dnsmasq) | 80 (web), 53 (dns) |
| Web (PHP) | `php` | `php` — one container, supervisord runs php-fpm + nginx (multi-project) | via proxy |
| Apps | _templates_ | Go / Rust / Java / Node containers (own compose) | via proxy |
| Kafka | `kafka` | `kafka-controller`, `kafka-broker`, `schema-registry` (Apicurio), `connect-debezium`, `connect-generic`, `kafka-ui` | 4420–4424 |
| Realtime | `soketi` / `centrifugo` / `mqtt` | WebSocket / MQTT pub-sub brokers — **off by default**, stateless | 4440 / 4441 / 4442–4444 |
| Admin UIs | `phpcacheadmin` / `dbgate` | cache browser / web DB client | 4500 / 4501 |
| DB design | `drawdb` | DrawDB — ER diagram designer (open at `localhost:4502`) | 4502 |
| Warehouse/BI | `hop` / `superset` | Apache Hop (ETL designer) / Apache Superset (BI) | 4503 / 4504 |
| Query engines | `duckdb` / `trino` | DuckDB (embedded OLAP, CLI) / Trino (distributed SQL + UI) | — / 4451 |
| Code quality | `semgrep` | Semgrep SARIF viewer (`lds tools semgrep` runs the scan) | 4505 |
| Vulnerability scanning | `zap` | OWASP ZAP — DAST web app scanner (UI at `zap.test`, proxy/API :4512) | 4510, 4512 |
| `trivy` | Trivy CVE scanner — containers, filesystems, deps (`lds tools trivy` runs the scan) | 4511 | |
| Code intelligence | `crg` | code-review-graph — AI code-intelligence graph viewer at `crg.test` (`lds tools crg` runs the scan) | 4530 |
| Security/Auth | `vaultwarden` | Vaultwarden password manager (Bitwarden-compatible) | 4506 |
| `mail` | Mailpit — local SMTP sink + web inbox | 4513 (UI) / 4514 (SMTP) | |
| Design | `penpot` | Penpot — collaborative design tool (reuses shared `postgres` + `valkey`) | 4518 |
| Websites/CMS | `instatic` | Instatic — self-hosted visual CMS / website builder (admin at `/admin`) | 4528 |
| Web analytics | `analytics` | LDS Analytics — Nuxt/Vue dashboard + Hono API (reuses shared `postgres`) | 4520 / 4521 |
| Project management | `tasks` | LDS Tasks — Angular project management (reuses shared `postgres`) | 4522 / 4523 |
| Documentation | `wiki` | LDS Wiki — Next.js docs app (reuses shared `postgres`) | 4524 / 4525 |
| Communication | `openwa` | OpenWA — WhatsApp API server (reuses shared `postgres` + `redis`) | 4507 |
| File storage | `rustfs` | RustFS — self-hosted file sharing (API + console) | 4508 / 4509 |
| Browser automation | `headlessx` | HeadlessX — undetected scraping platform (web + API/MCP + sidecars, built from `data/headlessx`) | 4515–4519 |
| E2E testing | `playwright` | Playwright runner + HTML report viewer (`lds playwright` / `lds e2e`) | 4526 (reports) / 4527 (UI Mode) |
| ERP | `erpnext` | ERPNext on Frappe — Accounting, CRM, HR, … (DB on shared `postgres` or `mariadb`); **heavy** | 4529 |
Realtime brokers are three independent choices for WebSocket pub/sub, each speaking a different client protocol (so pick the one matching your app): Soketi (Pusher protocol — drop-in for Laravel Reverb broadcasting + Laravel Echo / pusher-js), Centrifugo (raw WebSocket channels + admin UI, Centrifuge JS SDK), MQTT (Mosquitto + MQTTX) (MQTT + MQTT-over-WebSocket via Mosquitto with a browser client at
mqtt.test; clients use MQTT.js / Paho). One broker serves unlimited channels/topics — you never run a second one per channel. All three are stateless (no data volume) and mem/cpu-capped. Start one withlds up soketi/centrifugo/mqtt.
PHP extensions: rdkafka, redis, memcached, pdo_mysql, pdo_pgsql, opcache, intl, bcmath, gd, zip, sockets + composer.
Everything runs through the single lds wrapper (./lds.sh <cmd> or
lds.bat <cmd> on Windows cmd):
cp .env.example .env # then edit if needed
./lds.sh init # one-time: create the shared lds-network network
./lds.sh build-bases # one-time: build the lds/* base images
# Start the groups you need (any combination of profiles):
./lds.sh up # the default run-set (LDS_ENABLE_* toggles in .env)
./lds.sh up mysql postgres redis memcached
./lds.sh up php # (auto-builds the lds/php base if missing)
./lds.sh up kafka
./lds.sh up analytics
./lds.sh up vaultwarden
./lds.sh up wiki
./lds.sh up mqtt # a realtime broker (soketi | centrifugo | mqtt)
./lds.sh up all # or everything at once
./lds.sh down # stop (add -v to wipe data)
./lds.sh help # all commandsDefault run-set:
lds upwith no arguments starts every profile whoseLDS_ENABLE_<PROFILE>toggle in.envistrue(defaults:proxy,php,mysql,dbgateon → edge proxy, DNS, PHP, MySQL, DBGate). One independent on/off switch per service — flip a single line (e.g.LDS_ENABLE_KAFKA=true) to add/remove a group. Passing explicit profiles (lds up kafka) ignores the toggles and starts only those.
ldsjust dispatches to the scripts inscripts/. Every script also exists standalone in two forms:.sh(bash / Git Bash / WSL / Linux) and.bat(Windowscmd) — e.g.scripts/run/up.sh/up.bat.
The PHP extension set, each language's dev tooling (air, cargo-watch, Maven,
…), and a shared nginx runtime base are built once into lds/* images, then
reused by the stack and every template. Build/refresh them with ./lds.sh build-bases (--force to
rebuild, --push to push to $REGISTRY). Sources live in base-images/; the
build is orchestrated by docker-bake.hcl (docker buildx bake), so all
seven images build in parallel from one declarative definition.
Raw Compose equivalents:
docker compose --profile mysql --profile redis up -d
docker compose --profile kafka up -d
docker compose --profile '*' down- Control panel / dashboard: http://localhost
- Your projects: http://<folder>.test (e.g. http://example.test)
All host ports live in the 44xx–45xx block (set via *_HOST_PORT in .env).
From other containers on lds-network, use the service name + its internal port
(right column) instead. Full reference: docs/en/12-ports.md.
| Group | Service | Host + Port | From Container + Port |
|---|---|---|---|
| **Databases** `440x` | -------------------------------------------------------------------------------- | ||
| MySQL | `localhost:4400` | `mysql:3306` | |
| MariaDB | `localhost:4406` | `mariadb:3306` | |
| SQL Server 2025 | `localhost:4407` | `mssql:1433` | |
| Oracle 23ai | `localhost:4408` (EM: `localhost:4409/em`) | `oracle:1521` | |
| PostgreSQL | `localhost:4401` | `postgres:5432` | |
| MongoDB | `localhost:4402` | `mongo:27017` | |
| Redis | `localhost:4403` | `redis:6379` | |
| Valkey | `localhost:4405` | `valkey:6379` | |
| Memcached | `localhost:4404` | `memcached:11211` | |
| **Kafka** `442x` | -------------------------------------------------------------------------------- | ||
| Broker (bootstrap) | `localhost:4420` | `kafka-broker:9092` | |
| Schema Registry | `localhost:4421` | `schema-registry:8080` | |
| Connect — generic | `localhost:4422` | `connect-generic:8083` | |
| Connect — Debezium | `localhost:4423` | `connect-debezium:8083` | |
| Kafka UI | `localhost:4424` | `kafka-ui:8080` | |
| **Realtime** `444x` | -------------------------------------------------------------------------------- | ||
| Soketi (Pusher) | `localhost:4440` (`ws.test`) | `soketi:6001` | |
| Centrifugo + UI | `localhost:4441` (`centrifugo.test`) | `centrifugo:8000` | |
| Mosquitto — MQTT | `localhost:4442` | `mosquitto:1883` | |
| Mosquitto — MQTT/WS | `localhost:4443` (path `/`) | `mosquitto:9001` | |
| MQTTX web client | `localhost:4444` (`mqtt.test`) | `mqttx:80` | |
| **Query engines** | -------------------------------------------------------------------------------- | ||
| DuckDB | `n/a` (file engine, exec into `lds-duckdb`) | — | |
| Trino | `localhost:4451` (`/ui`) | `trino:8080` | |
| **Web UIs / tools** `450x+` | ------------------------------------------------------------------------------- | ||
| phpCacheAdmin | `localhost:4500` (`cache.test`) | `phpcacheadmin:80` | |
| DBGate | `localhost:4501` (`db.test`) | `dbgate:3000` | |
| DrawDB | `localhost:4502` (**not** `drawdb.test`) | `drawdb:80` | |
| Apache Hop | `localhost:4503` (`hop.test`) | `hop:8080` | |
| Apache Superset | `localhost:4504` (`superset.test`) | `superset:8088` | |
| Semgrep viewer | `localhost:4505` (`semgrep.test`) | `semgrep:8080` | |
| Vaultwarden | `localhost:4506` (`vaultwarden.test`) | `vaultwarden:80` | |
| OpenWA | `localhost:4507` (`openwa.test`) | `openwa:2785` | |
| RustFS — API | `localhost:4508` | `rustfs:9000` | |
| RustFS — Console | `localhost:4509` (`rustfs.test`) | `rustfs:9001` | |
| OWASP ZAP — UI | `localhost:4510` (`zap.test`) | `zap:8080` | |
| Trivy viewer | `localhost:4511` (`trivy.test`) | `trivy:8080` | |
| OWASP ZAP — proxy/API | `localhost:4512` | `zap:8090` | |
| Mailpit — inbox | `localhost:4513` (`mail.test`) | `mailpit:8025` | |
| Mailpit — SMTP | `localhost:4514` | `mailpit:1025` | |
| HeadlessX — web | `localhost:4515` (`headlessx.test`) | `headlessx-web:3000` | |
| HeadlessX — API/MCP | `localhost:4516` (`headlessx-api.test`) | `headlessx-api:8000` | |
| HeadlessX — HTML→MD | `localhost:4517` | `headlessx-html-to-md:8080` | |
| Penpot | `localhost:4518` (`penpot.test`) | `penpot-frontend:8080` | |
| HeadlessX — YT engine | `localhost:4519` | `headlessx-yt-engine:8090` | |
| Playwright — reports | `localhost:4526` (`playwright.test`) | `playwright-report:8080` | |
| Playwright — UI Mode | `localhost:4527` (via `lds playwright ui `) | `playwright:8787` | |
| Instatic | `localhost:4528` (`instatic.test`, admin at `/admin`) | `instatic:3001` | |
| ERPNext | `localhost:4529` (`erpnext.test`) | `erpnext-frontend:8080` | |
| code-review-graph viewer | `localhost:4530` (`crg.test`) | `crg:8080` | |
| **LDS apps** `452x` | -------------------------------------------------------------------------------- | ||
| Analytics — API/UI | `localhost:4520` / `localhost:4521` (`analytics.test`) | `analytics-api:3001` / `analytics-ui:4173` | |
| Tasks — API/UI | `localhost:4522` / `localhost:4523` (`tasks.test`) | `tasks-api:3002` / `tasks-ui:4174` | |
| Wiki — API/UI | `localhost:4524` / `localhost:4525` (`wiki.test`) | `wiki-api:3003` / `wiki-ui:4175` | |
| **Infra** | -------------------------------------------------------------------------------- | ||
| Web proxy | `localhost:80` (`*.test`) | — | |
| Web proxy (HTTPS) | `localhost:443` (`*.test`, opt-in) | — | |
| DNS | `localhost:53` (udp + tcp) | — |
The php profile runs one container (supervisord → php-fpm + nginx) doing
mass virtual hosting, plus a dnsmasq DNS server. Every folder under www/
is served automatically:
www/
example/public/index.php -> http://example.test
myshop/public/index.php -> http://myshop.test
legacy-site/index.php -> http://legacy-site.test
Docroot per project is auto-detected: public/ → htdocs/ → folder root.
No per-project config, no rebuild — drop a folder and refresh.
One-time DNS setup (so *.test resolves): point your Windows network
adapter's DNS server to 127.0.0.1. The dns container then answers *.test
with 127.0.0.1 and forwards all other lookups upstream (8.8.8.8/1.1.1.1), so
normal internet DNS keeps working while the stack is up.
Prefer not to change your system DNS? Run
scripts/run/hosts-sync.batin an admin command prompt (orhosts-sync.shwith sudo) — it writes each project into your hosts file. (Re-run it whenever you add a project.)
Mount a different folder by setting PHP_PROJECTS_PATH in .env (e.g. point it
at D:/projects/PHP to serve all your existing projects at once). It defaults to
./www, the example folder shipped with LDS.
PHP shares one runtime, so folders are enough. Compiled/runtime apps each run
as their own container and get a .test hostname through the nginx-proxy
edge router. Any container that sets VIRTUAL_HOST + VIRTUAL_PORT and joins
lds-network is routed automatically:
environment:
VIRTUAL_HOST: orders.test
VIRTUAL_PORT: "8080"
networks: [lds-network]
networks:
lds-network:
external: trueReady-to-run starters live in templates/, named by role (svc- = API,
web- = UI app) and by native-vs-framework. Native (language's own web tech):
go (net/http), node (http module), python (http.server), java (Servlet),
plus rust (axum). Frameworks (separate templates): Java — springboot,
micronaut, quarkus, vaadin; Node — express; Python — flask, fastapi,
django; PHP — laravel, symfony, slim, webman, codeigniter,
cakephp; SPA — angular, react. Most ship as a svc-+web- pair. Bring up the router once with
./scripts/run/up.sh proxy, then docker compose up -d in a template. See
templates/README.md.
Fastest way — the lds new scaffolder (the cross-language equivalent of
dropping a PHP folder):
lds new php myblog # plain PHP under PHP_PROJECTS_PATH -> http://myblog.test
lds new go orders # web template by default
lds new svc-python rates # the svc-template-python API
lds new web-laravel shop shop.test # framework + custom hostIt copies the matching templates/<role>-template-<tech> into that technology's
*_PROJECTS_PATH (set in .env — GO_PROJECTS_PATH, RUST_PROJECTS_PATH,
NODE_PROJECTS_PATH, PYTHON_PROJECTS_PATH, JAVA_PROJECTS_PATH; PHP uses
PHP_PROJECTS_PATH), and rewrites the template's name/container_name/host to
your project name.
Then manage it with lds app <command> — start ensures the
proxy/dns/network are up and runs docker compose up --build -d for you:
cd ../../Go/orders
lds app start # build & start (+ proxy) | lds app start ../../Go/orders
lds app logs # tail logs
lds app restart # rebuild & recreate
lds app stop # stop (add -v to wipe its volumes)(Plain PHP projects need none of this — they're served immediately by the shared
php container.)
Under the hood it's just copy-rename-run — you never edit local-dev-stack's
own files, and you can still do it by hand:
cp -r templates/svc-template-go D:/projects/Golang/orders # 1. copy (anywhere)
# 2. set APP_HOST=orders.test in the project's .env (or its compose file)
cd D:/projects/Golang/orders && docker compose up -d # 3. -> http://orders.test- No
public//index page needed for a template — that requirement is only for plain PHP dropped intowww/(the shared mass-vhost). A template runs its own server; it just listens on itsVIRTUAL_PORT. - No registration in LDS — nothing is added to
local-dev-stack/docker-compose.yml. New hostnames work automatically:dnsresolves*.testby wildcard, andproxyauto-discovers any container that setsVIRTUAL_HOST. - Standing requirements:
./scripts/run/init.shonce (createlds-network) + point your Windows adapter DNS at127.0.0.1; keep./scripts/run/up.sh proxyrunning. Templates already joinlds-networkand setVIRTUAL_HOST.
Not changing system DNS? Use
hosts-sync.bat(admin) /hosts-sync.shinstead — but re-run it per new project (the hosts file has no wildcard).
lds-network is an independent external network — create it once with
./scripts/run/init.sh (or docker network create lds-network). After that, the
stack and every project attach to it as equals; nothing owns its lifecycle, so
order of startup/shutdown never matters. (up.sh also auto-creates it.)
Point your project's compose file at it:
networks:
lds-network:
external: trueThen your app reaches services by name (in-network ports): mysql, postgres,
redis, memcached, kafka-broker:9092, schema-registry:8080,
connect-debezium:8083, connect-generic:8083. From the host, Connect is on
:4423 (Debezium) and :4422 (generic), and the registry on :4421.
The connect-debezium service is the Debezium Connect image with MySQL and
Postgres connectors built in. MySQL runs with binlog (ROW + GTID); Postgres runs
with wal_level=logical — both ready for change data capture. A second worker,
connect-generic (vanilla apache/kafka image), is there for non-Debezium
connectors — drop plugin JARs into configs/kafka/connect-generic/plugins/.
Register the example connectors once Kafka + a DB are up:
./scripts/run/register-connectors.sh # all
./scripts/run/register-connectors.sh mysql # just MySQLEdit the configs in configs/kafka/connect/*.json to match your databases.
Prefer a UI? The control panel (http://localhost/) has a connector builder
at http://localhost/connectors.php: pick a worker (Debezium or generic), pick a
plugin, and it renders a guided form from that connector's own config schema —
with live validation — then creates it. Lists and deletes existing connectors too.
- All Kafka images are Apache-licensed: the broker/controller and generic
Connect worker are
apache/kafka, CDC is the (Apache-2.0) Debezium image, and the Schema Registry is Apicurio Registry (Apache 2.0) — no Confluent images. Avro values use Apicurio's Connect converter (ENABLE_APICURIO_CONVERTERSon the Debezium worker + per-connector converter config). Swap versions via.env(APICURIO_VERSION,DEBEZIUM_VERSION,KAFKA_VERSION,KAFKA_UI_IMAGE). - Generate a fresh KRaft cluster id:
docker run --rm apache/kafka:3.9.1 /opt/kafka/bin/kafka-storage.sh random-uuidand setKAFKA_CLUSTER_IDin.envbefore first start.
See docs/en (English) and docs/id (Bahasa Indonesia) for more detail.