Skip to content

Deploy the engine

Start the CAPS engine from published container images. Pin a release tag. The current release is 3.0.0.

  • Docker 24 or newer, and Compose v2, on a host that can pull ghcr.io/hisptz/caps-engine/.
  • A tag to pin. Use 3.0.0. latest moves with the default branch and is only for a trial.
  • The ports, accounts, and image names in Requirements.
  • URLs the workers can call, from CHAP, climate, and DHIS2. Add those lines to .env before the first start. The process rejects an empty URL.

make up in the caps-engine repository always builds images from source. That is a local development loop, not this install.

Three different processes read URLs. A value that is right for one of them is often wrong for the others.

flowchart TB
  env[".env on the Docker host"]
  env -->|"postgres and rabbitmq hostnames"| containers[API workers scheduler migrate]
  env -->|"DHIS2 CHAP and climate URLs"| workers[Workers]
  admin[Operator browser] --> dhis2[DHIS2]
  dhis2 -->|"Route base URL plus /**"| api[CAPS API]

You can run CAPS with its own Postgres and RabbitMQ, or attach it to the Postgres and RabbitMQ that CHAP is already using. The second layout does not change CHAP_BASE_URL. That variable is the CHAP HTTP API, and it is required in both layouts.

flowchart TB
  subgraph own [CAPS with its own data stores]
    api1[API] --> pg1[(Postgres)]
    api1 --> mq1[[RabbitMQ]]
  end
  subgraph shared [CAPS on an existing CHAP network]
    api2[API] --> pg2[(CHAP Postgres)]
    api2 --> mq2[[CHAP RabbitMQ]]
  end

Save both files in an empty directory. Replace every replace-me with a password you keep out of git. The same password has to appear in the matching URL.

.env
IMAGE_TAG=3.0.0
API_PORT=4000
POSTGRES_USER=caps
POSTGRES_PASSWORD=replace-me
POSTGRES_DB=caps
# Hostnames are the Compose service names. localhost is the container itself.
DATABASE_URL=postgresql://caps:replace-me@postgres:5432/caps
RABBITMQ_USER=caps
RABBITMQ_PASSWORD=replace-me
RABBITMQ_URL=amqp://caps:replace-me@rabbitmq:5672
# Required. The API does not boot without it.
CHAP_BASE_URL=https://chap.example.org
CHAP_API_TOKEN=
compose.yml
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 5s
timeout: 5s
retries: 10
restart: unless-stopped
networks: [caps]
rabbitmq:
image: rabbitmq:3.13-management-alpine
environment:
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER}
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD}
volumes:
- rabbitmq_data:/var/lib/rabbitmq
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "ping"]
interval: 10s
timeout: 10s
retries: 10
restart: unless-stopped
networks: [caps]
migrate:
image: ghcr.io/hisptz/caps-engine/migrate:${IMAGE_TAG}
environment:
DATABASE_URL: ${DATABASE_URL}
depends_on:
postgres:
condition: service_healthy
restart: on-failure
networks: [caps]
worker-1:
image: ghcr.io/hisptz/caps-engine/worker:${IMAGE_TAG}
environment: &worker_env
DATABASE_URL: ${DATABASE_URL}
RABBITMQ_URL: ${RABBITMQ_URL}
DHIS2_BASE_URL: ${DHIS2_BASE_URL}
DHIS2_USERNAME: ${DHIS2_USERNAME}
DHIS2_PASSWORD: ${DHIS2_PASSWORD}
CHAP_BASE_URL: ${CHAP_BASE_URL}
CHAP_API_TOKEN: ${CHAP_API_TOKEN:-}
CLIMATE_API_BASE_URL: ${CLIMATE_API_BASE_URL}
CLIMATE_DATA_BASE_URL: ${CLIMATE_DATA_BASE_URL}
OUTPUTS_DIR: /tmp/caps-outputs
depends_on: &worker_depends
postgres:
condition: service_healthy
rabbitmq:
condition: service_healthy
migrate:
condition: service_completed_successfully
volumes:
- caps_outputs:/tmp/caps-outputs
restart: unless-stopped
networks: [caps]
worker-2:
image: ghcr.io/hisptz/caps-engine/worker:${IMAGE_TAG}
environment: *worker_env
depends_on: *worker_depends
volumes:
- caps_outputs:/tmp/caps-outputs
restart: unless-stopped
networks: [caps]
# Exactly one replica. A second scheduler double-fires pipelines.
scheduler:
image: ghcr.io/hisptz/caps-engine/scheduler:${IMAGE_TAG}
environment:
DATABASE_URL: ${DATABASE_URL}
DHIS2_BASE_URL: ${DHIS2_BASE_URL}
DHIS2_USERNAME: ${DHIS2_USERNAME}
DHIS2_PASSWORD: ${DHIS2_PASSWORD}
CHAP_BASE_URL: ${CHAP_BASE_URL}
CHAP_API_TOKEN: ${CHAP_API_TOKEN:-}
CLIMATE_API_BASE_URL: ${CLIMATE_API_BASE_URL}
CLIMATE_DATA_BASE_URL: ${CLIMATE_DATA_BASE_URL}
depends_on:
postgres:
condition: service_healthy
migrate:
condition: service_completed_successfully
restart: unless-stopped
networks: [caps]
api:
image: ghcr.io/hisptz/caps-engine/api:${IMAGE_TAG}
environment:
DATABASE_URL: ${DATABASE_URL}
RABBITMQ_URL: ${RABBITMQ_URL}
PORT: 4000
DHIS2_BASE_URL: ${DHIS2_BASE_URL}
DHIS2_USERNAME: ${DHIS2_USERNAME}
DHIS2_PASSWORD: ${DHIS2_PASSWORD}
CHAP_BASE_URL: ${CHAP_BASE_URL}
CHAP_API_TOKEN: ${CHAP_API_TOKEN:-}
CLIMATE_API_BASE_URL: ${CLIMATE_API_BASE_URL}
CLIMATE_DATA_BASE_URL: ${CLIMATE_DATA_BASE_URL}
ports:
- "${API_PORT:-4000}:4000"
depends_on:
postgres:
condition: service_healthy
migrate:
condition: service_completed_successfully
restart: unless-stopped
networks: [caps]
volumes:
postgres_data:
rabbitmq_data:
caps_outputs:
networks:
caps:

Postgres and RabbitMQ are not published to the host. The API is published on API_PORT (4000 unless you change it). Put TLS in front of that port when the Route URL uses https. The app does not terminate TLS.

Terminal window
docker compose -f compose.yml pull
docker compose -f compose.yml up -d
docker compose -f compose.yml ps

If the registry denies the pull, sign in to ghcr.io with a GitHub account that can read hisptz/caps-engine.

The API, worker, and scheduler images are distroless. They have no shell. Read them with docker compose logs, not docker exec.

The OpenAPI document is served by the API at /openapi (Scalar) and /openapi/json. It is a reference for the HTTP API, not a list of pipeline steps.

docker compose ps shows migrate exited with code 0. api, worker-1, worker-2, and one scheduler are running.

From a machine on the same Docker network as api:

Terminal window
curl -fsS http://api:4000/system/info

GET /system/info needs no authentication header. A 200 means the API process answered. CHAP, DHIS2, and the climate service can still report connected: false inside that body. A 200 from your laptop only proves the published host port, which is not the URL the DHIS2 server will use.

The API restarts immediately when CHAP_BASE_URL is missing, or when DATABASE_URL or RABBITMQ_URL uses localhost. See Troubleshooting.

Next: CHAP, climate, and DHIS2, then Run and upgrade.