Deploy with Docker Compose
The production Compose stack runs the gateway, PostgreSQL, and Redis. Put a TLS reverse proxy in front of it; the published application and metrics ports bind to loopback by default.
Prepare the deployment
Section titled “Prepare the deployment”Copy .env.prod.example to .env.prod, set its mode to 0600, and replace every required value.
Use an immutable image tag or digest—there is deliberately no latest default.
At minimum, set:
EMCP_TAGand, if needed,EMCP_IMAGEEMCP_BASE_URLto the public HTTPS originEMCP_PG_PASSWORDEMCP_KEYS_DIR- SMTP notification settings
Generate the four long-lived key files before the first start:
docker compose -f compose.prod.yml --env-file .env.prod \ --profile init up keygenCopy the printed key_… identifier into EMCP_KEK_ACTIVE_ID, then start the stack:
docker compose -f compose.prod.yml --env-file .env.prod up -dThe migration service applies pending migrations before the gateway becomes ready. Verify the
deployment through /healthz and /readyz, not merely by checking that the container is running.
Configure the reverse proxy
Section titled “Configure the reverse proxy”Forward HTTPS traffic to the application listener, normally 127.0.0.1:8080. Preserve WebSocket
upgrade headers and set the upstream read timeout to at least 14,700 seconds for long waits. Do not
publish the metrics listener (127.0.0.1:9090) to the internet.
Separate workers
Section titled “Separate workers”The default gateway process also runs background workers. For a two-tier deployment, set:
EMCP_WORKERS_IN_GATEWAY=falseEMCP_EXPECT_HA=trueThen include the tiered profile:
docker compose -f compose.prod.yml --env-file .env.prod \ --profile tiered up -dKeep at least one worker running. Without one, approvals, outgoing webhooks, cleanup sweeps, and security posture rechecks stop progressing.