A simple Docker Compose setup for DHIS2 Core 2.42.5 with a PostGIS-backed PostgreSQL database.
Optionally, it can also bring up a chap-core
v2.0.0 server (optionally with chapkit models such as EWARS) wired to DHIS2 through a
DHIS2 Route — see Running with chap-core.
- Docker installed
- Docker Compose v2.20+ (the
docker composeplugin; the chap overlay uses theincludedirective, which legacydocker-composedoes not support) make(optional, but the documented commands use it)
Every value has a working default baked into the compose files, so the stack runs
without a .env. Create one only to change credentials or published ports — copy the
template and edit:
cp .env.example .envDocker Compose automatically loads .env from the project directory.
Database credentials use clean, prefixed variables (the compose files map them onto
the images' own DB_* / POSTGRES_* names for you):
| Variables | Default |
|---|---|
DHIS2_DB_NAME / DHIS2_DB_USER / DHIS2_DB_PASSWORD |
dhis / dhis / dhis |
CHAP_DB_NAME / CHAP_DB_USER / CHAP_DB_PASSWORD |
chap_core / chap / chap |
From the repository root (runs in the foreground — Ctrl+C to stop):
make start # DHIS2 only
# equivalent to: docker compose up (add -d to detach)This starts the DHIS2-only stack (compose.yml):
dhis2-web- DHIS2 application (published on127.0.0.1:8080)dhis2-db- PostGIS database (published on127.0.0.1:15432for psql)dhis2-db-dump- one-shot: downloads and prepares the database dump, then exitsdhis2-db-prep- one-shot (after the dump loads, before DHIS2 boots): runsANALYZEso the freshly restored DB has query-planner statistics (without it the first analytics run takes ~20 min instead of ~20s), and resets any job leftRUNNING(e.g. from aCtrl+Cmid-analytics) back toSCHEDULEDso it can't block analytics. Runs on every startdhis2-analytics- one-shot: after DHIS2 is healthy, generates theanalytics_*tables (needed by the Data Visualizer, Climate app, and CHAP), then exitschap-route-init- one-shot: repoints the demo dump'schaproute (which ships aimed at a remote CHAP server) athost.docker.internal:8000— i.e. a chap-core you run from source on the host. SetCHAP_ROUTE_URLto override the target. (The chap overlay points it at the bundledchapservice instead — see below.)
First startup takes a few minutes: the dump loads, DHIS2 migrates and boots, then
analytics-trigger populates analytics before exiting. Then open DHIS2 at:
http://127.0.0.1:8080
Default credentials:
admin/district.
To also run chap-core alongside DHIS2, see Running with chap-core.
Published on the host (env-overridable); EWARS and redis stay internal:
| Var | Service | Bind | Default | |
|---|---|---|---|---|
DHIS2_PORT |
DHIS2 web | 127.0.0.1 |
8080 |
UI / API |
CHAP_PORT |
chap-core | 127.0.0.1 |
8000 |
API + /docs (no auth) |
DHIS2_DB_PORT |
DHIS2 PostGIS | 127.0.0.1 |
15432 |
browse with psql |
CHAP_DB_PORT |
chap Postgres | 127.0.0.1 |
15433 |
browse with psql |
DHIS2_PORT and CHAP_PORT are the two you'll most likely change if 8080 / 8000
clash with other services already running on this machine:
DHIS2_PORT=8081 CHAP_PORT=8001 make start-chapchap-core has no authentication, so CHAP_PORT binds 127.0.0.1 (loopback only, like
the DBs). Remove the mapping to keep chap fully internal — DHIS2 still reaches it over the
Compose network via the chap route either way.
Connect a SQL client to the data after make start-chap:
| Host | Port | Database | User | Password | |
|---|---|---|---|---|---|
| DHIS2 | 127.0.0.1 |
15432 |
dhis |
dhis |
dhis |
| chap | 127.0.0.1 |
15433 |
chap_core |
chap |
chap |
psql -h 127.0.0.1 -p 15432 -U dhis dhis # DHIS2 data
psql -h 127.0.0.1 -p 15433 -U chap chap_core # chap dataThe DB ports default to the 15xxx range to avoid clashing with a local postgres on
5432. Running several stacks? Give each its own .env with distinct ports.
The start targets run in the foreground, so Ctrl+C stops the stack (containers
remain and resume on the next make start).
For a full reset — remove containers, networks, and volumes (forces a fresh dump load and analytics run next time):
make clean
# equivalent to: docker compose -f compose.chapkit.yml down -vmake start-chap runs compose.chapkit.yml, the umbrella overlay for the whole chap
stack. It includes, in layers:
compose.chap.yml—includes the DHIS2 stack above and adds a chap-corev2.0.0server, its worker / broker / database, and a one-shot that creates the DHIS2 Route connecting the two.compose.ewars.yml— the EWARS chapkit model. Each chapkit model lives in its own per-model overlay (mirroring chap-core's own layout); add a model by copying this file and listing it incompose.chapkit.yml.
Bring up everything (DHIS2 + chap-core + chapkit models), foreground (Ctrl+C to stop):
make start-chap
# equivalent to: docker compose -f compose.chapkit.yml up (add -d to detach)This adds, on top of the DHIS2 services:
chap- chap-core REST API (internal; reached by DHIS2 athttp://chap:8000)chap-worker- Celery worker that runs the models (INLA/R baked in)chap-redis- broker (internal)chap-postgres- chap database (published on127.0.0.1:15433for psql)chap-ewars- EWARS chapkit model; self-registers with chap on startup (internal)chap-route-init- one-shot that wires up the DHIS2 → chap route, then exits
To run chap-core without the chapkit models, use the base overlay directly:
docker compose -f compose.chap.yml up. To run a single model, stack its overlay on the
base, e.g. docker compose -f compose.chap.yml -f compose.ewars.yml up.
All access to chap-core goes through DHIS2, via a DHIS2 Route
(a built-in reverse proxy) with code chap pointing at http://chap:8000/**. chap-core
itself has no authentication, so it is never published to the host — DHIS2 is the
only entry point, and it enforces auth.
chap-route-init sets this up automatically once DHIS2 is healthy (it runs in both
stacks). It is self-correcting: the climate demo dumps ship a chap route aimed at an
external CHAP server, so the one-shot repoints it rather than leaving the stale
target in place. The target depends on the stack:
make start(DHIS2 only) →http://host.docker.internal:8000/**(a chap-core you run from source on the host; override withCHAP_ROUTE_URL).make start-chap(overlay) →http://chap:8000/**(the bundled chap service).
Verify the route proxies through:
curl -u admin:district http://127.0.0.1:8080/api/routes/chap/run/health
# -> {"status":"success","message":"healthy"}The manual equivalent (e.g. if you recreate it by hand) is a POST/PUT to
/api/routes with {"name":"chap","code":"chap","url":"http://chap:8000/**"}.
chap-core is published on the host at CHAP_PORT (default 8000), so its Swagger UI is
at http://localhost:8000/docs and the API at
http://localhost:8000/.... (No auth; bound to 127.0.0.1.)
To instead serve the docs through the DHIS2 route at
http://localhost:8080/api/routes/chap/run/docs, uncomment the CHAP_ROOT_PATH line on
the chap service in compose.chap.yml — that sets FastAPI's root_path so the proxied
docs fetch the OpenAPI spec through the route instead of the DHIS2 origin. (The route
itself works regardless; this only affects the proxied /docs page.)
Install the Modelling App
from the App Hub (App Management → App Hub). It uses the chap route above and the
analytics_* tables populated by analytics-trigger. Installing apps is outside the
scope of this compose setup.
Both databases are published on 127.0.0.1 so you can browse the data with psql — see
Ports & databases for the connection details.
- The chap-core and chap-worker images are pinned to the
v2.0.0tag (note the leadingv). The EWARS chapkit model (compose.ewars.yml) tracks:latest(pulled on everyup) so it doesn't go stale. - To stop and remove the chap-core volumes as well:
docker compose -f compose.chapkit.yml down -v.
- Published host ports — DHIS2 web, chap-core, and both databases — bind
127.0.0.1and are env-overridable (see Ports & databases). EWARS and redis are internal to the Compose network. - The
db-dumpservice downloads and patches the dump file into the named volume only once; deleting the volume forces it to re-download. analytics-triggerruns the analytics export on first boot; it re-runs on everyup(cheap if already current). DHIS2 needs a few GB of RAM for the populate phase — if the container gets OOM-killed mid-run, raise Docker's memory limit.