Skip to main content

Local Development

What this covers / who it's for. The local dev environment: the Docker stack and its services/ports, the make-first workflow, MinIO-backed object storage, Xdebug, and the concurrency caveats that bite parallel sessions. Everything here runs from stables-core-app/ and everything executes inside Docker.

The make-first rule

Every common dev task has a Makefile target that wraps the right docker compose exec/run invocation — reach for make before raw docker/artisan/composer. make help lists all targets grouped by category; make status shows containers, URLs, and key config at a glance. When you discover a genuinely reusable command with no target, add one (with a ## help description) in the same change.

make setup # first time: .env from .env.example + APP_KEY + build + start
make up # start all services (detached); make up-fg for foreground logs
make down # stop (volumes preserved); make down-clean wipes volumes
make rebuild # --no-cache rebuild + recreate containers

.env is per-developer and gitignored; .env.example is the committed template with the Docker service hostnames already filled in. make setup creates it; manually: cp .env.example .env && make artisan ARGS="key:generate".

Services & ports

Host ports follow the 1XXXX scheme — prepend 1 to the standard port. Inside the Docker network, containers use the unchanged internal ports (the app reaches postgres on 5432).

ServiceHost URL / portPurpose
apphttp://localhost:18000Laravel web server (php artisan serve)
vitehttp://localhost:15173Frontend HMR
mailpithttp://localhost:18025 (UI), 11025 (SMTP)Mail catcher
postgreslocalhost:15432PostgreSQL 18
valkeylocalhost:16379Valkey (Redis-compatible) cache/queue
miniohttp://localhost:19001 (console), 19000 (S3 API)S3-compatible object storage
worker— (no HTTP)queue:work --tries=3 --timeout=90 — consumes the default queue only
scheduler— (no HTTP)schedule:work — drives all scheduled commands
minio-init— (one-shot)Creates the dev bucket then exits — Exited (0) is normal

Two runtime notes worth internalizing:

  • The local worker only consumes default. The specialized queues (webhooks, documents, audit, dlp, tasks) are consumed locally only by running Horizon (make artisan ARGS=horizon) — see Observability & ops for why a non-consumed queue is a silent failure.
  • MinIO backs the env-driven s3 disk (AWS_ENDPOINT=http://minio:9000, bucket stables-local — see .env.example). Helpers: make shell-s3 (an mc shell with the local alias preconfigured), make s3-reset (drop + recreate the bucket), make logs-s3. Integration tests that need the real S3 driver skip themselves when MinIO is unreachable.

Key targets by group

GroupTargets
Stacksetup up up-fg up-xdebug down down-clean restart(-app/-worker/-scheduler) ps status build rebuild
Logslogs logs-app logs-worker logs-scheduler logs-vite logs-db logs-cache logs-s3 pail
Shellsshell shell-worker shell-scheduler shell-db (psql) shell-cache (valkey-cli) shell-s3 (mc)
Artisan/Composerartisan ARGS="..." tinker routes composer ARGS="..." composer-install
DB & migrationsmigrate migrate-fresh migrate-rollback migrate-status seed test-db — the migrate targets also apply to the test DB and regenerate RLS policies
Tests & gatestest-parallel test test-filter FILTER=... test-coverage lint lint-check stan — see Testing & the gates
Queue & cachequeue-failed queue-retry queue-flush cache-clear config-clear
API artifactsopenapi-export abilities-export
Audit opsaudit-verify-chain audit-export-worm audit-verify-worm
Frontendnpm ARGS="..." npm-install build-assets

Xdebug

Installed in docker/Dockerfile.dev but off by default (XDEBUG_MODE=off). Enable per-session with make up-xdebug (or XDEBUG_MODE=debug,develop docker compose up). Each container dials back to a dedicated IDE port — configure three debug configurations:

ContainerIDE listen port
app9003
worker9004
scheduler9005

Concurrency & environment caveats

  • Shared test database. All sessions/worktrees share stables_testing; concurrent full-suite runs contend and produce failures that look like isolation bugs. Stagger full-suite runs across parallel sessions. (make test-parallel additionally provisions per-worker stables_testing_test_{token} databases — within one run, workers are isolated.)
  • Root-owned files. Files created inside a container are root-owned on the host — chown them before editing.
  • Secondary git worktrees. make may not resolve paths from a worktree — docker exec into the running app container directly (the repo is mounted at /app).
  • Production Docker files are out of bounds for local work: docker/production/ is used exclusively by the production CI/CD build.

← Engineering wiki index