Docker Development Environment

Local Docker Compose is the default KMP development workflow. The source tree stays on your machine and is bind-mounted into the app container, so PHP, template, CSS, and JavaScript source changes are visible without rebuilding the image.

Quick Start

./dev-up.sh --build

If app/config/.env does not exist yet, ./dev-up.sh creates it from app/config/.env.example. Before startup it also removes stale kmp-* containers and containers publishing the configured local dev ports, so the current worktree owns the active stack. The compose project is always named kmp, which keeps Docker Desktop and docker compose ps grouped under one local development stack even when startup is run from a worktree. After the app is healthy, it runs ./dev-reset-db.sh --seed by default so the database matches the current code and includes the full demo dataset. Set KMP_RESET_DB_ON_UP_ARGS= (empty) when you want a minimal schema-only reset, or KMP_RESET_DB_ON_UP=false to skip the reset entirely. After the first build, use ./dev-up.sh for normal startup. Stop the stack with ./dev-down.sh; add --volumes only when you intentionally want to delete the local database and pgAdmin volumes.

Architecture

Service Container Purpose Default host access
app kmp-app PHP 8.4, Apache, Composer, Node, Xdebug http://kmp.localhost:8080
scheduler kmp-scheduler Queue and scheduled-task loop using runtime database access Docker logs
admin-worker kmp-admin-worker Dedicated CLI worker for provisioning, migrations, backups, and restores Docker logs
db kmp-db PostgreSQL 16 127.0.0.1:5432
pgadmin kmp-pgadmin PostgreSQL administration UI for local development http://localhost:5050
mailpit kmp-mailpit Local email capture http://localhost:8025, SMTP 127.0.0.1:1025

All host ports are configurable in app/config/.env and bind to 127.0.0.1 by default.

Source and Dependency Workflow

The app source is mounted from ./app to /var/www/html. Run project commands inside the app container so the host machine does not need PHP, Composer, Node, or npm packages installed:

docker compose exec app bash
docker compose exec app composer install
docker compose exec app bin/cake migrations status
docker compose exec app npm install
docker compose exec app npm run build
docker compose exec app vendor/bin/phpunit

node_modules is stored in a Docker volume. This keeps dependencies out of the host checkout; use container commands for Vite, Jest, Playwright, and other npm-based tools.

The complete bash bin/verify.sh suite needs a full checkout with Git metadata and root deployment scripts, which the default app-only mount omits. Run it from app/ in a checkout with PHP 8.4, Git, Node, Composer and npm dependencies, and local test databases configured for that execution environment. Targeted tests can continue to run in the app container as shown above.

Local Host Aliases

The app container accepts any HTTP Host header. To test multiple local hostnames, add aliases to /etc/hosts:

127.0.0.1 kmp.localhost kmp2.localhost platform.kmp.localhost

Then list those aliases in app/config/.env so ./dev-up.sh prints them:

KMP_HOST_ALIASES="kmp.localhost kmp2.localhost platform.kmp.localhost"

With the default app port, the aliases are available at:

http://kmp.localhost:8080
http://kmp2.localhost:8080
http://platform.kmp.localhost:8080

If a feature generates absolute URLs, review the relevant CakePHP settings such as App.fullBaseUrl, cookie domain, CSRF, CORS, and callback URLs for the hostname being tested.

Xdebug

Xdebug is enabled in the app container and listens back to the host on port 9003.

Setting Default
XDEBUG_MODE debug,develop
XDEBUG_CLIENT_HOST host.docker.internal
XDEBUG_CLIENT_PORT 9003
PHP_IDE_SERVER_NAME KMP-Docker

The compose stack adds host.docker.internal:host-gateway, so Xdebug works on Linux as well as Docker Desktop. VS Code is configured with this path mapping:

"/var/www/html": "${workspaceFolder}/app"

The image uses xdebug.start_with_request=trigger. Start a debug session by listening on port 9003 in your IDE and setting an XDEBUG_TRIGGER cookie/query parameter, or by using an Xdebug browser extension.

Database Management

./dev-reset-db.sh
docker compose exec db psql -UKMPSQLDEV -d KMP_DEV
docker compose exec db psql -UKMPSQLDEV -d KMP_PLATFORM
docker compose logs db

pgAdmin is included as a development-only sidecar for browser-based PostgreSQL inspection:

URL:      http://localhost:5050
Login:    not required by default in local dev

The sidecar imports docker/pgadmin/servers.json on startup and preconfigures KMP Development (KMP_DEV) and KMP Platform (KMP_PLATFORM) under the KMP Local group. Both use host db, port 5432, username KMPSQLDEV, and the local POSTGRES_PASSWORD value. These are local development defaults only; do not use them for production.

