Skip to main content

Documentation 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.

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 adds 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.yml file 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

1

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 infra/dokploy/env.example as your checklist:
# On your workstation — copy the template and review every line.
cp infra/dokploy/env.example .env
# Populate real values, then inject them via Dokploy.
# NEVER commit the populated .env file.
The production compose file has no fallback defaults for secret variables. If 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.
2

Upload or reference docker-compose.prod.yml

Provide Dokploy with the production compose file at 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:
Aspectdocker-compose.local.ymldocker-compose.prod.yml
Host port for harness-core4321:4321 (bound to host)4321:4321 (fronted by Dokploy reverse proxy)
Bind mountsYes — source is mounted from the repo rootNone — only named volumes
Restart policyNone (dev default)restart: unless-stopped on all services
Env variable defaultsMany variables have fallback defaultsSecrets have no defaults; missing values abort startup
vLLM seamCommented placeholder for PR-2Same commented seam
Both files use the same Dockerfiles from infra/docker/ and the same Docker network (harness-net, bridge driver).
3

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:
docker compose -f infra/compose/docker-compose.prod.yml up -d --build
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.
4

Verify all services are healthy

Wait for the stack to settle and then inspect Docker’s health-check status for every service:
docker compose -f infra/compose/docker-compose.prod.yml ps
A correctly started Phase 0.b stack produces output similar to:
NAME                     STATUS
harness-postgres          running (healthy)
harness-postgres-auth     running (healthy)
harness-redis             running (healthy)
runtime-python            running (healthy)
mcp-identity-connector    running (healthy)
model-gateway             running (healthy)
harness-core              running
The runtime-python health check uses a Python stdlib one-liner (the python:3.12-slim base image does not ship wget):
# Confirm both supervised processes are alive inside runtime-python.
# You should see at least one `uvicorn` line and one `rq worker executions` line.
docker compose -f infra/compose/docker-compose.prod.yml \
  exec runtime-python ps -ef | grep -E 'uvicorn|rq worker'
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.
5

Verify the BFF health endpoint

The only publicly-exposed port is harness-core:4321. Confirm the BFF is serving traffic and can reach the runtime:
curl -fsS http://localhost:4321/api/healthz
A healthy stack returns HTTP 200 with a JSON body:
{
  "status": "ok",
  "runtime": "ok",
  "service": "harness-core",
  "version": "0.1.0"
}
If the runtime is unreachable, the BFF returns HTTP 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).
Do not probe runtime-python directly from the host (for example curl localhost:8000/healthz). The runtime has no ports: mapping in either compose file and is intentionally unreachable from outside the Docker network. All health observation must go through harness-core:4321 or through Docker’s built-in health-check status.
6

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:
bash infra/scripts/phase0-regression-smoke.sh
The script brings up the four-service Phase 0 stack using a test overlay, waits up to 90 seconds for 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:
CodeMeaning
0All assertions passed; stack torn down cleanly
1docker compose failed (up / healthcheck / down)
2/api/healthz did not return 200
3ps --services did not return exactly the expected four service names
4harness-core did not become healthy within the timeout
The script reads .env from the repository root. Copy and populate infra/dokploy/env.example to .env before running it.

The runtime-python Supervisor Entrypoint

The runtime-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:
#!/usr/bin/env bash
set -euo pipefail

API_HOST="${API_HOST:-0.0.0.0}"
API_PORT="${API_PORT:-8000}"
API_EXECUTION_QUEUE="${API_EXECUTION_QUEUE:-executions}"

# Fail fast if the broker URL is missing — silent defaulting would mask
# a misconfigured deploy until the worker crashes on first connection.
: "${REDIS_URL:?REDIS_URL must be set (e.g. redis://redis:6379/0)}"

UVICORN_PID=""
RQ_PID=""

cleanup() {
    local sig="$1"
    echo "[entrypoint] received ${sig}; forwarding to children"
    if [ -n "${UVICORN_PID}" ] && kill -0 "${UVICORN_PID}" 2>/dev/null; then
        kill -TERM "${UVICORN_PID}" 2>/dev/null || true
    fi
    if [ -n "${RQ_PID}" ] && kill -0 "${RQ_PID}" 2>/dev/null; then
        kill -TERM "${RQ_PID}" 2>/dev/null || true
    fi
}

trap 'cleanup SIGTERM' SIGTERM
trap 'cleanup SIGINT'  SIGINT

echo "[entrypoint] starting uvicorn on ${API_HOST}:${API_PORT}"
uvicorn runtime.api.main:app --host "${API_HOST}" --port "${API_PORT}" &
UVICORN_PID=$!

echo "[entrypoint] starting rq worker on queue '${API_EXECUTION_QUEUE}'"
rq worker "${API_EXECUTION_QUEUE}" --url "${REDIS_URL}" &
RQ_PID=$!

echo "[entrypoint] uvicorn_pid=${UVICORN_PID} rq_pid=${RQ_PID}"

# wait -n returns as soon as EITHER child exits and yields that child's
# exit status. The script propagates that exact code to Docker so the
# container flips to (unhealthy) if either process dies.
wait -n
WAIT_EXIT=$?

echo "[entrypoint] a child exited with status ${WAIT_EXIT}; terminating the surviving child"

if [ -n "${UVICORN_PID}" ] && kill -0 "${UVICORN_PID}" 2>/dev/null; then
    kill -TERM "${UVICORN_PID}" 2>/dev/null || true
fi
if [ -n "${RQ_PID}" ] && kill -0 "${RQ_PID}" 2>/dev/null; then
    kill -TERM "${RQ_PID}" 2>/dev/null || true
fi

exit "${WAIT_EXIT}"
The key contract: if either 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 exact healthcheck.test commands from the production compose file for quick reference.
ServiceHealth check command
postgrespg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB:-harness_db}
postgres-authpg_isready -U ${POSTGRES_AUTH_USER} -d ${POSTGRES_AUTH_DB:-harness_auth_db}
redisredis-cli ping
runtime-pythonpython -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-connectorpython -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-gatewaypython -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)"
All Python-based healthchecks use 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:
# Graceful shutdown only
docker compose -f infra/compose/docker-compose.prod.yml down

# Graceful shutdown + wipe named volumes (destructive — drops all data)
docker compose -f infra/compose/docker-compose.prod.yml down -v
You can verify that the production compose file parses correctly and that all variable references resolve before running up by using the --quiet flag with docker compose config:
docker compose -f infra/compose/docker-compose.prod.yml config --quiet
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.

Hard Rules for Production

  • Never bind localhost:8000 or any host port to runtime-python. The runtime is internal-only and must only be observed through harness-core:4321 and Docker’s health-check mechanism.
  • Never add any service other than harness-core to a public-facing network.
  • Never commit .env, client_secret values, or signed webhook payloads to the repository.
  • Never populate # PHASE 0.b environment variables in a Phase 0 deployment — they are inert seams and activating them prematurely will cause startup failures.
For the full environment variable reference, see Environment Variables Reference.

Build docs developers (and LLMs) love