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