The Compose sidecar sets KMP_PGADMIN_SERVER_MODE=False by default, which runs pgAdmin in desktop mode and skips the pgAdmin web login. Keep KMP_PGADMIN_BIND_ADDR=127.0.0.1 when using this mode. Set KMP_PGADMIN_SERVER_MODE=True plus KMP_PGADMIN_EMAIL and KMP_PGADMIN_PASSWORD if you intentionally need pgAdmin’s server-mode login.

The database volume is preserved across ./dev-down.sh. To start from an empty volume:

./dev-down.sh --volumes
./dev-up.sh

./dev-reset-db.sh --seed loads the PostgreSQL baseline seed from app/tests/pg_seed_baseline.sql, then runs the remaining migrations. This matches the production-style flow where a known historical database snapshot is restored before current migrations are applied. After migration, the reset moves business-effective dates forward from the snapshot’s February 16, 2026 reference date to the current UTC date. This keeps gatherings, warrant periods, warrants, offices, activity authorizations, memberships, and related deadlines current while preserving their relative spacing and leaving audit timestamps unchanged. Set KMP_DEV_SEED_AS_OF=YYYY-MM-DD to reproduce the seed calendar for a specific date. At the end of each reset, the script rebuilds KMP_DEV_test from the reset development schema so PHPUnit can run immediately.

Platform metadata uses a separate PostgreSQL database and migration track. Run schema changes and tenant provisioning in a disposable administrative CLI container. The admin-worker service supplies KMP_ADMIN_JOB and administrative database configuration; keep those settings out of the running app and scheduler. The PHP entrypoint runs only the requested command:

docker compose --env-file app/config/.env run --rm --no-deps --entrypoint php \
  admin-worker bin/cake.php platform_migrate migrate
docker compose --env-file app/config/.env run --rm --no-deps --entrypoint php \
  admin-worker bin/cake.php platform_migrate status
docker compose --env-file app/config/.env run --rm --no-deps --entrypoint php \
  admin-worker bin/cake.php tenant migrate --all --include-suspended

Use the same invocation for tenant provision <slug> and its desired options; inspect them with admin-worker bin/cake.php tenant provision --help after the same Compose arguments. The local stack must already be running because these commands use --no-deps.

./dev-reset-db.sh also recreates and migrates the local platform database, then registers local tenant metadata. By default it creates an active kmp tenant mapped to kmp.localhost, using KMP_DEV as the baseline tenant database, and provisions an active kmp2 tenant through bin/cake tenant provision at kmp2.localhost, using kmp2_dev as an isolated second-kingdom database. Seeded resets (./dev-reset-db.sh --seed) load the second database from the current baseline schema/data, then prune it to four standard demo users with @amp2demo.com login emails and no gatherings. Override KMP_DEV_TENANT_* in app/config/.env only when testing a different baseline tenant identity; future tenant creation should go through the platform tenant provisioning flow rather than new environment variables. The reset pauses running background workers, uses the isolated administrative CLI for provisioning and both tenants’ migrations, then restarts the workers that were running before the reset.

Set KMP_DEV_SECOND_TENANT_HOST to change the second tenant’s primary host, or set KMP_DEV_SECOND_TENANT_HOST_ALIASES to register additional HTTP/HTTPS edge hostnames while retaining kmp2.localhost for local tests. The reverse proxy must preserve the original Host header so KMP can select the correct tenant.

The reset also seeds a local Platform Admin account so dev logins are stable after every reset. The portal is disabled by default; set KMP_PLATFORM_ADMIN_PORTAL_ENABLED=true in app/config/.env only when testing platform administration:

URL:         http://platform.kmp.localhost:8080/platform-admin/login
Email:       admin@example.org
Password:    TestPassword
TOTP secret: QJR6QMYZYRHDZCOK5STD

Override KMP_DEV_PLATFORM_ADMIN_EMAIL, KMP_DEV_PLATFORM_ADMIN_PASSWORD, or KMP_DEV_PLATFORM_ADMIN_TOTP_SECRET in app/config/.env only when you need a different local-only platform admin identity. These committed values are disposable development credentials and must not be copied into a managed environment.

Local Docker auto-creates KMP_PLATFORM and KMP_PLATFORM_test when KMP_AUTO_CREATE_DATABASES=true. Production deployments should create platform databases through infrastructure automation and leave auto-create disabled.

Regenerate PostgreSQL Schema Dumps

After the PostgreSQL dev stack is healthy and migrations have been applied, refresh the committed schema-only dumps with:

docker compose exec -T db pg_dump -UKMPSQLDEV --schema-only --no-owner --no-privileges KMP_DEV > app/config/schema_dump.sql
cp app/config/schema_dump.sql app/tests/kmp_sql.sql

