Dokploy is the target production deployment platform for Harness AI. The same Docker Compose service topology used in local development maps directly to production — the same images, the same network isolation rules, and the same health-check contracts apply in both environments. The only structural difference is that the production compose file removes all host bind-mounts and addsDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/joseluis-dev/harness-ai/llms.txt
Use this file to discover all available pages before exploring further.
restart: unless-stopped to every long-running service. TLS termination is handled by Dokploy’s reverse proxy layer rather than by the application.
Prerequisites
Before deploying, confirm the following are in place:- A Dokploy instance with Docker 24+ and Compose v2 installed on the target host.
- A reachable public hostname pointing to the host (TLS is terminated by Dokploy or an upstream reverse proxy — the application never handles TLS directly).
- All required environment variables configured in the Dokploy secrets manager. See Environment Variables Reference for the complete list.
- The
infra/compose/docker-compose.prod.ymlfile from the repository. - PostgreSQL credentials (
POSTGRES_USER,POSTGRES_PASSWORD,POSTGRES_AUTH_USER,POSTGRES_AUTH_PASSWORD) generated as strong random values and stored exclusively in Dokploy — never in the repository.
Deployment Procedure
Configure all environment variables in Dokploy secrets manager
Before any compose operation, every variable in the production env template must be populated in the Dokploy secrets manager. Use The production compose file has no fallback defaults for secret variables. If
infra/dokploy/env.example as your checklist:HARNESS_DATABASE_URL, POSTGRES_USER, POSTGRES_PASSWORD, or any other required value is absent, docker compose up will fail immediately with a clear error rather than silently using an insecure default.Refer to Environment Variables Reference for the description and target service of every variable.Upload or reference docker-compose.prod.yml
Provide Dokploy with the production compose file at
Both files use the same Dockerfiles from
infra/compose/docker-compose.prod.yml. You can either upload the file through the Dokploy UI or configure Dokploy to reference the repository path directly.The production compose file differs from the local one in these specific ways:| Aspect | docker-compose.local.yml | docker-compose.prod.yml |
|---|---|---|
Host port for harness-core | 4321:4321 (bound to host) | 4321:4321 (fronted by Dokploy reverse proxy) |
| Bind mounts | Yes — source is mounted from the repo root | None — only named volumes |
| Restart policy | None (dev default) | restart: unless-stopped on all services |
| Env variable defaults | Many variables have fallback defaults | Secrets have no defaults; missing values abort startup |
| vLLM seam | Commented placeholder for PR-2 | Same commented seam |
infra/docker/ and the same Docker network (harness-net, bridge driver).Build images and bring the stack up
Run the following command on the Dokploy host to build all images from source and start every service in detached mode:Dokploy can also trigger this step automatically on each deployment. The
--build flag ensures images are rebuilt from the current Dockerfiles rather than using a potentially stale cached layer.Verify all services are healthy
Wait for the stack to settle and then inspect Docker’s health-check status for every service:A correctly started Phase 0.b stack produces output similar to:The
runtime-python health check uses a Python stdlib one-liner (the python:3.12-slim base image does not ship wget):There is no separate
worker container in Phase 0 or Phase 0.b. Both uvicorn (FastAPI) and rq worker executions run inside a single runtime-python container, supervised by infra/docker/entrypoint.runtime-python.sh. Splitting the RQ worker into its own compose service is a future Phase 5 optimisation that requires a spec change.Verify the BFF health endpoint
The only publicly-exposed port is A healthy stack returns HTTP If the runtime is unreachable, the BFF returns HTTP
harness-core:4321. Confirm the BFF is serving traffic and can reach the runtime:200 with a JSON body:503 with "status": "degraded". The response body deliberately does not include a postgres field — Postgres container health is observed exclusively via docker compose ps, not through the BFF (the BFF does not open direct database connections by design).Run the Phase 0 regression smoke test (optional)
To verify the full Phase 0 acceptance criteria against the production-shaped compose configuration, run the regression smoke script:The script brings up the four-service Phase 0 stack using a test overlay, waits up to 90 seconds for
The script reads
harness-core to become healthy, asserts that GET /api/healthz returns 200, verifies that exactly the expected set of services is running, and then tears the stack down. Exit codes:| Code | Meaning |
|---|---|
0 | All assertions passed; stack torn down cleanly |
1 | docker compose failed (up / healthcheck / down) |
2 | /api/healthz did not return 200 |
3 | ps --services did not return exactly the expected four service names |
4 | harness-core did not become healthy within the timeout |
.env from the repository root. Copy and populate infra/dokploy/env.example to .env before running it.The runtime-python Supervisor Entrypoint
Theruntime-python container runs two long-lived processes inside a single container. The supervisor is implemented in infra/docker/entrypoint.runtime-python.sh and is set as the image ENTRYPOINT:
uvicorn or the rq worker exits for any reason, wait -n returns immediately with that process’s exit code and the entrypoint terminates the surviving process and exits with the same non-zero code. Docker then flips the container to (unhealthy), which triggers Dokploy’s restart policy.
Health Check Commands by Service
The table below lists the exacthealthcheck.test commands from the production compose file for quick reference.
| Service | Health check command |
|---|---|
postgres | pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB:-harness_db} |
postgres-auth | pg_isready -U ${POSTGRES_AUTH_USER} -d ${POSTGRES_AUTH_DB:-harness_auth_db} |
redis | redis-cli ping |
runtime-python | python -c "import urllib.request,sys; r=urllib.request.urlopen('http://127.0.0.1:8000/healthz',timeout=2); sys.exit(0 if r.status==200 else 1)" |
mcp-identity-connector | python -c "import urllib.request,sys; r=urllib.request.urlopen('http://127.0.0.1:9001/healthz',timeout=2); sys.exit(0 if r.status==200 else 1)" |
model-gateway | python -c "import urllib.request,sys; r=urllib.request.urlopen('http://127.0.0.1:9002/healthz',timeout=2); sys.exit(0 if r.status==200 else 1)" |
urllib.request from the standard library because the python:3.12-slim base image does not include wget or curl.
Rollback
To bring the stack down and optionally wipe the Postgres volumes for a clean slate:You can verify that the production compose file parses correctly and that all variable references resolve before running A zero exit code with no output confirms the file is valid and all required variables are present in the current environment. This is a useful pre-flight check to run in CI before triggering a Dokploy deployment.
up by using the --quiet flag with docker compose config:Hard Rules for Production
- Never bind
localhost:8000or any host port toruntime-python. The runtime is internal-only and must only be observed throughharness-core:4321and Docker’s health-check mechanism. - Never add any service other than
harness-coreto a public-facing network. - Never commit
.env,client_secretvalues, or signed webhook payloads to the repository. - Never populate
# PHASE 0.benvironment variables in a Phase 0 deployment — they are inert seams and activating them prematurely will cause startup failures.