Do not hand-edit or fake these dumps. If the local PostgreSQL stack cannot be started safely, leave the existing dumps unchanged and report the blocker.

Queue and Scheduled Jobs

Local Docker runs ordinary queues and schedules in the scheduler service, and privileged platform operations in a separate administrative worker. Both start with ./dev-up.sh and are independently observable and restartable while the web container handles requests.

Service Command Purpose
scheduler kmp-scheduler-loop Dispatch platform schedules and continuously drain default, tenant, and platform queues
admin-worker kmp-admin-job-loop-dev Run platform jobs run --limit 1 for queued provisioning, backup, restore, and migration operations

With KMP_TENANCY_ENABLED=true, the scheduler loop polls every ten seconds by default. Each poll runs bin/cake platform worker run when its five-second queue interval is due. Each cycle dispatches due platform schedules, drains up to 100 jobs or 45 seconds from each datasource within a 240-second fleet budget. Privileged platform jobs are claimed only by the administrative worker. Tenant ordering rotates between cycles and duplicate physical datasources are skipped. Queue cycles repeat while work remains instead of waiting for a minute cron tick. In single-database mode, the scheduler loop drains the default queue directly and runs the legacy commands listed below.

Variable Default Purpose
KMP_SCHEDULER_POLL_INTERVAL 10 seconds Sleep between scheduler-loop checks
KMP_QUEUE_DRAIN_INTERVAL 5 seconds Minimum due interval for the bounded worker/default-queue cycle
KMP_SKIP_INITIAL_DB_SETUP true on both workers Prevent workers from repeating entrypoint schema initialization
KMP_ADMIN_JOB_POLL_INTERVAL 10 seconds Sleep between administrative job cycles
KMP_WORKFLOW_SCHEDULER_INTERVAL 60 seconds bin/cake workflow_scheduler
KMP_ACTIVE_WINDOW_SYNC_INTERVAL 900 seconds bin/cake sync_active_window_statuses
KMP_MEMBER_WARRANTABLE_SYNC_INTERVAL 86400 seconds bin/cake sync_member_warrantable_statuses
KMP_AGE_UP_MEMBERS_INTERVAL 86400 seconds bin/cake age_up_members

Inspect background output with:

docker compose logs -f scheduler admin-worker

KMP_SKIP_CRON=true is set for the Compose app and both worker services so the old in-container cron path does not duplicate background work. Manual runs use the same container-first pattern:

docker compose exec app bin/cake platform worker run
# Single-database mode only:
docker compose exec app bin/cake queue run -q --exit-when-empty
docker compose exec app bin/cake workflow_scheduler
docker compose exec app bin/cake sync_active_window_statuses
docker compose exec app bin/cake sync_member_warrantable_statuses
docker compose exec app bin/cake age_up_members

Configuration

The local helper scripts use app/config/.env for Docker Compose and the application. ./dev-up.sh creates it from app/config/.env.example when it is missing.

Variable Default Description
KMP_APP_URL http://kmp.localhost:8080 Primary local app URL printed by ./dev-up.sh and used to derive its /livez health check
KMP_PLATFORM_ADMIN_URL http://platform.kmp.localhost:8080/platform-admin Platform-admin URL printed by ./dev-up.sh
KMP_SECOND_TENANT_URL http://kmp2.localhost:8080 Second test-tenant URL printed by ./dev-up.sh
KMP_APP_BIND_ADDR 127.0.0.1 Bind address for the app HTTP port; use 0.0.0.0 only on a trusted network
KMP_APP_PORT 8080 Host port for the app
KMP_DB_HOST_PORT 5432 Host port for PostgreSQL
KMP_PGADMIN_BIND_ADDR 127.0.0.1 Bind address for pgAdmin
KMP_PGADMIN_PORT 5050 Host port for pgAdmin
KMP_PGADMIN_SERVER_MODE False Run pgAdmin in no-login desktop mode for localhost-bound dev
KMP_PGADMIN_EMAIL admin@kmpdev.org Local pgAdmin login email when server mode is enabled
KMP_PGADMIN_PASSWORD kmpdevpass Local pgAdmin login password when server mode is enabled
KMP_MAILPIT_WEB_PORT 8025 Host port for Mailpit UI
KMP_MAILPIT_WEB_BIND_ADDR 127.0.0.1 Bind address for Mailpit UI; this can be exposed without exposing SMTP
KMP_MAILPIT_URL http://localhost:8025 Mailpit URL printed by ./dev-up.sh
KMP_MAILPIT_BIND_ADDR 127.0.0.1 Bind address for Mailpit SMTP; keep loopback-only unless remote SMTP access is required
KMP_MAILPIT_SMTP_PORT 1025 Host port for Mailpit SMTP
KMP_HOST_ALIASES kmp.localhost kmp2.localhost platform.kmp.localhost Space-separated host aliases printed by ./dev-up.sh
KMP_DB_DRIVER postgres CakePHP database driver
POSTGRES_DB / DB_DATABASE KMP_DEV Development database name
POSTGRES_USER / DB_USERNAME KMPSQLDEV Development database user
POSTGRES_PASSWORD / DB_PASSWORD kmpdevpass Development database password
DATABASE_URL postgres://KMPSQLDEV:kmpdevpass@db:5432/KMP_DEV App database DSN
DATABASE_TEST_URL postgres://KMPSQLDEV:kmpdevpass@db:5432/KMP_DEV_test Test database DSN
PLATFORM_DB_DATABASE KMP_PLATFORM Platform metadata database name
PLATFORM_DATABASE_URL postgres://KMPSQLDEV:kmpdevpass@db:5432/KMP_PLATFORM Platform metadata database DSN
PLATFORM_DATABASE_TEST_URL postgres://KMPSQLDEV:kmpdevpass@db:5432/KMP_PLATFORM_test Test platform metadata database DSN
KMP_AUTO_CREATE_DATABASES true Local-only database auto-create guard used by the container entrypoint
KMP_TENANCY_ENABLED true Enable host-based tenant resolution for local tenant testing
KMP_DEV_TENANT_SLUG kmp Local baseline tenant slug registered during database reset
KMP_DEV_TENANT_DISPLAY_NAME KMP Development Local baseline tenant display name
KMP_DEV_TENANT_HOST kmp.localhost Host mapped to the local baseline tenant
KMP_DEV_TENANT_HOST_ALIASES empty Comma- or space-separated additional hosts mapped to the baseline tenant during reset
KMP_DEV_SECOND_TENANT_HOST kmp2.localhost Primary host used when provisioning the second test tenant
KMP_DEV_SECOND_TENANT_HOST_ALIASES empty Comma- or space-separated additional hosts mapped to the second test tenant during reset
XDEBUG_MODE debug,develop Runtime Xdebug mode
KMP_SKIP_CRON true Disable legacy cron setup; Compose worker/scheduler services own background work
KMP_SCHEDULER_POLL_INTERVAL 10 Sleep between local scheduler-loop checks
KMP_QUEUE_DRAIN_INTERVAL 5 Minimum interval between bounded worker/default-queue cycles
KMP_*_INTERVAL See Queue and Scheduled Jobs Scheduler loop intervals for local background commands
KMP_RESET_DB_ON_UP true Run dev-reset-db.sh after the app becomes healthy
KMP_RESET_DB_ON_UP_ARGS --seed Arguments passed to dev-reset-db.sh during startup

Mailpit contains captured development email, including password-reset links and other test data. Only bind its UI to a trusted network, and keep its SMTP port loopback-only unless remote SMTP submission is explicitly needed.

Troubleshooting

Container won’t start

docker compose logs app
docker compose logs db
docker compose down -v
./dev-up.sh --build

Wrong worktree is serving locally

Run ./dev-up.sh from the worktree you want active. It removes existing kmp-app, the legacy kmp-worker, kmp-scheduler, kmp-admin-worker, kmp-db, kmp-pgadmin, and kmp-mailpit containers and any running containers publishing the configured app, database, pgAdmin, or Mailpit ports before starting the current stack.

Database connection issues

docker compose ps
docker compose logs db
docker compose exec db psql -UKMPSQLDEV -d KMP_DEV -c "\\dt"

Permission issues

docker compose exec app chown -R www-data:www-data /var/www/html/logs /var/www/html/tmp
docker compose exec app chmod -R 775 /var/www/html/logs /var/www/html/tmp

Clear all caches

docker compose exec app bin/cake cache clear_all

Devcontainer Notes

The .devcontainer/ configuration remains available as optional legacy tooling, but Docker Compose is the default local development path. Do not run the devcontainer and the root Docker Compose stack at the same time unless you have changed ports; they can compete for the same host bindings.

File Structure

docker/
|-- Dockerfile.app      # PHP/Apache development container
|-- apache-vhost.conf   # Apache configuration
|-- app_local.php       # CakePHP config copied by the entrypoint
|-- entrypoint.sh       # Container initialization and cron setup
|-- scheduler-loop.sh   # Local scheduler service loop

docker-compose.yml      # Local service definitions
dev-up.sh               # Start environment
dev-down.sh             # Stop environment
dev-reset-db.sh         # Reset database
app/config/.env.example # Environment template for app and local Docker