Introduction
Chap forecasts cases of climate-sensitive diseases such as dengue and malaria. It learns from past case counts together with climate data and population, scores how well each forecasting model would have done on the past, and predicts the coming months per district or province. It is often used together with DHIS2, through the Modeling App. Your first forecast in the Modeling App shows it end to end on demo data.
chap-devops deploys and manages Chap and the services
around it, as Docker Compose deployments:
chap-core, the forecasting model
services published in the
Chap model marketplace,
Open Climate Service (OCS) with
an S3-compatible object store, and DHIS2.
They deploy together or any of them on its own. A deployment is made of
components (chap-core, OCS, the object store, DHIS2) plus the model
services you enable. chap-core is on unless you leave it out, so the default is
a Chap deployment. chap-devops init --only ocs,s3 is an OCS server,
chap-devops init --only dhis2 a DHIS2, and
chap-devops init --only none --models ID one model service answering on its own
port. Use cases walks through each shape, and
Components is the reference.
The name
chap-devops is the development and operations tool of Chap. The name says
the two halves of the work it does:
- Operations: it deploys Chap and the services around it, keeps them up to
date, backs them up, and says what is wrong when something stops (
init,up,update,backup,status,doctor). - Development: it makes and tests model projects, and runs the chap and
chapkit CLIs with no Python or R on the machine (
models new,models test,chap,chapkit).
The command has a short form: cdx is the same command as chap-devops. The install
script puts it next to chap-devops.
What it does, in one screen
One command writes a self-contained deployment directory: the base services
(chap-core, its worker, Valkey and PostgreSQL), one Compose overlay per enabled
model, an .env file, and a .chap-devops/ directory that records exactly what the
deployment is meant to be.
chap-devops init mychap --models chapkit_simple_multistep_model # writes the deployment directory
cd mychap
chap-devops up # sync the compose files, docker compose up -d
chap-devops status # chap-core health and registered models
chap-devops ui # browse the marketplace, toggle models
chap-devops up # apply what the browser changed
chap-devops update # move the pins to what upstream publishes now
chap-devops restart # apply them to the services that are running
Other shapes:
chap-devops init mychap --with ocs # Chap with Open Climate Service beside it
chap-devops init climate --only ocs,s3 # OCS alone, with no Chap
A new model:
chap-devops models new my-model # a model project, from a template
After init, chap-devops is two things at once: a thin wrapper around
docker compose that always passes the explicit -f list, and a model manager
that can add or remove models without you hand-editing YAML.
Every command takes --json for machine-readable output, and every command
ends with a line saying what it did or found. Empty output is a bug.
The three words to remember
chap-devops upstarts what is on disk. It renders the compose files from.chap-devops/, checks the host ports, and runsdocker compose up -d. It never moves a pin. Onlychap-devops up --pulldownloads a newer build, and only for an image on a moving tag such as themaintag of OCS.chap-devops updatefetches newer versions. It asks the marketplace and the chap-core release feed what they publish today, moves the pins that follow a channel or a release, and pulls the images. It never touches a container: it ends by naming the services that are now running something out of date.chap-devops restartapplies them to the services that are running. It recreates the containers that no longer match the files and leaves the rest alone, and it changes no file and no pin.
Everything else follows from that split. chap-devops up, chap-devops restart,
chap-devops models expose and chap-devops models unexpose are safe on a deployment
running a build you do not want moved. chap-devops update, chap-devops models enable,
chap-devops models add and chap-devops components enable --tag are the commands that
move a version; see What moves a pin.
The pure server promise
A machine that runs Chap with chap-devops needs Docker and one binary. Nothing
else: no Python, no uv, no checkout of chap-core, no build step on the
server. The Linux releases are static musl builds, so the same file runs on any
distribution.
The compose files chap-devops writes are plain Compose files with nothing
chap-devops-specific in them, so
docker compose -f compose.yml -f compose.chap-devops.yml -f compose.marketplace.yml up -d
works on a machine that has never had chap-devops installed. That is the point of
generating them rather than keeping the deployment inside the tool.
That is the list for chap-core and models. A component adds its own file
before compose.marketplace.yml, for example -f compose.ocs.yml. A
deployment without chap-core has no compose.yml or compose.chap-devops.yml.
Where to go next
- Install puts the binary on the machine.
- Quickstart walks a first deployment end to end.
- Your first forecast in the Modeling App uses one.
- Concepts explains the deployment directory, intent versus artifacts, and the pins.
- Commands is the tour; Command reference is the generated, complete list.
chap-devops is licensed under the AGPL-3.0, like chap-core. The full text is in
LICENSE at the repository root.
For AI assistants
This page is written for an AI assistant that helps someone install and run
something with chap-devops. If a person pointed you here, follow the steps below in order. Keep
your answers short and concrete, and give one command at a time.
What Chap is
Use this when the person asks what they are installing, or seems unsure why they would want it.
Chap forecasts cases of climate-sensitive diseases such as dengue and malaria. It learns from past case counts together with climate data (rainfall, temperature) and population, and predicts the next months for each district or province. Before trusting a model it runs an evaluation: it pretends to stand at several past points in turn, forecasts the months after each, and scores how close each forecast came. Ministries of health use it together with DHIS2, the health information system where their case data already lives, through the Modeling App inside DHIS2.
chap-devops is the installer. It runs Chap, its forecasting models, a DHIS2 with
demo data and the other pieces on one computer with Docker, so the person can
use them without setting each one up by hand.
A few words the person will see:
| Word | Meaning |
|---|---|
| chap-core | Chap’s backend: the service that trains, evaluates and runs the models. The Modeling App is its interface in DHIS2. |
| model | One forecasting method, for example Simple Multistep. Each runs in its own container. |
| DHIS2 | The health information system. chap-devops can run a demo one with data from Laos. |
| Modeling App | Chap’s user interface, installed inside DHIS2. |
| evaluation | Testing a model on past data: it forecasts from several past points and scores each forecast. Also called a backtest. |
| prediction | A forecast for the coming months. |
| OCS | Open Climate Service: downloads and serves climate data. Optional. |
How to help
-
Ask what they want to do, in one question, and match the answer to a group in The options. Do not show all nineteen options.
- Learn Chap, try forecasting, see what it does (a student, an analyst, a public health person): option 1. It is the only one with a user interface they can click through. If their computer cannot give Docker 8 GB of memory, option 2 instead (6 GB).
- Run Chap for a DHIS2 that already exists: option 5.
- One piece on its own (climate data, one model, a DHIS2): options 6 to 10.
- Develop a model, chap-core or DHIS2: options 11 to 18.
- Two deployments at once: option 19.
- Evaluate a model on their own data file, without DHIS2: option 20.
If they are unsure, recommend option 1.
-
Check the machine with Before any option.
-
Give the commands for their option, in order, one block at a time. Wait for them to say it worked before giving the next block. Every option has an “It worked when” line: use it to decide.
-
When it works, say what to do next. Option 1 continues in the Modeling App, and so do options 3 and 4 when they added DHIS2: walk them through Your first forecast in the Modeling App. Option 2 continues with What to try next without DHIS2.
-
If something fails, ask them to paste the exact output. Then look for the first line that starts with
error:orwarning:. chap-devops puts the fix on the same line, usually a command in backticks: tell them to run that command. If that does not help, search Troubleshooting for the exact message. If chap-devops says it does not know a command or an option, the installedchap-devopsis older than this page:chap-devops self updateinstalls the newest release, andchap-devops <command> --helpis always right for the installed one. -
Never tell them to edit the
compose*.ymlfiles by hand (chap-devops rewrites them), and never ask them to paste the.envfile (it holds passwords).
When an option has a “More” link, that page has the details. You only need it if the person asks for something the steps here do not cover.
If you run the commands yourself
If you have a shell on the person’s machine and run the commands rather than telling the person what to type:
- Run each block as written, top to bottom. Where an option shows alternatives, it says so and you pick one. Check its “It worked when” line yourself instead of waiting for the person to say it worked.
- If your shell does not keep its folder between commands, put
-C mychap(the folder fromchap-devops init) on every command in place of thecd:chap-devops -C mychap up. chap-devops statusexits0only when everything the deployment declares is up and registered, and non-zero otherwise, so poll it by its exit code where a step says to run it every minute. A first DHIS2 start can take 20 minutes.chap-devops status --jsongives the same report as one JSON document; every command takes--json.- The first
chap-devops updownloads the images that take most of the disk in Before any option. Give it a long timeout, or run it in the background, and do not start a second one while it runs. Runningchap-devops upagain after it has finished or timed out is safe: it continues from where it stopped. chap-devops dhis2 connectwaits by itself for DHIS2 to start, up to 20 minutes; it prints a line on stderr while it waits.- Nothing prompts you: without a terminal, a question becomes an
error:with the flag that answers it.chap-devops self updateis the exception: without a terminal it replaces the binary and does not ask, so ask the person before you run it.chap-devops up --replacestops another deployment holding the ports,chap-devops down --volumes --yesdeletes data; ask the person before either. - Credentials go in the
.envfile in the folder. Ask the person to add them there themselves, and do not print or read that file. - Do not run
chap-devops ui(an interactive screen),chap-devops up --attachorchap-devops logs -f(they never return).chap-devops logs SERVICEwithout-fprints and returns. chap-devops open NAMEopens a browser on the machine it runs on. Runchap-devops open NAME --no-browserinstead: it prints the address and opens nothing, and you give the person that address (or open it in your own browser tool).--jsonnever opens a browser either.
Before any option
The person needs Docker: Docker Desktop on macOS and Windows, Docker Engine on Linux. On Windows, chap-devops runs inside WSL 2: Docker Desktop with its WSL integration turned on for the distribution, and every command below typed in the WSL shell. Check two numbers before they start:
With DHIS2 (options 1, 8, 9, 10, 17, and 3 or 4 with dhis2 added) | Without DHIS2 | |
|---|---|---|
| Memory for Docker | 8 GB or more | 6 GB or more |
| Free disk | about 30 GB | about 25 GB |
Most of the disk is the model and worker images, which the first chap-devops up
downloads: expect it to take a while on a slow connection. On Docker Desktop
the memory is set under Settings, Resources. Then:
curl -fsSL https://raw.githubusercontent.com/winterop-com/chap-devops/main/install.sh | sh
chap-devops doctor
On Windows, run those two lines in the WSL shell, which installs the Linux binary; do not use the native Windows zip, which is published untested. See Install.
It worked when chap-devops doctor shows no fail lines, and its disk space
line shows at least the number in the table above. chap-devops doctor passes with
less, so compare the number yourself. On an Apple Silicon Mac a warn line
about linux/amd64 and emulation is normal and can be ignored: the models
run, only slower.
chap-devops doctor shows the memory line only in a folder with DHIS2. For an
option with DHIS2, run chap-devops doctor again in the new folder after
chap-devops init. Its memory line, for example 31.3 GB available to docker,
must show at least 8 GB.
Every option below creates a new folder. Tell the person to run the commands
from the folder where they want it created, and then to cd into it as shown.
Several options use the same folder name (mychap): if one from an earlier
option is still there, chap-devops init says it already contains a chap-devops deployment.
Either pick another name in the chap-devops init line (and the cd), or remove the
old one first as shown in Every option: stop, start, remove.
The options
| # | Option | Pick this if the person wants to… |
|---|---|---|
| Use Chap | ||
| 1 | Chap in DHIS2, with the Modeling App | learn Chap: evaluate a model and make a forecast by clicking |
| 2 | Chap with forecasting models | run Chap without DHIS2, from the terminal or its API |
| 3 | Chap with climate data | use Chap together with Open Climate Service |
| 4 | Chap with every model | try all the forecasting models, not just one |
| 5 | Chap for a DHIS2 that already exists | connect Chap to a DHIS2 someone else runs |
| One piece on its own | ||
| 6 | Climate data only | run Open Climate Service, no Chap |
| 7 | One forecasting model only | call one model’s API directly, no Chap |
| 8 | DHIS2 only | have a DHIS2 to play with, no Chap |
| 9 | A particular DHIS2 version | run DHIS2 2.41, 2.43 or the next, unreleased one |
| 10 | Climate data and DHIS2, no Chap | have OCS and a DHIS2 together, without Chap |
| Develop | ||
| 11 | A new model of my own | start a new model, in Python or R, from a template |
| 12 | My own model | run a model they wrote themselves |
| 13 | My own model, while I work on it | develop a model in its folder and have Chap use it |
| 14 | A model image I built | run a model image they built with docker build, without publishing it |
| 15 | My own chap-core, with the models | develop chap-core itself and have the models register with it |
| 16 | chap-core built from my checkout | test their chap-core changes with everything else around them |
| 17 | My own DHIS2, with Chap | connect a DHIS2 they run themselves to Chap from chap-devops |
| 18 | A DHIS2, with my own chap-core | have chap-devops run DHIS2 in front of the chap-core they are developing |
| More than one | ||
| 19 | Two of these at the same time | keep one option running while starting another |
| The chap CLI | ||
| 20 | Evaluate a model on my own data | run chap eval on their CSV without installing Python or uv |
| The chapkit CLI | ||
| 21 | Make my MLproject model a chapkit service | change a model with an MLproject file to a chapkit service, without installing Python or uv |
| 22 | Serve my MLproject model, with no build | evaluate and test a model with an MLproject file, without an image |
1. Chap in DHIS2, with the Modeling App
Needs 8 GB of memory for Docker. The DHIS2 comes with demo data from Laos: weekly dengue cases for 2019 to 2024 in 18 provinces, with rainfall, temperature and population.
chap-devops init mychap --models chapkit_simple_multistep_model --with dhis2
cd mychap
chap-devops doctor
chap-devops up
chap-devops status
Before chap-devops up, check the memory line of chap-devops doctor: 8 GB or more.
The first time, DHIS2 takes several minutes to start; the dhis2 line of
chap-devops status says starting until DHIS2 answers, then up. Then:
chap-devops dhis2 connect
chap-devops open dhis2
chap-devops dhis2 connect waits for DHIS2 to finish starting, then takes about a
minute: it connects DHIS2 to Chap, gives each model its configured models, installs
the Modeling App and the Climate App, and prepares the data the Modeling App
reads. Its last line is the Modeling App can reach Chap; open DHIS2 with `chap-devops open dhis2` . It worked when DHIS2 opens in the browser, the login admin /
district works, and typing Modeling in the app menu (the grid icon at the
top right) finds Modeling.
If chap-devops dhis2 connect says that a model is not registered with chap-core,
wait a minute. Then run chap-devops status. When it shows the model as
registered, run chap-devops models configs sync.
Next: Your first forecast in the Modeling App, which takes them from here to an evaluation and a forecast in about fifteen minutes.
More: Chap with a local DHIS2.
2. Chap with forecasting models
chap-core and one model, without DHIS2: less memory, but no user interface beyond chap-core’s API page.
chap-devops init mychap --models chapkit_simple_multistep_model
cd mychap
chap-devops up
chap-devops status
It worked when chap-devops status says chap-core is up and the line under the
table says the models are registered (1 model registered, or all 2 models registered with more). Models can take a minute or two after chap-devops up: if
some say not registered, wait a minute and run chap-devops status again. Then
chap-devops open chap-core opens chap-core’s API page in the browser.
Next: What to try next without DHIS2.
More: Chap with forecasting models.
3. Chap with climate data
chap-devops init mychap --models chapkit_simple_multistep_model --with ocs,s3
cd mychap
chap-devops up
chap-devops status
It worked when chap-devops status shows chap-core and ocs as up. Some climate
datasets need an account with Copernicus or the Earth Data Hub; the keys go in
the .env file in the folder, where they are already listed, commented out.
Add ,dhis2 to --with for the Modeling App as well (8 GB of memory), then
continue as in option 1 from chap-devops dhis2 connect. OCS covers Laos, like the
demo DHIS2, until the person changes it.
Next, when they want climate data: Getting climate data.
More: Chap with climate data from OCS.
4. Chap with every model
Like option 2, with every marketplace model. The model images are 1 to 7 GB
each. chap-devops init pulls one of them to read its user, and the first
chap-devops up pulls the others. Both can take many minutes on a slow connection.
chap-devops init mychap --models all
cd mychap
chap-devops up
chap-devops status
chap-devops models test --all
It worked when chap-devops status ends with all 4 models registered (one per
marketplace model; all leaves out the two starters) and
chap-devops models test --all ends with 4 of 4 models pass. When the
marketplace has more models, the numbers are higher.
The tests take one to two minutes. If some models say not registered, wait
a minute and run chap-devops status again.
After a plain chap-devops up, the table shows each model as
registered, not configured, and that is normal. chap-devops models test --all
does not need a configured model, but a backtest and the Modeling App do.
Run chap-devops models configs sync to create them. Then chap-devops status shows each
model as registered.
Add --with dhis2 to the chap-devops init line to compare them in the Modeling
App (8 GB of memory, then as in option 1 from chap-devops dhis2 connect, which
also creates the configured models).
More: Chap with forecasting models.
5. Chap for a DHIS2 that already exists
The person needs the DHIS2 address, an admin account on it, and an address at
which that DHIS2 can reach this computer (not localhost).
chap-devops init mychap --models chapkit_simple_multistep_model --api-token
cd mychap
chap-devops up
chap-devops dhis2 use https://THEIR-DHIS2 --chap-url https://ADDRESS-OF-THIS-MACHINE
chap-devops dhis2 connect
It worked when chap-devops dhis2 connect finishes without error:. It only sets
the route on their DHIS2: its skipped: lines name the apps to install and
the analytics run, which are for whoever runs that DHIS2 to decide (chap-devops dhis2 apps, chap-devops dhis2 analytics). The first time, chap-devops dhis2 use warns and
chap-devops dhis2 connect stops with error: chap-devops has no credentials for this DHIS2. Then ask the person to put DHIS2_ADMIN_USERNAME and
DHIS2_ADMIN_PASSWORD (or DHIS2_API_TOKEN) in the .env file in the
folder, and run chap-devops dhis2 connect again. If
--chap-url is an http:// address and the DHIS2 is 2.42 or later, it may say
DHIS2 refused the route: whoever runs that DHIS2 has to add the --chap-url
address, with no path, to route.remote_servers_allowed in its dhis.conf
and restart it; the error line names the exact value.
If chap-devops dhis2 connect says that a model is not registered with chap-core,
wait a minute. Then run chap-devops status. When it shows the model as
registered, run chap-devops models configs sync.
More: Chap for a DHIS2 that runs elsewhere.
6. Climate data only
chap-devops init climate --only ocs,s3
cd climate
chap-devops up
chap-devops status
chap-devops open ocs
It worked when chap-devops status shows ocs as up and the browser opens OCS.
To put some data in (public rainfall data, no account needed):
curl -X POST http://localhost:8790/ingestions -H 'Content-Type: application/json' -d '{"dataset_id": "chirps3_precipitation_daily", "start": "2024-01-01", "end": "2024-01-03", "publish": true}'
It worked when the answer contains "status":"completed".
Next: Getting climate data.
More: An OCS server on its own.
7. One forecasting model only
No folder needed: from any directory that is not a deployment,
chap-devops run chapkit_simple_multistep_model
It worked when the last lines say running chapkit_simple_multistep_model on http://localhost:5001 (answered in ...) and name the chap-devops stop that stops
it. With -a, chap-devops stays in the foreground and shows the model’s log, and
Ctrl-C stops the model. The model’s API is at that URL, and its documentation at /docs under it.
It is reachable from this machine only. A first run pulls the image, which can
take several minutes; the command waits for it. Other models: chap-devops models list shows their ids, and chap-devops run takes any of them, a GitHub repository
URL or a ghcr image the same way. chap-devops ps lists what runs, chap-devops top shows
it live, and chap-devops stop chapkit_simple_multistep_model stops it.
More than one works the same way: one chap-devops run per model, each on its own
port, and chap-devops ps lists them all. --group NAME keeps a set apart, and
chap-devops stop --group NAME --purge removes that set with its data.
For a folder of its own instead, with chap-devops status and the rest:
chap-devops init onemodel --only none --models chapkit_simple_multistep_model
cd onemodel
chap-devops up
chap-devops status
It worked when chap-devops status shows the model as up. Its API is at the
port in the REACH column, usually http://localhost:5001, and
chap-devops open chapkit_simple_multistep_model opens its documentation.
More: Running one model, A chapkit model service on its own, Any number of model services, no folder.
8. DHIS2 only
Needs 8 GB of memory for Docker. The first start takes several minutes.
chap-devops init dhis --only dhis2
cd dhis
chap-devops up
chap-devops status
Run chap-devops status every minute until the dhis2 line says up, then
chap-devops open dhis2. It worked when the login admin / district works. This
is DHIS2 2.42 with the Laos demo data; for another version, see option 9.
More: A DHIS2 on its own.
9. A particular DHIS2 version
Needs 8 GB of memory for Docker. Each version starts with an empty DHIS2
(only 2.42 has demo data, see option 8). chap-devops init says so in a
warning: chap-devops knows no DHIS2 demo dump line: that is expected. Run one of
these, not all three:
2.43,
chap-devops init dhis --only dhis2 --dhis2-tag 2.43
2.41,
chap-devops init dhis --only dhis2 --dhis2-tag 2.41
or the next DHIS2, not released yet:
chap-devops init dhis --only dhis2 --dhis2-image dhis2/core-dev --dhis2-tag master
Then:
cd dhis
chap-devops up
chap-devops status
Run chap-devops status every minute until the dhis2 line says up, then
chap-devops open dhis2. It worked when the login admin / district works. To
have Chap with it, use option 1 with the same --dhis2-tag (and
--dhis2-image) added to its chap-devops init line; without demo data there is
nothing to forecast until they import some.
More: A DHIS2 on its own.
10. Climate data and DHIS2, no Chap
Needs 8 GB of memory for Docker.
chap-devops init lab --only ocs,s3,dhis2
cd lab
chap-devops up
chap-devops status
Run chap-devops status every minute until every line says up, then
chap-devops open ocs and chap-devops open dhis2. It worked when both open, and the
DHIS2 login admin / district works.
More: Combinations.
11. A new model of my own
For someone who wants to write a new model. Ask which language they use: Python
or R. chap-devops models new makes the project. Docker builds it, so Python, uv
and R are not necessary on their machine.
chap-devops models new my-model
For R, add --template shell-r. For a model in the MLproject format, use
--template mlproject-py or --template mlproject-r.
It worked when the first line says created my-model in my-model. Then build
the image in the new folder:
cd my-model
git init && git add . && git commit -m "feat: first version"
docker build --platform linux/amd64 --build-arg GIT_REVISION=$(git rev-parse HEAD) -t my-model:dev .
It worked when the build ends with no error. The first build pulls the base image, which takes some minutes. Then run it with chap-core:
cd ..
chap-devops init mychap --models none
cd mychap
chap-devops models add my-model:dev
chap-devops up --wait
chap-devops models test my_model --backtest
It worked when the last line says 1 of 1 model passes. The model is in
main.py (the default type) or in the scripts/ folder. The README.md of
the project lists the files.
After a change, raise version in main.py (or in MLproject) first:
chap-core stores each version only once. Then commit, build the image again
with the same docker build line, and run chap-devops restart and the
chap-devops models test line in the mychap folder. If the test says
is stored from revision, the version was not raised.
More: A new model project.
12. My own model
The model has to be a chapkit model whose GitHub repository publishes a public
image on ghcr.io (most do, through their publish workflow). If chap-devops models add says the repository publishes no public image, use option 14 instead:
build the image from their checkout and add that.
To try it, with no folder:
chap-devops run https://github.com/THEIR-ORG/THEIR-MODEL
It worked when the last lines say running ... on http://localhost:.... To keep
it in a folder of its own:
chap-devops init mymodel --only none
cd mymodel
chap-devops models add https://github.com/THEIR-ORG/THEIR-MODEL
chap-devops up
chap-devops status
It worked when chap-devops status shows the model as up, on the port in its
REACH column. A repository the marketplace already lists is enabled as that
marketplace model, with its reviewed version; the line ... is the marketplace model ...; enabling that says so.
More: Models outside the marketplace.
13. My own model, while I work on it
For someone developing a chapkit model in its folder. chap-devops runs chap-core; they run the model from the folder.
chap-devops init mychap --models none
cd mychap
chap-devops up
Then, in the model’s folder (keep the single quotes):
export SERVICEKIT_ORCHESTRATOR_URL='http://localhost:8700/v2/services/$register'
export SERVICEKIT_HOST=host.docker.internal
export SERVICEKIT_PORT=8001
uv run uvicorn main:app --host 0.0.0.0 --port 8001
main:app is right for a model with main.py at the top of its folder, which
is how chapkit models are laid out. If main.py is inside a package folder
instead, use PACKAGE.main:app.
It worked when chap-devops status, run in the mychap folder, lists the model with
the state unmanaged and a LAST PING of a few seconds (unmanaged because chap-devops
did not start it, which is expected).
More: Your model from its checkout, with Chap.
14. A model image I built
In the model’s folder, which must be a git checkout:
docker build --platform linux/amd64 --build-arg GIT_REVISION=$(git rev-parse HEAD) -t my-model:dev .
GIT_REVISION gives the image the commit it was built from. chap-core needs
it to store the model template. Without it, chap-devops models configs sync stops
with answered HTTP 409 Conflict and is stored from revision None. Then:
chap-devops init mychap --models none
cd mychap
chap-devops models add my-model:dev
chap-devops up
chap-devops status
It worked when chap-devops status lists the model as registered. Usually it does
not the first time:
- Right after
chap-devops up, the model isrunning, not registered. Wait a minute and runchap-devops statusagain. - The model registers under the
idin itsMLServiceInfo(inmain.py), not asmy-model. Thenchap-devops statusshows that id asunmanaged, next tomy-modelasrunning, not registered. - The line under the table gives the two commands that fix it:
chap-devops models remove my_model, thenchap-devops models add my-model:dev --service-idwith the id from theunmanagedrow. Run them, then runchap-devops upagain. - When the model is
registered, not configured, runchap-devops models configs sync.
If chap-devops status or chap-devops models configs sync says that the model reports no
git revision, build the image again with the GIT_REVISION line above. Then
run chap-devops restart and chap-devops models configs sync.
More: A model image you built yourself.
15. My own chap-core, with the models
For someone developing chap-core itself. They run chap-core from its folder,
listening on 0.0.0.0:8000; chap-devops runs the models. Have them start their
chap-core first: chap-devops init then sees whether it runs in Docker and
configures the models to match.
chap-devops init models --chap-core-url http://localhost:8000 --models chapkit_simple_multistep_model
cd models
chap-devops up
chap-devops status
It worked when chap-devops status shows chap-core up and the model registered.
Right after chap-devops up, the model is running, not registered; run
chap-devops status again after some seconds. If the model is
registered, not configured, run chap-devops models configs sync.
If chap-core was started after chap-devops up, the model stays
running, not registered: it tries to register for about ten seconds only.
Two minutes after chap-devops up, the line under the table gives
chap-devops restart --all chapkit-simple-multistep-model. Run it.
registered, unreachable means chap-core cannot call the model back, usually
because chap-core was not running during chap-devops init. If their chap-core
runs in a container, the line under the table gives the two commands that fix
it: chap-devops components enable chap-core with
--models-host host.docker.internal, then chap-devops up.
More: chap-core from its checkout, with the models.
16. chap-core built from my checkout
For someone changing chap-core who wants to run their version with models. Replace the path with where they cloned chap-core:
chap-devops init mychap --source ~/dev/chap-core --models chapkit_simple_multistep_model
cd mychap
chap-devops up
chap-devops status
The first chap-devops up builds chap-core and takes several minutes (longer on an
Apple Silicon Mac). It worked when chap-devops status shows chap-core up and the
models registered. After changing the code, chap-devops up again rebuilds it.
More: chap-core built from its checkout.
17. My own DHIS2, with Chap
For someone who already runs DHIS2 on this machine (for DHIS2 or app work). Ask whether their DHIS2 runs in Docker. If it does not:
chap-devops init mychap --models chapkit_simple_multistep_model
cd mychap
chap-devops up
chap-devops dhis2 use http://localhost:8080 --chap-url http://localhost:8700
chap-devops dhis2 connect
If it runs in Docker, it reaches Chap through host.docker.internal:
chap-devops init mychap --models chapkit_simple_multistep_model
cd mychap
chap-devops up
chap-devops dhis2 use http://localhost:8080 --chap-url http://host.docker.internal:8700
chap-devops dhis2 connect
It worked when chap-devops dhis2 connect finishes without error:. If it says it
has no credentials, put DHIS2_ADMIN_USERNAME and DHIS2_ADMIN_PASSWORD (or
DHIS2_API_TOKEN) in the .env file and run it again. On a DHIS2 chap-devops did not start it sets only
the route: if its skipped: lines say an app is missing, chap-devops dhis2 apps
installs it, and chap-devops dhis2 analytics generates the tables the Modeling App
reads.
On DHIS2 2.42 and later it can then say DHIS2 refused the route: their
DHIS2 only proxies to the addresses its own dhis.conf allows. Tell them to
add the --chap-url address, with no path (for example
route.remote_servers_allowed = https://*,http://localhost:8700), to their
DHIS2’s dhis.conf, restart their DHIS2, and run chap-devops dhis2 connect again.
A value with a path, such as http://localhost:8700/**, stops DHIS2 from
starting at all.
More: A DHIS2 you run yourself, with Chap from chap-devops.
18. A DHIS2, with my own chap-core
For someone developing chap-core who wants DHIS2 and the Modeling App in front
of it. Their chap-core must listen on 0.0.0.0:8000. Needs 8 GB of
memory for Docker.
chap-devops init lab --chap-core-url http://localhost:8000 --with dhis2
cd lab
chap-devops up
chap-devops status
Then, once chap-devops status shows chap-core up (chap-devops dhis2 connect waits
for DHIS2 by itself):
chap-devops dhis2 connect
chap-devops open dhis2
It worked when chap-devops dhis2 connect finishes without error: and the login
admin / district works.
More: A DHIS2 from chap-devops, with a chap-core elsewhere.
19. Two of these at the same time
Every option uses the same ports, so a second one needs its own. Create the
second one with these added to its chap-devops init line, one flag per piece it
has (--ocs-port only with OCS, --dhis2-port only with DHIS2; a flag for a
missing piece is an error):
| Piece | Flag |
|---|---|
| chap-core | --api-port 8701 |
| models | --port-base 5101 |
| OCS | --ocs-port 8791 |
| DHIS2 | --dhis2-port 8781 |
For example, a second option 2: chap-devops init second --models chapkit_simple_multistep_model --api-port 8701 --port-base 5101.
For a third, use 8702, 8792, 8782 and 5201, and so on. It worked when both
folders’ chap-devops status say up. Running them one at a time needs none of
this; see the note below.
More: Several deployments on one machine.
20. Evaluate a model on my own data
They have a dataset as a CSV, with a GeoJSON of the same name beside it. Do not install Python or uv for them. In the directory with the data:
chap-devops chap eval --model-name https://github.com/dhis2-chap/minimalist_example_r \
--dataset-csv data.csv --output-file eval.nc
chap-devops chap plot-backtest eval.nc --output-file eval.html
Use their model’s GitHub URL in --model-name, or a marketplace id such as
auto_arima_chapkit: chap-devops starts that model by itself, and they need no
chap-devops up. Never give a localhost URL. The first run pulls
an image of about 12 GB, so tell them it takes some minutes. If chap-devops stops with
runs in docker, run the same command with chap-devops chap --docker and tell
them that the flag gives the container control of docker. If the directory of
an output file is not there, chap-devops creates it.
It worked when the last lines say chap finished; it wrote eval.nc and
chap finished; it wrote eval.html. They open eval.html in a browser.
More: Evaluating a model on your own data, which goes from installing chap-devops to a comparison of two models, and The chap CLI.
21. Make my MLproject model a chapkit service
They have a model directory with an MLproject file. Do not install Python or
uv for them. Make sure that the directory is in git, or that they have a copy:
the migration moves files to _old/. In the directory above the model:
chap-devops chapkit mlproject migrate my-model --yes
It worked when the last lines say chapkit finished; it wrote ... and
wrote my-model/uv.lock with `uv lock` in the same image. If the second line
is a warning that chap-devops could not write uv.lock, the build fails; show them
the warning.
If chapkit stops with No Python (.py) or R (.r/.R) scripts, run the command
again with --base-image chapkit-py for Python. For R, use chapkit-r,
chapkit-r-tidyverse or chapkit-r-inla.
Then build the image and test it:
cd my-model
docker build --platform linux/amd64 --build-arg GIT_REVISION=$(git rev-parse HEAD) -t my-model:dev .
chap-devops run my-model:dev
chap-devops -C ~/.local/share/chap-devops/run/default models test my_model
The build needs a git commit for GIT_REVISION. It worked when the last line
says 1 of 1 model passes.
More: The chapkit CLI.
22. Serve my MLproject model, with no build
They have a model directory with an MLproject file. Do not install Python,
R or uv for them. In the directory above the model:
chap-devops chapkit mlproject run my-model
It worked when the line says the MLproject service answers at http://localhost:PORT. The service runs in the foreground. Leave it running,
and use a second terminal for the next steps. Ctrl-C stops it.
If the train job stops with No module named, the image does not have a
package that the model needs. Run chap-devops chapkit --image, and show them the
list. Then run the command again with --image NAME.
To evaluate it, give the same port at host.docker.internal:
chap-devops chap eval --model-name http://host.docker.internal:PORT \
--dataset-csv laos_subset.csv --output-file my-model.nc
It worked when the line says chap finished; it wrote my-model.nc. For the
data, see option 20.
To test it through chap-core, commit the model directory first. Then start
it from a deployment directory, such as mychap from option 2, with
chap-devops chapkit mlproject run ../my-model. In a second terminal, in that
deployment directory:
chap-devops models configs sync my-model
chap-devops models test my-model --backtest
Use the name in MLproject as the id. It worked when the last line says
1 of 1 model passes.
More: An MLproject model, with no image to build.
What to try next without DHIS2
Option 2 has no user interface, but the same evaluation the Modeling App runs
works from the terminal. In the mychap folder:
chap-devops models test --all --backtest
chap-devops jobs
The first line creates the configured models that chap-core needs, then makes
every enabled model train, forecast and run an evaluation through chap-core on
sample data the model generates itself, and prints the scores. Under each
row, the line configured model: NAME (id N) names the configured model that
the backtest used. For Simple Multistep
it takes under a minute. If every model was skipped, it says warning: no model was tested and exits non-zero; the line under each row says why. chap-devops jobs lists what
chap-core did. To see it in the Modeling App instead, add DHIS2 to the same
folder (8 GB of memory for Docker):
chap-devops components enable dhis2
chap-devops up
then continue as in option 1 from chap-devops dhis2 connect. See
Growing a deployment.
Getting climate data
For options 3, 6 and 10, once OCS is up. OCS covers Laos until the person
changes the extent (the --ocs-name, --ocs-country and --ocs-bbox flags of
chap-devops init). Six years of monthly rainfall, public, no account needed; it
takes a few minutes and the command waits:
curl -X POST http://localhost:8790/ingestions -H 'Content-Type: application/json' -d '{"dataset_id": "chirps3_precipitation_monthly", "start": "2019-01", "end": "2024-12"}'
It worked when the answer contains "status":"completed".
curl http://localhost:8790/data-sources lists every other dataset;
temperature (era5land_temperature_monthly) needs a Copernicus account, whose
key goes in .env. To get the values out per province as the CSV Chap reads,
or as a NetCDF file, follow
Getting it out step by step.
Every option: stop, start, remove
chap-devops down # stop everything; the data is kept
chap-devops up # start it again
chap-devops down --volumes --yes # stop and delete all the data
After chap-devops down --volumes --yes the folder can be deleted.
All the options use the same ports, so only one of them runs at a time
(option 19 is the exception). While another deployment is up, chap-devops init
warns that a port is already in use on this machine. You can ignore that
warning if you stop the other one before chap-devops up.
When chap-devops up says a port is used by another deployment and asks whether to
stop it, answer y. The other one keeps its data, and chap-devops up in its
folder starts it again. chap-devops up --replace is the same y given in advance.
Without a terminal, chap-devops up does not ask. It stops with
error: 1 host port this deployment needs is already in use; nothing was started
and names chap-devops up --replace; ask the person before you run it.
While the other one is up, chap-devops status in this folder says this
deployment’s chap-core is not running and names the one that is.
Install
The one-liner
On macOS and Linux:
curl -fsSL https://raw.githubusercontent.com/winterop-com/chap-devops/main/install.sh | sh
Or, to get the binary and nothing else - into this directory as ./chap-devops, no
PATH advice, no completion scripts, nothing written anywhere outside it:
curl -fsSL https://raw.githubusercontent.com/winterop-com/chap-devops/main/install.sh | sh -s -- --here
If the script printed ... is not on your PATH, add the export PATH=...
line it printed to your shell profile and open a new shell.
Then run chap-devops doctor (./chap-devops doctor after --here): it checks in one
pass that this machine has everything a deployment needs - Docker, Compose
2.24.4 or newer, disk, and a route to the hosts Chap pulls from - and says what
to do about anything it did not find. See Doctor.
The script prints the release, the platform, the archive URL and the install
path, then the steps. On macOS with --here it ends like this:
downloading chap-devops-universal-apple-darwin.tar.gz
verifying the checksum
unpacking
installed /Users/you/chap-devops-test/chap-devops
chap-devops 0.100.1
The script works out the platform from uname, downloads the release archive
for it, checks the download against the release’s SHA256SUMS and installs the
binary. It refuses to install anything it could not verify.
Next to chap-devops, it puts the link cdx, the short form of the command: cdx up
is the same as chap-devops up. chap-devops self update replaces the binary, and the
link then points at the new one. --here installs the binary alone, with no
link.
If the install folder already has a cdx that is not this link, the script
leaves it alone and says so. Two small tools also install a command named
cdx: the cdx crate (text tools, with cargo install cdx) and the cdx
package on PyPI (directory bookmarks). If you need one of them, delete the
link and use chap-devops.
It goes into /usr/local/bin when that directory is writable and
~/.local/bin otherwise. It makes the directory if it is not there. If the
directory is not on your PATH, it prints the export PATH=... line to add to
your shell profile. Pipe it through sudo sh to take the first branch on a
machine where /usr/local/bin needs root.
Without --here, the script also copies the completion scripts into the
directories of bash, zsh and fish that already exist in your home (see
Shell completions). This is also true with --dir. It
prints a completions line for each file it wrote. Only --here writes
nothing outside the current directory.
| Option | Environment variable | What it does |
|---|---|---|
--version TAG | CHAP_DEVOPS_VERSION | Install that release instead of the newest. dev is the rolling build of main |
--dir DIR | CHAP_DEVOPS_INSTALL_DIR | Install chap-devops and cdx into that directory |
--here | CHAP_DEVOPS_INSTALL_DIR=. | Put ./chap-devops in the current directory and do nothing else |
--dry-run | Print the release, the archive and the install path, and change nothing | |
--help | Print the options |
curl -fsSL https://raw.githubusercontent.com/winterop-com/chap-devops/main/install.sh |
CHAP_DEVOPS_INSTALL_DIR="$HOME/bin" CHAP_DEVOPS_VERSION=v0.100.0 sh
The oldest release that has chap-devops archives is v0.100.0. For an older tag,
the script stops with could not download ..., and chap-devops self update --version stops with an error that names the archive it did not find:
error: v0.2.1 has no chap-devops-universal-apple-darwin.tar.gz, so chap-devops cannot install it on aarch64-apple-darwin; pick a newer tag with --version, or run `chap-devops self update` for the newest
Use v0.100.0 or newer. Without --version, the error ends in download a build from https://github.com/winterop-com/chap-devops/releases.
--here and --dir together are an error; use one of them.
Without --version the newest release is looked up through the GitHub API, and
through the redirect of the releases/latest page when the API rate limit has
been reached. Unauthenticated API calls are limited to 60 an hour per address,
which a CI runner can exhaust.
Windows
On Windows, chap-devops runs inside WSL. Install WSL 2 and Docker Desktop, turn on
Docker Desktop’s WSL integration for your distribution (Settings, Resources,
WSL integration), then open the WSL shell and follow the Linux steps above:
the one-liner installs the Linux binary, and chap-devops doctor checks that docker
answers from inside WSL.
The release also carries native Windows builds (chap-devops-*-pc-windows-msvc.zip
in the table below). They are published untested: CI runs on Linux and macOS
only. WSL with the Linux binary is the supported way on Windows.
Which version
There are two series, and a binary knows which one it belongs to.
Stable is a vX.Y.Z tag: a release that was cut on purpose, with notes,
and it never changes once published. It is what the one-liner installs, what
chap-devops self update follows, and what the download links point at.
dev is the rolling build of main, republished under the single moving
tag dev on every push. It is the same seven archives, built and signed the
same way, from a commit that has passed CI and nothing more: no release notes
worth the name, no promise that anything in it stays. It reports the version
of the last tag, so chap-devops --version says chap-devops 0.100.1 on a dev build as
well; chap-devops self version is what tells the two apart:
version v0.100.1
revision b95828a
channel dev
target aarch64-apple-darwin
path /usr/local/bin/chap-devops
installed by release archive
target is the slice that runs. A universal macOS binary on Apple silicon
shows aarch64-apple-darwin.
channel is always printed, stable or dev, and is in --json under the
same name. path is the chap-devops file itself, also when you run it through the
link cdx. A binary built anywhere else, cargo install included, is stable.
Picking one with the installer:
# the newest stable release
curl -fsSL https://raw.githubusercontent.com/winterop-com/chap-devops/main/install.sh | sh
# the rolling build of main
curl -fsSL https://raw.githubusercontent.com/winterop-com/chap-devops/main/install.sh | sh -s -- --version dev
# one named release
curl -fsSL https://raw.githubusercontent.com/winterop-com/chap-devops/main/install.sh | sh -s -- --version v0.100.0
and with an installed chap-devops:
chap-devops self update # the newest build of the channel this one is on
chap-devops self update --version dev # cross over to the rolling build
chap-devops self update --version v0.100.1 # go back to a stable release
chap-devops self update on a stable build follows releases/latest, which
excludes pre-releases, so a stable install is never moved onto a dev build by
itself. On a dev build the same command follows the dev release, and
compares the commit it was built from rather than the version number, since
the version does not move between two rolling builds. --version TAG always
wins over both.
Downloading an archive
Each release attaches an archive per platform. These links always point at the newest release:
| Platform | Download | Notes |
|---|---|---|
| Linux x86_64 | chap-devops-x86_64-unknown-linux-musl.tar.gz | static, any distro |
| macOS | chap-devops-universal-apple-darwin.tar.gz | universal, signed and notarized |
| Windows x86_64 | chap-devops-x86_64-pc-windows-msvc.zip | unsigned, untested |
Other platforms
| Platform | Download | Notes |
|---|---|---|
| Linux arm64 | chap-devops-aarch64-unknown-linux-musl.tar.gz | static, any distro |
| macOS, Apple silicon | chap-devops-aarch64-apple-darwin.tar.gz | signed and notarized |
| macOS, Intel | chap-devops-x86_64-apple-darwin.tar.gz | signed and notarized |
| Windows arm64 | chap-devops-aarch64-pc-windows-msvc.zip | unsigned, untested |
An archive is named after the platform alone, with no version in it: the
release it belongs to is the one in the URL, so a link written down today
still resolves after the next release. A release attaches these seven archives
and one SHA256SUMS covering all of them, and nothing else.
An archive holds one directory, chap-devops-<version>-<target>/, containing the
chap-devops binary, README.md, LICENSE and a completions/ directory. In a
dev archive that version is dev-<short commit>, which is the one place the
commit shows up without running the binary.
The Linux builds are static musl binaries, so one file runs on any
distribution with no shared library to match. Linux and macOS ship as
tar.gz, Windows as zip.
On macOS take the universal archive unless you have a reason to pick a single architecture: it carries the Apple silicon and Intel slices in one binary and runs on any Mac.
On macOS:
curl -fsSLO https://github.com/winterop-com/chap-devops/releases/latest/download/chap-devops-universal-apple-darwin.tar.gz
tar -xzf chap-devops-universal-apple-darwin.tar.gz
sudo install -m 0755 chap-devops-*-universal-apple-darwin/chap-devops /usr/local/bin/chap-devops
On a Linux server:
curl -fsSLO https://github.com/winterop-com/chap-devops/releases/latest/download/chap-devops-x86_64-unknown-linux-musl.tar.gz
tar -xzf chap-devops-x86_64-unknown-linux-musl.tar.gz
sudo install -m 0755 chap-devops-*-x86_64-unknown-linux-musl/chap-devops /usr/local/bin/chap-devops
On an arm64 server, write aarch64 in place of x86_64 in the three lines.
The commands do not add the link cdx. To add it, run
sudo ln -s chap-devops /usr/local/bin/cdx.
Verifying a download
Every release carries a SHA256SUMS file covering every archive:
Download it into the directory that has the archive, then check:
curl -fsSLO https://github.com/winterop-com/chap-devops/releases/latest/download/SHA256SUMS
shasum -a 256 -c SHA256SUMS --ignore-missing
It prints one line for the archive that is there:
chap-devops-universal-apple-darwin.tar.gz: OK
On Linux, use sha256sum -c SHA256SUMS --ignore-missing; many distributions
have no shasum. The sha256sum of BusyBox (Alpine, small containers) has no
--ignore-missing. There, check the one line of your archive:
grep ' chap-devops-x86_64-unknown-linux-musl.tar.gz$' SHA256SUMS | sha256sum -c -
The macOS binaries are signed with an Apple Developer ID certificate and
notarized by Apple, so Gatekeeper lets them run: no right-click to open, no
xattr -d com.apple.quarantine. To see who signed one:
codesign -dv --verbose=4 /usr/local/bin/chap-devops
The Authority=Developer ID Application: ... line names the signer. To see
that Apple notarized it:
spctl -a -vvv -t install /usr/local/bin/chap-devops
It prints accepted and source=Notarized Developer ID.
Apple serves the notarization ticket online rather than it being stapled to the file, because a bare executable has nowhere to carry a ticket. The first run on a machine therefore wants to reach Apple once; after that the verdict is cached.
The native Windows binaries are untested and not signed, which would need an
Authenticode certificate. SmartScreen may warn the first time chap-devops.exe runs;
“More info” then “Run anyway” gets past it. There SHA256SUMS is the check
that matters. Under WSL none of this applies: it is the Linux binary.
Keeping it up to date
chap-devops self update # replace this binary with the newest release
chap-devops self update --check # say whether there is one, change nothing
chap-devops self version # version, revision, channel, target, path, install method
chap-devops self update looks up the release, downloads the archive built for this
target (on macOS the universal one, whichever slice you installed), checks it
against SHA256SUMS and renames the new binary over the running one. The
installed file is never written through, so a download that fails or does not
verify leaves what you have exactly as it was.
--version TAG installs a named release, going backwards included, and dev
is a tag like any other. --yes skips the question
replace ... with chap-devops vX.Y.Z? [y/N]. chap-devops does not ask under --json, or
when stdin or stdout is not a terminal. --json reports the result as a
document, with channel alongside current and latest. If the binary lives
somewhere you cannot write, chap-devops says so and suggests sudo or an install
directory of your own rather than half-replacing itself.
A successful update prints:
updated chap-devops: v0.100.0 -> v0.100.1
With -v, a hint names the file: hint: replaced /usr/local/bin/chap-devops.
--check on a binary that is up to date prints
chap-devops v0.100.1 is up to date. With --version dev or another tag, --check
reports that release:
chap-devops dev b95828a (2026-10-07) is available (you have v0.100.1)
hint: the download is chap-devops-universal-apple-darwin.tar.gz
hint: `chap-devops self update --version dev` installs it
The hints come with -v. They name the same --version TAG that was
checked, because a plain chap-devops self update follows the channel of this
build. The day of a dev build is the day it was built, from the release notes
or the newest asset. If chap-devops finds no date, the line says chap-devops dev b95828a
with no day.
Without --version, an update follows the channel this build is on: the
newest tag for a stable build, the newest build of main for a dev one. See
Which version.
Once a day, after a command that succeeded, chap-devops asks the release feed
whether there is a newer version and prints one dimmed line on stderr if there
is:
chap-devops v0.100.1 is available (you have v0.100.0): run `chap-devops self update`
On a dev build the same notice compares commits, because the version number does not move between two rolling builds:
a newer chap-devops dev build is available (commit 9f3a2c1, you have bff294c): run `chap-devops self update`
It stays quiet unless it can see that the commit moved, so a build with no recorded revision is never nagged about nothing.
The check has a two-second timeout, its answer is cached in
self-update-check.json in the cache directory ($CHAP_DEVOPS_CACHE_DIR, else
$XDG_CACHE_HOME/chap-devops, else ~/.cache/chap-devops, also on macOS), and any failure
is ignored: it can never
change what a command did or what it exited with. It is skipped under --json,
under --offline, when stdout is not a terminal, and for chap-devops self itself.
CHAP_DEVOPS_NO_UPDATE_CHECK=1 turns it off entirely.
A chap-devops installed with cargo install is replaced the same way, but
cargo install --path . from an updated checkout is the more honest way to
move that one on. installed by in chap-devops self version tells which one you
have:
release archivefor a binary that the release workflow built, wherever it is now.cargo installfor a local build in a.cargo/bindirectory.cargo buildfor a local build in atarget/releaseortarget/debugdirectory.local buildfor a local build at any other path, also formake installand forcargo install --root DIR.
Shell completions
Every release archive ships the scripts for bash, zsh, fish and PowerShell
under completions/ (chap-devops.bash, _chap-devops, chap-devops.fish, _chap-devops.ps1). The
install script copies the first three into place when the directory a shell
reads already exists:
~/.local/share/bash-completion/completions/chap-devops(or under$XDG_DATA_HOME)~/.zsh/completions/_chap-devops~/.config/fish/completions/chap-devops.fish(or under$XDG_CONFIG_HOME)
When the script adds the link cdx, it also writes the bash and fish script a
second time under the name cdx (completions/cdx and cdx.fish). bash and fish
load a completion file by the name of the command, so cdx then completes
before chap-devops was completed once. zsh needs nothing more, because _chap-devops
starts with #compdef chap-devops cdx.
chap-devops completions <shell> prints one to stdout for the cases it does not
cover: a checkout, a different directory, or a shell that reads its completions
from somewhere else. The shells are bash, zsh, fish, powershell and
elvish. The scripts complete chap-devops and the link cdx.
bash, with the bash-completion package installed (on macOS,
bash-completion@2 from Homebrew):
mkdir -p ~/.local/share/bash-completion/completions
chap-devops completions bash > ~/.local/share/bash-completion/completions/chap-devops
chap-devops completions bash > ~/.local/share/bash-completion/completions/cdx
zsh, into a directory that is on your fpath. If it is a new directory, add
fpath=(~/.zsh/completions $fpath) before compinit in ~/.zshrc:
mkdir -p ~/.zsh/completions
chap-devops completions zsh > ~/.zsh/completions/_chap-devops
fish:
mkdir -p ~/.config/fish/completions
chap-devops completions fish > ~/.config/fish/completions/chap-devops.fish
chap-devops completions fish > ~/.config/fish/completions/cdx.fish
PowerShell, appended to your profile:
chap-devops completions powershell | Out-String | Invoke-Expression
elvish:
mkdir -p ~/.config/elvish/lib
chap-devops completions elvish > ~/.config/elvish/lib/chap-devops.elv
Then add use chap-devops to ~/.config/elvish/rc.elv.
The scripts are generated from the same command tree --help is rendered
from, so they are current for the binary that printed them. Regenerate them
after an update.
From a checkout
A checkout needs Rust 1.91 or newer. In the root of the checkout, run:
cargo install --path .
This builds the binary and puts it in ~/.cargo/bin (or $CARGO_HOME/bin).
cargo build --release builds it into target/release/chap-devops and installs
nothing.
Or build and install through the Makefile, which puts the binary in
$PREFIX/bin with PREFIX defaulting to ~/.local:
make install
To install into another directory, set PREFIX, for example
make install PREFIX=/usr/local.
On macOS make release builds a universal (arm64 plus x86_64) binary at
bin/chap-devops first, and make install copies that. It needs the two Rust
targets; if one is missing, it stops and prints the command to add it:
rustup target add aarch64-apple-darwin x86_64-apple-darwin. On Linux it
builds the host binary. make install also adds the link cdx, as the install script does;
cargo install installs chap-devops alone. See Development for
the rest of the targets.
chap-devops is released as binaries on GitHub, not on crates.io: publish = false in
Cargo.toml makes a cargo publish fail at once.
Requirements
On the machine that runs Chap: Docker, with Compose v2.24.4 or newer. That
version is where the !override YAML tag arrived, and every compose.chap-devops.yml
chap-devops sync writes uses it to replace chap-core’s own port mapping rather than
add to it; see Ports. The other requirement is older: include:,
which chap-devops uses for the umbrella file that names one overlay per enabled
model, arrived in 2.20.
Nothing else is required. No Python, no uv, no checkout of chap-core. This
is also true for chap-core’s own chap CLI: chap-devops chap runs it in a
container. See The chap CLI. The same is true for chapkit’s
CLI: chap-devops chapkit runs it in the chap-core image. See
The chapkit CLI.
chap-devops checks the installed Compose version and warns on stderr when it is
older than 2.24.4 rather than failing, because an older Compose still runs most
of Chap. Between 2.20 and 2.24.4 the model overlays load and the API ends up
published on two ports, its own and the override’s; below 2.20 the model
overlays are the part that will not load at all.
A note on the name
The binary is chap-devops, not chap, because chap-core’s own Python package
already installs a console script called chap. The two never collide, so a
machine can have both.
Quickstart
0. Check the machine
After installing, run:
chap-devops doctor
ok docker cli docker 29.8.2
ok docker daemon Docker Engine 29.8.2
ok docker compose v5.5.1
warn os and arch macos/aarch64; Chap images are linux/amd64 only, so they run under emulation
nothing to do: Rosetta runs the amd64 images, they are only slower
ok disk space 583.7 GB free on /srv
ok leftovers no volumes left by removed deployments
ok network ghcr.io reachable (HTTP 401)
ok network marketplace reachable (HTTP 200)
ok network releases reachable, newest chap-core is v2.4.0
ok github api reachable, 60 of 60 requests left this hour (no token; set GITHUB_TOKEN for 5000)
ok chap-devops v0.100.1, aarch64-apple-darwin, release archive
11 checks: 10 ok, 1 warn, 0 fail
There is one line per check: Docker, Compose, the CPU architecture, free disk,
and the hosts Chap pulls from. A check that is not ok has a second line that
tells you what to do. The run above is on a Mac, so the architecture check is
a warn. Inside a deployment directory, doctor also checks the deployment.
See Doctor.
1. Write the deployment directory
chap-devops init mychap --models chapkit_simple_multistep_model
created a deployment in /srv/mychap
components: chap-core
chap-core: v2.4.0, API on http://localhost:8700
enabled chapkit_simple_multistep_model v0.1.2 at http://localhost:8700/v2/services/chapkit-simple-multistep-model/run/
run `cd /srv/mychap && chap-devops up` to start the deployment
init writes the compose files, .env and the state in .chap-devops/, and starts
nothing. With -v, it also names each file it wrote.
Model services publish no host port. chap-core reaches them over the compose
network, and you reach them through chap-core at the URL on the enabled
line. That route accepts only GET (and HEAD) requests, for example
/api/v1/info. For anything else, chap-devops models expose ID publishes a port of
the model’s own (step 5).
chap-devops init mychap --with ocs adds Open Climate Service beside chap-core, and
--with ocs,s3 adds the object store with it. --with dhis2 adds a demo or
development DHIS2 and a PostgreSQL of its own; its first start takes minutes
rather than seconds, and DHIS2 says why and what else to know
first. See Components.
--models chapkit_simple_multistep_model enables Simple Multistep on its
stable channel, and --models a,b takes a list of marketplace ids. Without
--models, init enables no model and says no models are enabled; run \chap-devops models enable ID` to add one. –interactiveopens the model browser instead of reading–models`.
A model the marketplace does not list yet is added from its repository or
image: chap-devops models add https://github.com/my-org/chapkit_dengue_model (see
Models outside the marketplace).
chap-core itself is pinned to the newest release, v2.4.0 in the run above,
and init takes the compose.ghcr.yml that release publishes as the base
of the deployment. --chap-tag picks another tag; see Updating.
The deployment above has no authentication: anything that can reach the port can
use the API. chap-devops init mychap --models chapkit_simple_multistep_model --api-token generates a token
and protects the API with it. chap-devops auth enable does the same to a deployment
that already exists. See Authentication.
2. Start it
cd mychap
chap-devops up
...
Container mychap-dabd2e-chapkit-simple-multistep-model-1 Started
started chap, chapkit-simple-multistep-model, postgres, redis, worker
The first up on a machine pulls the images: about 6 GB to download and
about 20 GB on disk. The chap-worker image alone is 3 GB. On a slow line this
can take close to an hour (47 minutes in one test run). Compose shows
the pull progress while it works. A second deployment uses the same images and
starts in seconds.
up renders the compose files from .chap-devops/ first, then checks that every
host port Chap is about to publish is free, then calls
docker compose up -d. The last line names the services it started; a
recreated container counts as started. With -v, a hint also names the
services it left alone.
up returns when compose has started the containers, not when chap-core
answers. chap-devops up --wait returns only once chap-core, every model, OCS and
DHIS2 answer. It also gives the models their configured models (step 3).
Every command that operates on a deployment finds its directory the way git
finds .git: from the current directory (or -C DIR) upwards to the nearest
.chap-devops/, so chap-devops up works from any subdirectory of the deployment.
3. Check it
chap-devops status
chap-core up http://localhost:8700 2.4.0 auth: off
MODEL STATE REACH LAST PING
chapkit-simple-multistep-model registered, not configured via chap-core 1s ago
1 model registered
chapkit-simple-multistep-model: chap-core has no configured model for it, so nothing can run it; run `chap-devops models configs sync`
registered, not configured is the normal state after a plain chap-devops up.
chap-core v2.4.0 makes no configured model when a model registers. A backtest
and the Modeling App need one. Make them:
chap-devops models configs sync
chapkit_simple_multistep_model: created configured model monthly_climate
chapkit_simple_multistep_model: created configured model monthly_selfhistory
chap-devops up --wait does the same step for you. See
Configured models. Now chap-devops status shows
the model as registered:
chap-core up http://localhost:8700 2.4.0 auth: off
MODEL STATE REACH LAST PING
chapkit-simple-multistep-model registered via chap-core 7s ago
1 model registered
With -v, status also gives the route through chap-core and the next
command to try:
hint: models without a host port are reachable through chap-core at http://localhost:8700/v2/services/<id>/run/
hint: run `chap-devops models test --all` to check it can run
A model can also be running, not registered or not running, and then
status names the command that fixes it. A model that started a moment ago
is running, not registered until it is ready (step 5 shows this).
On a deployment whose containers do not exist at all, status skips the table
and says so:
Chap is not running; start it with `chap-devops up`
Status and output explains every column and every state.
4. Add a model
chap-devops ui
The browser shows the catalogue as one table, with a one-line summary of the
selected row under it. space enables or disables, p publishes a host port,
v switches channel, and s saves. Enter or i shows the full entry, and
q quits. Nothing is written until you save. See
The browser.
The same thing without the browser:
chap-devops models search arima
chap-devops models info auto_arima_chapkit
chap-devops models enable auto_arima_chapkit --channel stable
enable changes only the files:
enabled auto_arima_chapkit v1.0.2 at http://localhost:8700/v2/services/auto-arima-chapkit/run/
run `chap-devops up` to apply
The STATUS column of search tells you how ready a model is. No model in the
marketplace is production yet. Simple Multistep is limited data: it shows
promise, but needs careful evaluation. Auto-ARIMA is experimental. Read
chap-devops models info before you enable a model for real work.
Start the model:
chap-devops up
chap-devops status
up says started auto-arima-chapkit. Right after it, the new
model is not registered yet, and status exits with 1:
chap-core up http://localhost:8700 2.4.0 auth: off
MODEL STATE REACH LAST PING
auto-arima-chapkit running, not registered via chap-core -
chapkit-simple-multistep-model registered via chap-core 1s ago
1 of 2 models is not registered.
auto-arima-chapkit: started under two minutes ago and registers once it is ready; run `chap-devops status` again in a minute
In the test run, the model registered within 10 seconds. Then it is
registered, not configured, as in step 3. Give it its configured models:
chap-devops models configs sync
auto_arima_chapkit: created configured model monthly
chapkit_simple_multistep_model: chap-core has a configured model of it already
chap-devops status now shows both models as registered:
chap-core up http://localhost:8700 2.4.0 auth: off
MODEL STATE REACH LAST PING
auto-arima-chapkit registered via chap-core 4s ago
chapkit-simple-multistep-model registered via chap-core 6s ago
all 2 models registered
If you run chap-devops models configs sync before the model registers, it says so
and tells you to run it again later.
chap-devops models list shows the whole catalogue and what this deployment
enabled:
ID SERVICE NAME STATUS STABLE LATEST PORT
chapkit_simple_multistep_model chapkit-simple-multistep-model Simple Multistep limited data 0.1.2 0.1.2 via chap-core
auto_arima_chapkit auto-arima-chapkit Auto-ARIMA experimental 1.0.2 1.0.2 via chap-core
chapkit_ghr_model chapkit-ghr-model GHRmodel experimental 0.1.3 0.1.3 -
chapkit_rwanda_malaria_bym_model chapkit-rwanda-malaria-bym-model Rwanda Malaria BYM not for use 0.1.3 0.1.3 -
4 listed, 2 enabled in this deployment
5. Reach a model from your own machine
Model services publish no host port of their own. Either go through chap-core, which accepts only GET (and HEAD) requests:
curl http://localhost:8700/v2/services/chapkit-simple-multistep-model/run/api/v1/info
or give the model a port:
chap-devops models expose chapkit-simple-multistep-model
exposed chapkit-simple-multistep-model on http://localhost:5001
run `chap-devops up` to apply
chap-devops up
up recreates the model container to publish the port. The model needs up to
a minute before it answers on http://localhost:5001 and registers again.
Until then, chap-devops status shows it as running, not registered:
1 of 2 models is not registered.
chapkit-simple-multistep-model: started under two minutes ago and registers once it is ready; run `chap-devops status` again in a minute
See Ports.
6. Back it up
chap-devops backup create
wrote /srv/mychap/chap-devops-backup-mychap-20261012-094057.tar.gz (19.0 KB gzipped, 195.6 KB of data)
One tar.gz in the deployment directory holds the deployment files, a pg_dump
of the chap-core database and one tar per model data volume. The time in the
file name is UTC. The archive holds .env with the database password, so keep
it private. chap-devops backup restore ARCHIVE puts it back. See
Backup and restore.
7. Move the pins forward
chap-devops update --dry-run # the plan, writing nothing
chap-devops update # move the pins, pull the images
chap-devops restart # apply them to the services that are running
When nothing is newer, update gives one row per pin and one closing line:
auto_arima_chapkit v1.0.2 (sha-e9047d4) unchanged
chapkit_simple_multistep_model v0.1.2 (sha-10b2bc7) unchanged
chap-core v2.4.0 unchanged, the newest release
already up to date
restart then shows the compose progress, and its last line is
nothing needed a restart: every container matches its files.
chap-devops update never touches a container. When a pin moved, its closing line
says what moved and which running services are now behind it, and
chap-devops restart recreates exactly those. On a deployment that is not running
there is nothing to restart; with -v, a hint says that chap-devops up starts it
with the new versions. See Updating.
Running one model
chap-devops run starts one model service and prints where it answers. There is no
chap-devops init first and no directory to keep: name the model, by marketplace id,
GitHub repository or image, and use the URL.
chap-devops run chapkit_simple_multistep_model
chap-devops run https://github.com/chap-models/chapkit_ghr_model
chap-devops run ghcr.io/my-org/chapkit_dengue_model:sha-1eb8cf1
chap-devops run my-dengue-model:dev # built on this machine
starting chapkit-simple-multistep-model (chapkit_simple_multistep_model in /home/me/.local/share/chap-devops/run/default)
running chapkit_simple_multistep_model on http://localhost:5001 (answered in 4s)
With -v, two hints follow: the command that stops the model, and the command
that shows its log:
hint: `chap-devops stop chapkit_simple_multistep_model` stops it
hint: `chap-devops -C /home/me/.local/share/chap-devops/run/default logs chapkit-simple-multistep-model` shows its log
The command returns once the model answers its own /health, so the URL works
when the line is printed. With --no-wait, the line starts with started
instead of running, and chap-devops ps shows when the model answers. A first run pulls the image, which takes a while for
the R-INLA models; --timeout SECONDS (300 by default) is how long it waits
before it fails naming the state the model was in.
In the foreground: -a
-a (also --attach or --foreground, as on chap-devops up) keeps chap-devops run in
the foreground. After the URL, it shows the model’s log, and Ctrl-C stops the
model:
running chapkit_simple_multistep_model on http://localhost:5001 (answered in 4s)
following the log of chapkit-simple-multistep-model; Ctrl-C stops it
...
stopped chapkit_simple_multistep_model; its data stays, and `chap-devops run chapkit_simple_multistep_model` starts it again with it
Ctrl-C takes the model out of its group, as chap-devops stop does. The model’s data
volume stays, so the next run starts with the model’s trained state. --rm
removes the data volume too, and the group when it has no model left
(removed group default), as chap-devops stop --purge does. A second Ctrl-C exits at once. A model that ran
before the command is not stopped: Ctrl-C then stops only the log. With
--no-wait, you watch the model start in its log. --json always returns.
Ctrl-C during the start, with or without -a, takes back out what the start
put in: a model that this run enabled goes back out of the group, as after a
failed start. The command then exits with 130:
error: stopped by Ctrl-C; chapkit_simple_multistep_model is taken back out of /home/me/.local/share/chap-devops/run/default
Your own chap-core
A group has no chap-core of its own, so its models register nowhere.
--chap-core URL makes them register with a chap-core that runs elsewhere,
for example one from its own checkout:
chap-devops run -a --rm --chap-core http://localhost:8000 https://github.com/chap-models/chapkit_ghr_model
running chapkit_ghr_model on http://localhost:5001 (answered in 2s)
registered with http://localhost:8000
- chap-devops records the chap-core on the group, as
chap-devops components enable chap-core --urldoes in a deployment. Every model of that group then registers with it. Use--group NAMEfor a group of its own. - chap-devops decides where chap-core calls the models back. A chap-core in a
container on this machine calls
host.docker.internal, and a chap-core process callslocalhost.--models-host HOSTsets it. - After the start, chap-devops asks chap-core whether it lists the model, and says
registered with URL, or gives the warningnot registered with URL. “Up” means chap-core answered, not only that the container started. - A group that had another chap-core moves to the new one. Its models that run now register with the old one until they restart, and chap-devops says so.
In a deployment of your own, --chap-core is refused: set the chap-core there
with chap-devops components enable chap-core --url URL.
What the model can be
MODEL | What run does |
|---|---|
A marketplace id or service id: chapkit_simple_multistep_model | Enables it at the catalogue’s stable version. |
| A GitHub repository the marketplace lists | The same: the catalogue’s reviewed pin, not the newest build of the branch. |
| Any other GitHub repository URL | Adds it the way chap-devops models add does: the newest published sha- build of the default branch. Needs the network. |
| An image on a registry, with a tag or digest | Pins exactly that image. A tag the marketplace publishes for one of its models enables that marketplace version instead. |
A local image, name:tag with no registry | Runs the image from the local store, never pulled. |
A model that is already enabled is started as it is: a second chap-devops run of
the same id does not move its version or its port. A run by the repository URL
of an enabled marketplace model, or by its image at the tag of the enabled
version, finds that model and starts it as it is. The same holds for a
repository or an image: one added before, by an earlier chap-devops run or
models add, is that entry again, enabled or not, so running the same URL twice
starts one model, not two. An image counts as the same only at the same tag.
--id names an added model (--id auto picks a free one), exactly as on
models add; an --id other than the one the source was added under adds a
second copy beside it.
A word with no :, / or @ is a marketplace id, never a local image (a local
image always carries a tag), so chap-devops run does_not_exist says there is no such
model and names chap-devops models search. A marketplace starter is refused like it
is on models enable; --allow-starter starts it anyway.
When the container cannot be started - the image does not exist, the pull is
denied, the port is taken - run says what compose said and takes the model it
enabled back out, so chap-devops ps does not list it and the next chap-devops run of the
same source starts afresh. The definition of an added model stays.
Where it runs: groups
Outside a deployment, chap-devops run keeps its models in a group: a deployment
chap-devops owns under its data directory, run/<group>/. The data directory is
$CHAP_DEVOPS_DATA_DIR, else $XDG_DATA_HOME/chap-devops, else ~/.local/share/chap-devops.
--group NAME picks one; without it the group is default. The first
chap-devops run into a group creates it, as chap-devops init --only none --models none
would, so there is no chap-core and no component in it: only model services,
each on its own host port.
Groups keep models apart. Each one is a compose project of its own, so its
containers, volumes and network carry its name, and stopping a group touches
nothing else. .chap-devops/project.yaml records the group as group: <name>, and
every container in it carries the labels com.winterop.chap-devops.kind: run and
com.winterop.chap-devops.group: <name>, so
docker ps --filter label=com.winterop.chap-devops.group=trial lists one group’s
containers (Container labels):
chap-devops run chapkit_simple_multistep_model # group default
chap-devops run auto_arima_chapkit --group trial
chap-devops run chapkit_ghr_model --group trial
chap-devops ps
GROUP ID SERVICE STATE URL
default chapkit_simple_multistep_model chapkit-simple-multistep-model up http://localhost:5001
trial auto_arima_chapkit auto-arima-chapkit up http://localhost:5002
trial chapkit_ghr_model chapkit-ghr-model not running http://localhost:5003
With -v, a hint after the table names the directory that holds the groups,
for example hint: groups live in /home/me/.local/share/chap-devops/run.
--port auto takes the lowest port in 5001-5999 that nothing on the machine
holds and no group has given to a model, running or stopped, so two groups
never collide.
Inside a deployment of your own - the working directory, or -C DIR - run,
ps and stop work on that deployment instead, and --group is refused,
because the deployment is the group. So a tool that only wants “start this
model and give me its URL” has one code path whether or not someone ran
chap-devops init.
Only this machine
A group publishes every model port on 127.0.0.1, so a model chap-devops run
started is reachable from this machine and not from the network: models have no
login. --bind 0.0.0.0 publishes one on every address instead. Inside a
deployment, run publishes as that deployment does. See
Ports.
Listing and stopping
chap-devops ps lists every model in every group, or in one with --group, with the
state chap-devops status would give it: up when its /health answers,
running, not answering while it starts or when it failed to,
not running when its container is gone.
When it finds no model, chap-devops ps says where it looked:
there is no `chap-devops run` group X; `chap-devops ps` lists every group, for a--group Xthat does not exist.no model runs in a `chap-devops run` group, when the groups exist and have no model.nothing has been started with `chap-devops run` yet, when there is no group.
-C on a group that chap-devops stop --purge removed names the command that makes
it again: the `chap-devops run` group X does not exist, or `chap-devops stop --purge` removed it; `chap-devops run <model> --group X` makes it again. chap-devops ps,
chap-devops stop, chap-devops run, chap-devops chap, chap-devops logs and chap-devops status all
give this error. A -C to another directory that is not a deployment, or to
one that does not exist, gives DIR is not a chap-devops deployment. So chap-devops ps
does not list every group, and chap-devops stop --all does not stop every model,
because of a wrong -C.
chap-devops stop ID stops a model and takes its overlay away, in whichever group
has it; when two groups do, it asks for --group. chap-devops stop --all stops
every model in every group, and chap-devops stop --group NAME every model in one.
The data volume stays, as with models disable, and so does the definition of
an added model, so chap-devops run ID starts it again without asking GitHub.
chap-devops stop prints one line for each model it stopped. With -v, hints say
what became of the container and the volume, and how to start the model again:
$ chap-devops -v stop chapkit_simple_multistep_model
stopped chapkit_simple_multistep_model
hint: stopped and removed 2 containers (chapkit-simple-multistep-model, chapkit-simple-multistep-model-init); host port 5001 is free again
hint: kept volume default-1ab2c3_ck_chapkit_simple_multistep_model_data; remove it with `chap-devops -C /home/me/.local/share/chap-devops/run/default models disable chapkit_simple_multistep_model --purge` or `docker volume rm default-1ab2c3_ck_chapkit_simple_multistep_model_data`
hint: `chap-devops run chapkit_simple_multistep_model` starts it again
--purge takes the volume too, and the group it leaves with no model in it
goes as well: its network, its directory and every volume compose made for
it, including the volume of a model stopped earlier without --purge. Only
that group: another group with no model left, emptied by an earlier plain
stop, keeps its data. chap-devops stop --group NAME --purge empties and removes
that one group, and chap-devops stop --all --purge every group. A volume docker
will not remove keeps its group, so the same chap-devops stop --group NAME --purge
can finish the job once the volume is free.
The deployment behind a group is an ordinary chap-devops deployment, so every other
command works on it with -C: chap-devops -C ~/.local/share/chap-devops/run/default logs chapkit-simple-multistep-model, chap-devops -C ... status. Messages from inside a group name
their commands that way.
For tools
Every one of the three takes --json and prints one document on stdout:
{
"bind": "127.0.0.1",
"enabled": true,
"group": "default",
"id": "chapkit_simple_multistep_model",
"messages": [...],
"ok": true,
"port": 5001,
"project_dir": "/home/me/.local/share/chap-devops/run/default",
"service_id": "chapkit-simple-multistep-model",
"url": "http://localhost:5001",
"wait": {"ready": true, "waited_s": 4, "models": [...]}
}
Each document also has the messages list of every closing line with its
level; see Output. chap-devops ps --json is
{"models": [...]} with the same fields per model plus state; chap-devops stop --json lists what it stopped, the groups it
removed and the volumes that went with them, removed_volumes. A failure is
{"ok": false, "error": ..., "hint": ...} like every other command’s, a usage error included; see
--json for scripts and tools.
docker’s own progress, such as a pull, goes to stderr, so stdout is the one
document.
Two chap-devops run started at the same time into one group wait for each other
rather than writing over each other, and land on different ports; see
One change at a time. That holds for a
group neither of them has created yet, and for two runs of the same model,
which start it once and both report it.
Watching everything: chap-devops top
chap-devops top shows every chap-devops deployment on this machine as one tree that keeps
itself up to date: the deployments made with chap-devops init and the
chap-devops run groups alike, each with its services under it -
chap-core and its worker and databases, DHIS2, OCS, the object store and the
models - and what each container is doing.
chap-devops top 3 deployments 8/12 running at 13:17:51 (every 2s)
┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│DEPLOYMENT / SERVICE STATE CPU MEMORY URL │
│▾ mychap init 6/9 running /home/me/mychap │
│ ├─ chap running (healthy) 6.52% 442.9MiB / 31.29GiB http://localhost:8700 │
│ ├─ postgres running (healthy) 0.02% 35.27MiB / 31.29GiB │
│ ├─ redis running (healthy) 0.28% 12.56MiB / 31.29GiB │
│ ├─ worker running (healthy) 0.79% 1.118GiB / 31.29GiB │
│ ├─ dhis2 running (healthy) 2.82% 1.982GiB / 31.29GiB http://localhost:8780 │
│ ├─ dhis2-db running (healthy) 0.04% 71.71MiB / 31.29GiB │
│ ├─ dhis2-dump exited │
│ ├─ dhis2-prep exited │
│ └─ chapkit-simple-multistep-model (chapkit_simple_multistep_model) exited http://localhost:5001 │
│▸ run/default run 1/1 running /home/me/.local/share/chap-devops/run/ │
│▾ run/trial run 1/2 running /home/me/.local/share/chap-devops/run/ │
│ ├─ auto-arima-chapkit (auto_arima_chapkit) running (healthy) 0.43% 108.9MiB / 31.29GiB http://localhost:5003 │
│ └─ chapkit-simple-multistep-model (chapkit_simple_multistep_model) exited http://localhost:5001 │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
j/k move enter fold l logs s stop model r refresh q quit
| Key | What it does |
|---|---|
j/k, the arrows | Move. |
enter, space | Fold or unfold the deployment under the cursor. |
l | Show the last 200 lines of the service’s log in a pane underneath, colour codes removed; l or esc closes it. The log is asked for on a thread of its own, so the pane says asking docker ... until it arrives and a slow daemon never holds up a key. |
s | Stop the model under the cursor, after a y: the same as chap-devops stop, which takes the model out of its deployment - its overlay goes and chap-devops up no longer starts it; its data stays. The prompt says so. chap-devops run or chap-devops models enable brings it back. Only a model stops here; a whole deployment stops with chap-devops -C DIR down, which the footer names. |
r | Look again now rather than at the next tick. |
q, esc | Quit. |
--interval SECONDS (2 by default, at most 3600) sets how often docker is asked. Docker is
asked from a thread of its own, so a slow daemon never holds up a key.
How it finds them
Every container chap-devops starts carries labels that say what it is (see
Container labels), so one
docker ps -a --filter label=com.winterop.chap-devops.role finds them all, on any
deployment, wherever its directory is. Compose’s own labels say which
deployment and which service each one is, and the directory compose ran in;
from that directory chap-devops top reads the deployment’s state for the URLs. A
deployment whose directory is gone still shows, without them.
The deployment the command runs in, and every chap-devops run group with a model
enabled, show even with nothing running: their enabled models are listed as
not running. A group with no model is left out.
In a script
Off a terminal - piped, or under --json - chap-devops top prints one snapshot of
the same tree and exits:
chap-devops top --json | jq '.deployments[] | {name, kind, services: [.services[] | {service, state, cpu}]}'
Each deployment has project (the compose project name), name, kind
(init or run), group, dir and services; each service has service,
role, model, state, health, cpu, memory and url. For the models
of one deployment with their registration state, chap-devops ps and
chap-devops status say more.
Your first forecast in the Modeling App
A walk from a fresh deployment to an evaluated model and a three-month dengue forecast, by clicking in the Modeling App. It takes about fifteen minutes, most of it waiting for the model. It starts where Chap with a local DHIS2 ends:
chap-devops init mychap --models chapkit_simple_multistep_model --with dhis2
cd mychap
chap-devops up
chap-devops status # until the dhis2 line says up
chap-devops dhis2 connect
chap-devops open dhis2
chap-devops dhis2 connect points the DHIS2 route chap at chap-core, installs the
Modeling App and the DHIS2 Climate App, and runs the DHIS2 analytics tables.
It also makes the configured models that the Modeling App lists. It took about
40 seconds. Its last line is
the Modeling App can reach Chap; open DHIS2 with `chap-devops open dhis2`.
Tested on 2026-10-19 with chap-devops v0.110.0, Modeling App 7.3.0, DHIS2
2.42.6 with the Laos demo database, chap-core v2.5.0 and Simple Multistep
(chapkit) 0.1.2. Other versions of the Modeling
App may name a button differently; the steps stay the same. chap-devops dhis2 show
says which Modeling App version a deployment has.
What the demo data holds
The DHIS2 that chap-devops starts is seeded with a demo database from Laos:
| Data | Where it is in DHIS2 | Range |
|---|---|---|
| Dengue cases | Dengue Cases (Any) - Weekly | 2019 to 2024, weekly |
| Rainfall | CCH - Precipitation (CHIRPS) | 2015 to April 2025 |
| Temperature | CCH - Air temperature (ERA5-Land) | 2015 to April 2025 |
| Population | LSB: Population (Estimated-single age) | 2016 to 2025, yearly |
for Lao PDR, its 18 provinces and their districts. The Modeling App adds the weekly cases up to months when an evaluation is monthly.
1. Open the Modeling App
Log in to DHIS2 with admin / district, open the app menu (the grid of
squares at the top right), type Modeling in its search box and pick
Modeling: the menu shows only a few top apps until you search. The
dashboard is empty until the first evaluation.
2. Start an evaluation
An evaluation asks: had this model been used in the past, how close would its forecasts have come? It trains the model on the earlier data, forecasts the months after, and compares the forecast with what really happened, several times over.
In Evaluate, Overview, choose New evaluation. The page opens on the tab Use saved dataset, which needs a dataset saved before. Choose the tab Import from DHIS2 and fill in:
| Field | Value |
|---|---|
| Name | anything, for example Multistep Laos dengue |
| Period type | Monthly |
| From period / To period | 2019 January to 2024 December: pick the year in the box at the top of the picker, then the month |
| Organisation units | tick Lao PDR, pick the level Province below the tree, then Confirm Selection |
| Model | Select model, click Simple Multistep Model (chapkit) [Monthly climate] in the list, then Use this model |
The list cuts long names short, for example Simple Multistep Model (chapkit) [Mont…. The panel on the right shows the full name of the entry you
clicked, and Use this model picks that entry.
Leave the Backtest parameters as they are: 3 forecast periods, 7 splits, a step of 1, 1 training run, and the future-weather provider Seasonal climatology.
Simple Multistep Model (chapkit) is the model chap-devops started with
--models chapkit_simple_multistep_model. It has two entries, one for each
configuration in its marketplace entry: Monthly climate uses rainfall,
temperature and relative humidity, and Monthly selfhistory uses only the
case history. The list shows one entry for each configured model, as
<model name> [<variant name>]. Each model enabled with chap-devops models enable adds its own entries. chap-core 2.4 and later does not make these
entries itself: chap-devops up --wait, chap-devops dhis2 connect and chap-devops models configs sync make them. If a model is not in the list, run chap-devops status. If it
shows the model as registered, not configured, run chap-devops models configs sync.
To add an entry of your own with other option values, run chap-devops models configs add. See Adding.
chap-devops models test --all makes each model train and predict. See
Configured models.
Under each name, the list shows the author of the model. The list also holds
models that chap-core ships configured on its own and runs inside its worker,
such as CHAP-EWARS Model and Monthly CHAP-EWARS model. Some of them
have the status Deprecated. From chap-core v2.5.0, it also has the three
built-in baselines, Persistence, Global median and Seasonal
median. chap-core runs them beside each evaluation, so you can compare a
model with them. None of these are models that chap-devops started. The two
(chapkit) entries have the status Limited.
3. Map the data
Configure sources asks which DHIS2 data each input of the model comes from. Search by name in each box:
| Model input | DHIS2 data item |
|---|---|
| Disease cases | Dengue Cases (Any) - Weekly |
| Rainfall | CCH - Precipitation (CHIRPS) |
| Mean temperature | CCH - Air temperature (ERA5-Land) |
| Mean relative humidity | CCH - Relative humidity (ERA5-Land) |
A search for the dengue cases also finds the items
CHAP Dengue Cases (Any) - Weekly Quantile ... and
NCLE: 7. Dengue cases (any). The CHAP items hold forecasts, not cases.
Pick Dengue Cases (Any) - Weekly exactly.
Save, then Start dry run. It checks the data without starting anything, and should say All 18 locations can be successfully imported.
Close it and choose Start import. The app moves to Jobs, where the
evaluation runs. With Simple Multistep it took about 24 seconds on an Apple
Silicon Mac (M2 Max), where the model runs under emulation. chap-devops jobs in the
terminal shows the same job; its type is create_backtest_from_data.
4. Read the result
Back in Evaluate, Overview, open the evaluation. The chart shows the real cases (orange) and, for the period picked under Split period, the model’s forecast with its 50% and 80% prediction intervals. The list on the left switches between all of Laos and one province.
Evaluation metrics, lower on the right, scores the forecasts over every split and province: lower is better for CRPS (log1p), MAE and RMSE, and Coverage 10-90 should be close to its target of 0.8 (80% of the real values inside the 80% interval). Show all 13 metrics lists the others.
To compare two models, run a second evaluation on the same data:
- On the first evaluation, choose Create new based on…. It asks which settings to copy; leave them all ticked and choose Create.
- The copy has the name, provinces, model, data mapping and periods of the
first, and its name ends in
(Copy). - On the copy, Select model picks another model, for example Simple Multistep Model (chapkit) [Monthly selfhistory]. Then choose Start import.
- In Evaluate, Compare, pick the first evaluation in the left box and the copy in the box beside it.
The evaluation of Monthly selfhistory took about 20 seconds. The comparison shows the two side by side, province
by province; the box Location(s) starts with ten provinces selected. More
marketplace models come with chap-devops models enable ID and chap-devops up
(chap-devops models list shows the ids).
5. Make a forecast
On the evaluation, Create prediction setup keeps the model, the data mapping and the provinces under a name: type one in Setup name, leave Alert policy at No alert policy and Set default import mapping off, and Save. The app then opens the setup. Once the evaluation has a setup, the same button says Predict.
On the setup, choose Run prediction. The page fills in a name for the run. In Training period, set the last training period to the last month with case data: 2024 December for the demo data. Then choose Run prediction again. The app goes back to the setup, where the run shows as Running. The forecast covers the three months after that month.
The job took about 7 seconds in chap-devops jobs. The row in the app said
Success within a minute, without a reload.
Go to last run shows one chart per province:
the real cases up to December 2024 and the forecast for January to March 2025.
Import, on the right of the forecast, opens a page that writes the
forecast back into DHIS2 as data, where dashboards and maps can use it. It
needs five data elements for the forecast’s quantiles. Each box is empty at
first; open it, and it lists the elements of the demo database under
Suggested. Pick CHAP Dengue Cases (Any) - Weekly Quantile High to
... Quantile Low.
Use alert outputs is on by default, and then the page also needs an
Outbreak indicator: Clear and import does nothing until it has one,
and the message is under the box, below the first screen. Pick
CHAP Dengue Cases (Any) - Weekly Outbreak Alert; the box does not suggest it,
so type Outbreak to find it. Or turn Use alert outputs off.
Clear and import asks once more before it writes, then the app goes back
to the setup. DHIS2 2.42.6 accepted the monthly values into those elements,
but their data set is weekly. For a monthly forecast, create monthly data
elements in DHIS2’s Maintenance app and pick those.
When something goes wrong
| What you see | What to do |
|---|---|
Typing Modeling in the app menu finds nothing | chap-devops dhis2 connect has not run, or failed; run it and read its last line. |
The model list does not have the model chap-devops enabled | chap-devops status: the model must be registered. If it says registered, not configured, run chap-devops models configs sync. Otherwise the line under the table says why. |
Oops! Sorry, an unexpected error, or Unnamed evaluation rows, after the deployment was recreated | The browser holds a login to the DHIS2 that was removed: open DHIS2 again and log in. See Troubleshooting. |
| Clear and import does nothing | Scroll down: with Use alert outputs on, the page says Outbreak indicator is required. Pick CHAP Dengue Cases (Any) - Weekly Outbreak Alert, or turn Use alert outputs off. |
| A job in Jobs says it failed | chap-devops jobs, then chap-devops jobs logs ID with the id it prints: the model’s own error is at the end. |
More on connecting DHIS2 and Chap: DHIS2. The Modeling App’s own guide is at chap.dhis2.org.
Use cases
Helping someone as an AI assistant? Start at For AI assistants, which turns these pages into a menu of options with the exact commands.
chap-devops deploys Chap, but Chap is only one of the shapes a deployment can take.
The same directory layout, the same commands and the same chap-devops up /
chap-devops status / chap-devops doctor loop work for any combination of:
| Piece | What it is | How you ask for it |
|---|---|---|
| chap-core | Chap itself: the API, its worker, Valkey and PostgreSQL | on by default; --without chap-core or --only leaves it out |
| model services | chapkit forecasting models from the marketplace, or your own | --models ID,..., later chap-devops models enable ID |
ocs | Open Climate Service: climate data over STAC and openEO | --with ocs, or --only ocs |
s3 | an S3-compatible object store for OCS to keep objects in | --with s3, or --only ocs,s3 |
dhis2 | a demo or development DHIS2 with its own database | --with dhis2, or --only dhis2 |
--with adds components to chap-core. --only names the whole set, and
chap-core is part of it only if you list it. Each page below is one shape
people actually deploy: the command that creates it, what you get, and what to
run next. They are not exclusive: a deployment can move from one shape to
another later with chap-devops components enable / disable and chap-devops models enable / disable. See Growing a deployment.
Only one piece
| I want | Page | First command |
|---|---|---|
| only DHIS2 (2.42, 2.43, 2.41 or the next one) | A DHIS2 on its own | chap-devops init dhis --only dhis2 |
| only OCS (with or without S3) | An OCS server on its own | chap-devops init climate --only ocs,s3 |
| only one model service | A chapkit model service on its own | chap-devops run chapkit_simple_multistep_model |
| only some model services | Several model services side by side | chap-devops init models --only none --models ID,ID |
| any number of model services, no folder | Any number of model services, no folder | chap-devops run ID, once per model |
Chap
- Chap with forecasting models: chap-core and the models it runs; the default, or every model in the marketplace.
- Chap with climate data from OCS: chap-core with Open Climate Service beside it.
- Chap with a local DHIS2 and the Modeling App: chap-core, models and a DHIS2 of the version you pick, connected.
- Chap for a DHIS2 that runs elsewhere: chap-core on a server, used by a DHIS2 someone else runs.
- Combinations: OCS and DHIS2 without Chap, a model beside OCS, everything at once.
Evaluating with the chap CLI
- Evaluating a model on your own data: chap-core’s
chap evalon your CSV, throughchap-devops chap, with no Python or uv.
Developing Chap, a model or DHIS2
- A new model of your own:
chap-devops models newmakes a model project in Python or R, and Docker builds it. - An MLproject model, with no image to build:
chap-devops chapkit mlproject runserves the directory, chap-core tests it, andmigrateandexportchange it to a chapkit service and back. - Your model from its checkout, with Chap: chap-devops runs chap-core; you run your model with
uv runand it registers. - A model image you built yourself:
docker builda model and run it in Chap without publishing it. - chap-core from its checkout, with the models: you run chap-core; chap-devops runs the models and registers them with it.
- chap-core built from its checkout: chap-devops builds chap-core from your clone and runs it with everything else.
- A DHIS2 you run yourself, with Chap from chap-devops: your DHIS2 and a chap-core from chap-devops, connected.
- A DHIS2 from chap-devops, with a chap-core elsewhere: a DHIS2 from chap-devops and your chap-core, connected.
More than one, and changing your mind
- Several deployments on one machine: taking turns on the default ports, or giving each its own.
- Growing a deployment: adding or removing chap-core, OCS, DHIS2 or models later.
- Taking over a chap-admin deployment: moving a server that runs chap-core’s compose files and
chap-adminto chap-devops, with its database.
Which commands work where
| Command | Needs chap-core | Without chap-core |
|---|---|---|
up, down, restart, logs, status, doctor, sync | no | work on whatever the deployment has |
components, models enable/disable/add/expose | no | work; models get a host port |
models test | no | runs chapkit test in the model’s container |
update | no | pulls the images; --chap-tag and --pin-chap-core refuse |
backup create / restore | no | back up the files and the volumes, and a pg_dump of the DHIS2 database when DHIS2 is a component; there is no chap-core database to dump |
open | no | opens whatever is enabled |
models test --backtest, jobs, api | yes | refuse, naming chap-devops components enable chap-core (api --url still works) |
dhis2 connect | yes | refuses: there is no chap-core to connect DHIS2 to |
Chap with forecasting models
The default, and what most deployments are: chap-core plus the models it runs.
chap-devops init mychap --models chapkit_simple_multistep_model
cd mychap
chap-devops up
chap-devops status # chap-core up, every model registered
chap-devops models test --all # make each model train and predict once
It worked when chap-devops status shows chap-core up and every model
registered:
chap-core up http://localhost:8700 2.4.0 auth: off
MODEL STATE REACH LAST PING
chapkit-simple-multistep-model registered, not configured via chap-core 1s ago
1 model registered
chapkit-simple-multistep-model: chap-core has no configured model for it, so nothing can run it; run `chap-devops models configs sync`
and chap-devops models test --all says every model passes:
testing 1 model (model level; add --backtest to go through chap-core)
chapkit-simple-multistep-model pass 5s 1 training, 1 prediction
1 of 1 model passes
registered, not configured is the normal state after a plain chap-devops up.
chap-core v2.4.0 makes no configured model when a model registers, and the
model level of chap-devops models test does not need one. A backtest and the
Modeling App need one. The backtest below creates
them, and so do chap-devops models configs sync and chap-devops up --wait. After that,
chap-devops status shows the model as registered. See
Configured models.
You get chap-core’s API on port 8700 and one service per model. The models
register with chap-core over the compose network and publish no host port.
You reach them through chap-core at
http://localhost:8700/v2/services/<service_id>/run/.
To change the models later, use chap-devops ui, or chap-devops models enable ID and
chap-devops models disable ID. disable stops the model at once. After enable,
run chap-devops up to start the model.
Every model in the marketplace
all is every model the marketplace lists (starters aside), and a list of
ids picks some. chap-devops models list prints them. Without --models, init
enables no model.
This is a new deployment. It needs port 8700 too. If the deployment from the
first section is still up, go to mychap and run chap-devops down --volumes --yes
first, then cd ... chap-devops init then warns that mychap also uses port
8700. While mychap is down, you can ignore that warning.
chap-devops init allmodels --models all
cd allmodels
chap-devops up
chap-devops status # again, until every model is registered
chap-devops models test --all # make each model train and predict once
The model images are large: four images of 1 to 7 GB each, all linux/amd64
only. chap-devops init already pulls one of them to read its user, and says
asking ghcr.io/chap-models/chapkit_ghr_model:sha-dfb2e3f what uid `app` is (this pulls the image).
The first chap-devops up pulls the others, which can take many minutes.
chap-devops up returns before the models register. Right after it, chap-devops status
can show some or all models as not registered yet, and exits with 1:
MODEL STATE REACH LAST PING
auto-arima-chapkit running, not registered via chap-core -
...
chapkit-simple-multistep-model running, not registered via chap-core -
4 of 4 models are not registered.
auto-arima-chapkit: started under two minutes ago and registers once it is ready; run `chap-devops status` again in a minute
...
If you see this, wait a minute and run chap-devops status again. On an M2 Max,
all four models registered in less than a minute. It worked when chap-devops status
ends with all 4 models registered, followed by one
chap-core has no configured model for it line for each model, as in the
first section. chap-devops models test --all then says:
testing 4 models (model level; add --backtest to go through chap-core)
auto-arima-chapkit pass 10s 1 training, 1 prediction
chapkit-ghr-model pass 24s 1 training, 1 prediction
chapkit-rwanda-malaria-bym-model pass 20s 1 training, 1 prediction
chapkit-simple-multistep-model pass 6s 1 training, 1 prediction
4 of 4 models pass
On an Apple Silicon Mac the models run under emulation; the four tests above took about 60 seconds on an M2 Max.
Test through chap-core
chap-devops models test --all --backtest tests each model through chap-core, the
way the Modeling App does. It makes a dataset from the sample data of the
model, runs a backtest on it and reads the scores. A backtest needs a
configured model, so the command first creates the configured models from the
marketplace entry of each model, as chap-admin install does. You make no
API call yourself.
chap-devops models test --all --backtest
It worked when every model passes with its scores, and the closing lines name the configured models it created:
testing 4 models (through chap-core: a dataset, a backtest and its scores)
auto-arima-chapkit pass 18s crps 3.4 mae 4.9 rmse 5.8
configured model: monthly (id 14)
chapkit-ghr-model pass 1m 33s crps 14.7 mae 21.8 rmse 23.8
configured model: monthly_climate (id 15)
chapkit-rwanda-malaria-bym-model pass 38s crps 16.3 mae 21.5 rmse 26.5
configured model: monthly (id 16)
chapkit-simple-multistep-model pass 13s crps 3.9 mae 5.3 rmse 6.8
configured model: monthly_climate (id 17)
created configured models in chap-core for auto_arima_chapkit (monthly), chapkit_ghr_model (monthly_climate), chapkit_rwanda_malaria_bym_model (monthly), chapkit_simple_multistep_model (monthly_climate, monthly_selfhistory)
4 of 4 models pass
The four backtests took about three minutes on an M2 Max. The scores are not
the same from one run to the next. A second run creates no configured models,
so it has no created configured models line. After the first run,
chap-devops status shows each model as registered.
Next: Models and the marketplace, Authentication before you put it on a network, Backup and restore.
All shapes: Use cases.
Chap with climate data from OCS
chap-core with an Open Climate Service beside it, so Chap has climate data to fetch from inside the same deployment.
chap-devops init mychap --models chapkit_simple_multistep_model --with ocs,s3
cd mychap
chap-devops up
chap-devops status # chap-core, ocs and s3 lines, then the models
The first chap-devops up pulls the images that are not on the machine yet: the
chap-core images, the model image and the OCS image. The OCS image alone is
about 3 GB on disk. With the chap-core and model images already present, the
first chap-devops up took about a minute in one test run. It ends with one line:
started chap, chapkit-simple-multistep-model, ocs, postgres, redis, s3, worker
chap-devops status then shows this:
chap-core up http://localhost:8700 2.4.0 auth: off
ocs up http://localhost:8790 0 datasets 32.0 KB data
s3 up internal
MODEL STATE REACH LAST PING
chapkit-simple-multistep-model registered, not configured via chap-core 1s ago
1 model registered
chapkit-simple-multistep-model: chap-core has no configured model for it, so nothing can run it; run `chap-devops models configs sync`
registered, not configured is the normal state after a plain chap-devops up.
A backtest and the Modeling App need a configured model. Make them:
chap-devops models configs sync
chapkit_simple_multistep_model: created configured model monthly_climate
chapkit_simple_multistep_model: created configured model monthly_selfhistory
chap-devops up --wait does the same step for you. See
Configured models. Now chap-devops status shows
the model as registered:
MODEL STATE REACH LAST PING
chapkit-simple-multistep-model registered via chap-core 2s ago
1 model registered
chap-core reaches OCS at http://ocs:9000 on the compose network, and you
reach it at http://localhost:8790. chap-devops does not configure chap-core to
fetch from OCS. chap-core has no setting for it: the two run side by side, and
you connect them. See OCS.
init writes ocs/climate-service.yaml, which is yours to edit. chap-devops changes
only its read_only and plugins_dir keys. It also puts the data source credentials into .env, commented
out. ERA5-Land needs one or both of a Copernicus Climate Data Store account
and an Earth Data Hub account. WorldPop and CHIRPS3 need no account.
It worked when chap-devops status shows chap-core, ocs and s3 as up and each
model as registered.
Climate data in and out
To put climate data in and get it out, see Ingesting a first dataset and the sections after it. The same commands work here. Three days of CHIRPS3 took between 10 and 40 seconds, and one year of monthly CHIRPS3 between 30 seconds and one minute.
The per-district CSV in Getting it out reads the districts from a DHIS2 on port 8780. This deployment has no DHIS2. Do one of these:
- Give
geometriesa GeoJSONFeatureCollectionof your own districts. The feature ids become thelocationcolumn. - Add the demo DHIS2 with
chap-devops components enable dhis2, thenchap-devops up.chap-devops upreturns while DHIS2 still starts. Runchap-devops statusagain untildhis2showsup. See Growing a deployment.
The extent is Laos, like the demo DHIS2, until you change it.
chap-devops doctor warns about this on its components line, and tells you how
to change it. See
ocs/climate-service.yaml.
Next: OCS, and Data source credentials for which dataset needs which account.
All shapes: Use cases.
Chap with a local DHIS2 and the Modeling App
A complete local setup: chap-core, its models, and a DHIS2 seeded from the demo database, connected so the Modeling App in DHIS2 talks to this Chap. For development, demos and training.
chap-devops init mychap --models chapkit_simple_multistep_model --with dhis2
cd mychap
chap-devops up # the first start of DHIS2 can take minutes
chap-devops status # wait until the dhis2 line says up, not starting
chap-devops dhis2 connect # the route to chap-core, the apps, analytics
chap-devops open dhis2
The first chap-devops up on a machine pulls the images. The Chap images are about
6 GB to download (see Quickstart), and DHIS2 adds 1 to
1.5 GB. When the images are on the machine, up returns in seconds. DHIS2 then
restores the Laos demo database and migrates its schema. On an arm64 Mac, the
dhis2 line of chap-devops status said up less than a minute after up
returned. Under emulation, this can take 8 to 15 minutes. chap-devops logs dhis2
shows the progress.
chap-devops up does not connect DHIS2 to Chap; chap-devops dhis2 connect does. Until
it has run once, up ends with this line, and status shows a shorter form:
chap-devops has not connected this DHIS2 to Chap; run `chap-devops dhis2 connect` once DHIS2 answers
If DHIS2 is still starting, chap-devops dhis2 connect waits for it, for at most 20
minutes. It logs in as admin / district, makes the chap route, installs
the two apps from the App Hub, and generates the analytics tables. That took
about 40 seconds on the demo database:
DHIS2 2.42.6 at http://localhost:8780, as `admin`
repointed the `chap` route at http://chap:8000/**; chap-core answered through it
installed Modeling App 7.3.0
installed DHIS2 Climate App 1.16.2
analytics finished in 36 seconds (job Xx1SH9cHGCV)
the Modeling App can reach Chap; open DHIS2 with `chap-devops open dhis2`
The demo database has a chap route already, so connect repoints it at this
chap-core. On an empty DHIS2, that line starts with created instead.
It worked when:
chap-devops dhis2 connectends withthe Modeling App can reach Chap.chap-devops open dhis2opens DHIS2 onhttp://localhost:8780. Log in withadmin/district.- Typing
Modelingin the app menu (the grid icon at the top right) finds Modeling, and its Models page lists the models.
Next, in the Modeling App: Your first forecast in the Modeling App evaluates a model on the demo data and makes a forecast with it.
To remove the deployment and its data, run chap-devops down --volumes --yes in
mychap.
Another DHIS2 version
chap-devops init chap243 --models chapkit_simple_multistep_model --with dhis2 --dhis2-tag 2.43
chap-devops init chap241 --models chapkit_simple_multistep_model --with dhis2 --dhis2-tag 2.41
chap-devops init chapdev --models chapkit_simple_multistep_model --with dhis2 --dhis2-image dhis2/core-dev --dhis2-tag master
Each line makes a new deployment. If the directory already exists, init
stops with already contains a chap-devops deployment; choose another directory, or use --force to overwrite it. Use a
different name.
The deployments use the same ports (8700 and 8780), so only one of them can be
up at a time. If another deployment is up, init tells you, for example:
warning: port 8780 is already in use on this machine (needed by dhis2); free it, or run `chap-devops components enable dhis2 --port 8781`
Before you start the new deployment, run chap-devops down in the old one.
The Laos climate demo database exists for 2.42 only, so these start with an
empty DHIS2. init tells you so:
warning: chap-devops knows no DHIS2 demo dump for 2.43, so `dhis2_db` starts empty; name one with `seed:` in `.chap-devops/components.yaml` (a URL or a path) and run `chap-devops sync`
An empty DHIS2 still has the admin / district login, and dhis2 connect
works the same. An empty DHIS2 has no data for the Modeling App to evaluate a
model on.
Do not seed them with DHIS2’s Sierra Leone demo when you want Chap connected.
Its admin is not a superuser, and dhis2 connect stops at the route:
error: DHIS2 refused to write the `chap` route as `admin` (HTTP 403, "You don't have the proper permissions to create this object."): that needs the F_ROUTE_PUBLIC_ADD authority; add it to one of that user's roles in DHIS2 (Users, User role), or name a superuser with `--user NAME`
See Picking the version and Picking the data in A DHIS2 on its own.
Next: DHIS2, especially Connecting the Modeling App to Chap and Memory.
All shapes: Use cases.
Chap for a DHIS2 that runs elsewhere
chap-core and its models on a server, used by an existing DHIS2 instance.
chap-devops init mychap --models chapkit_simple_multistep_model --api-token
cd mychap
chap-devops up # returns when the containers start
chap-devops status # run it again until the chap-core line says up
chap-devops dhis2 use https://dhis2.example.org --chap-url https://chap.example.org
chap-devops dhis2 connect
Replace the two URLs in chap-devops dhis2 use:
https://dhis2.example.orgis the URL of the DHIS2, as this machine reaches it.https://chap.example.orgis the address of chap-core, as the DHIS2 server reaches it. The route points there.
--api-token protects chap-core’s API from the start. dhis2 use records the
external DHIS2 and the address it reaches chap-core at. connect then creates
the chap route on that DHIS2, or points an existing chap route at this
chap-core. It puts chap-core’s token in the route, so the Modeling App reaches
chap-core and never holds the token itself.
The first chap-devops up on a machine pulls the images: about 6 GB to download and
about 20 GB on disk (see Quickstart). When the images are
on the machine, up returns in seconds, and chap-core answers some seconds
later.
The credentials
chap-devops has no default login for a DHIS2 it did not deploy. So the first
chap-devops dhis2 use ends with this warning, and chap-devops dhis2 connect stops with
the same text as an error::
warning: chap-devops has no credentials for this DHIS2, and did not deploy it, so there is no default to try; set `DHIS2_ADMIN_USERNAME` and `DHIS2_ADMIN_PASSWORD`, or `DHIS2_API_TOKEN`, in `.env`
.env has these three lines as comments at its end. To give chap-devops a login:
- Remove the
#fromDHIS2_ADMIN_USERNAMEandDHIS2_ADMIN_PASSWORD, and set the values of a DHIS2 superuser. Or setDHIS2_API_TOKENto a personal access token of that user. - Run
chap-devops dhis2 useagain. Itsloginline ends with(accepted). - Run
chap-devops dhis2 connect.
See The credentials.
What connect changes
On a DHIS2 that chap-devops does not run, connect changes the route and nothing
else. The admin of that DHIS2 decides about the other two steps:
chap-devops dhis2 appsinstalls the Modeling App and the Climate App.chap-devops dhis2 analyticsgenerates the analytics tables. On a large instance this can take hours.
For a DHIS2 2.42 that has a chap route already and none of the apps, the
first connect printed this:
external DHIS2 2.42.6 at https://dhis2.example.org, as `admin`
repointed the `chap` route at https://chap.example.org/**; chap-core answered through it
skipped: installing apps on a DHIS2 chap-devops does not run (the Modeling App is not installed, the Climate App is not installed); `chap-devops dhis2 apps` installs them if its admin agrees
skipped: generating analytics tables on a DHIS2 chap-devops does not run; `chap-devops dhis2 analytics` starts a run if its admin agrees
created configured models in chap-core for chapkit_simple_multistep_model (monthly_climate, monthly_selfhistory)
the Modeling App can reach Chap after the admin of this DHIS2 installs the Modeling App and the Climate App; `chap-devops dhis2 show` says when they are there
On a DHIS2 with no chap route, the second line starts with created. Only
the first connect prints the created configured models line. It makes the
configured models that the Modeling App lists.
It worked when:
chap-devops dhis2 connectsayschap-core answered through it.chap-devops dhis2 showlists the route as(healthy).- When the two apps are installed, the last line of
chap-devops dhis2 showandchap-devops dhis2 connectisthe Modeling App can reach Chap; open DHIS2 with `chap-devops open dhis2`. - The Models page of the Modeling App lists the models.
Until the admin installs the apps, chap-devops dhis2 show names each missing app
on a missing: line.
If connect warns nothing answered through it, read the rest of the
warning:
- If it says
chap-core answers at, chap-core is up, but the DHIS2 server may not reach the--chap-urladdress. Set the correct address withchap-devops dhis2 use --chap-url URL. Then runchap-devops dhis2 connectagain. - If it says
run `chap-devops status`, chap-core did not answer on this machine. Runchap-devops status.
See The route is there but nothing answers through it.
Until connect gets an answer from chap-core through the route, chap-devops up
ends with this line:
chap-devops has not connected this DHIS2 to Chap; run `chap-devops dhis2 connect` once DHIS2 answers
On a DHIS2 that chap-devops does not run, the route is all that connect changes.
So when chap-core answers through the route, connect records the connect in
.chap-devops/components.yaml, and the line stops. The apps do not have to be
installed for this.
The address of chap-core
--chap-url is the address at which the DHIS2 server reaches this machine:
- A DHIS2 on another server needs the name or IP address of this machine.
localhostworks only for a DHIS2 that runs directly on this machine.chap-devops dhis2 usewarns about it.- A DHIS2 in Docker on this machine uses
http://host.docker.internal:8700.
chap-devops serves chap-core over http on port 8700. An https:// address needs a
reverse proxy in front of it. If you use http://SERVER:8700, the DHIS2 admin
must add that origin, with no path, to route.remote_servers_allowed in the
dhis.conf of the DHIS2 server (DHIS2 2.42 and later).
Try it on one machine
A second chap-devops deployment can be the external DHIS2. It uses port 8790, so it
does not conflict with chap-core. Start it before chap-devops dhis2 use. Run these
commands in the directory that holds mychap, not in mychap:
chap-devops init ext-dhis2 --only dhis2 --dhis2-port 8790
cd ext-dhis2
chap-devops up
chap-devops status # run it again until the dhis2 line says up
DHIS2 answers after about a minute on a fast machine, and after some minutes on a slow one.
In mychap, use chap-devops dhis2 use http://localhost:8790 --chap-url http://host.docker.internal:8700. In The credentials, use
the demo login admin / district. The demo database has a chap route
already, so connect says repointed.
This DHIS2 is yours, so you are its admin. In mychap, chap-devops dhis2 apps
installs the two apps in about ten seconds.
To remove the two deployments and their data, run chap-devops down --volumes --yes
in mychap and in ext-dhis2.
Next: A DHIS2 that runs elsewhere, Authentication.
All shapes: Use cases.
An OCS server on its own
Open Climate Service and no Chap at all: a climate data server for anything that speaks STAC or openEO.
chap-devops init climate --only ocs,s3
cd climate
chap-devops up
chap-devops status
chap-devops open ocs
The first chap-devops up pulls the OCS image, about 3 GB. After that, a start takes
some seconds. chap-devops status then shows:
ocs up http://localhost:8790 0 datasets 32.0 KB data
s3 up internal
both components are up
You get OCS on http://localhost:8790, with the S3-compatible store beside it.
OCS does not read the store yet: it keeps its data on its own ocs_data volume
(The object store says why the store is
there). Nothing Chap-specific is created: no compose.yml, and no chap-core
release is looked up. It worked when chap-devops status shows ocs as up and
chap-devops open ocs opens its web interface.
To leave out the object store, use this init instead of the first one. OCS
works the same, on the same volume:
chap-devops init climate --only ocs
Ingesting a first dataset
CHIRPS3 rainfall is public and needs no account. Three days of it, over the instance’s extent:
curl -X POST http://localhost:8790/ingestions \
-H 'Content-Type: application/json' \
-d '{"dataset_id": "chirps3_precipitation_daily", "start": "2024-01-01", "end": "2024-01-03", "publish": true}'
curl http://localhost:8790/stac/collections/chirps3_precipitation_daily
The first command waits until the data is in, about 20 seconds. It worked when
it answers with "status":"completed" and the second returns the collection;
chap-devops status then counts one dataset:
ocs up http://localhost:8790 1 dataset 424.0 KB data
The data size can be a little different on your machine.
The extent is Laos until you change it, the country of the DHIS2 demo database
chap-devops seeds, so data ingested here covers the provinces that DHIS2 holds case
data for. Change it in ocs/climate-service.yaml, or at init with
--ocs-name, --ocs-country and --ocs-bbox; curl http://localhost:8790/extent shows the one in use.
Which datasets there are
curl http://localhost:8790/data-sources
lists every data source OCS knows, and "ingestable": true marks the ones
/ingestions takes. The ones Chap models use:
| Dataset id | Period | Account |
|---|---|---|
chirps3_precipitation_monthly, chirps3_precipitation_daily | month, day | none |
era5land_temperature_monthly, era5land_precipitation_monthly | month | Copernicus Climate Data Store |
era5land_temperature_daily, era5land_precipitation_daily | day | Earth Data Hub |
worldpop_population_global2_100m | year | none |
Data source credentials says where the two accounts go.
A range worth forecasting with
start and end are written in the dataset’s own period: YYYY-MM-DD for a
daily one, YYYY-MM for a monthly one and YYYY for a yearly one. Six years
of monthly rainfall, the years the demo DHIS2 has cases for:
curl -X POST http://localhost:8790/ingestions \
-H 'Content-Type: application/json' \
-d '{"dataset_id": "chirps3_precipitation_monthly", "start": "2019-01", "end": "2024-12"}'
The request waits until the ingestion is complete. For Laos that took between
two and eleven minutes, as the download speed changes, and curl shows
nothing in that time. With -H 'Prefer: respond-async' it answers 202 at
once instead, with a Location of /ingestions/jobs/ID to poll. Leaving out
end ingests up to the current period, and sending the same request again
answers at once with the earlier ingestion rather than downloading it twice.
curl 'http://localhost:8790/datasets?f=json' lists what has been ingested.
Getting it out
Values per district or province, as the CSV Chap reads: OCS averages the
dataset over each area in a GeoJSON FeatureCollection whose feature ids
become the location column. A DHIS2 hands out its org units in that shape,
so with the demo DHIS2 beside OCS (chap-devops components enable dhis2, then
chap-devops up; or --only ocs,s3,dhis2 at init, option 10 on the
AI page). chap-devops up returns
while DHIS2 still starts. Run chap-devops status again until dhis2 shows up,
then run:
curl -u admin:district -o provinces.geojson \
'http://localhost:8780/api/organisationUnits.geojson?level=2'
python3 - > request.json <<'PY'
import json
areas = json.load(open("provinces.geojson"))
print(json.dumps({"process": {"process_graph": {"agg": {
"process_id": "aggregate_to_chap_csv",
"arguments": {"dataset_id": "chirps3_precipitation_monthly",
"temporal_extent": ["2024-01-01", "2024-12-31"],
"geometries": areas, "method": "mean", "period_type": "month"},
"result": True}}}}))
PY
curl -X POST http://localhost:8790/result \
-H 'Content-Type: application/json' -d @request.json -o rainfall.csv
It worked when rainfall.csv starts with time_period,location,precip and has
one row per province and month (216 for the 18 provinces over 2024). The values
are what the dataset holds: CHIRPS3 is a rate in mm per day.
The whole grid over a bounding box, as NetCDF, for anything that reads it
(xarray, QGIS, Panoply):
curl -X POST http://localhost:8790/result -H 'Content-Type: application/json' -o rainfall.nc -d '{
"process": {"process_graph": {
"load": {"process_id": "load_collection", "arguments": {
"id": "chirps3_precipitation_monthly",
"temporal_extent": ["2024-01-01", "2024-12-31"],
"spatial_extent": {"west": 100.0, "south": 13.9, "east": 107.7, "north": 22.5}}},
"save": {"process_id": "save_result",
"arguments": {"data": {"from_node": "load"}, "format": "NetCDF"}, "result": true}}}}'
GET /datasets/ID/download answers 409 for data OCS keeps in its default
storage; /result is the way out. Both calls above still work on a read-only
instance, which refuses ingestion but not reading.
As a server
Two settings matter once other people use it:
chap-devops components enable ocs --read-only # refuse writes over HTTP
chap-devops restart ocs # apply read-only
chap-devops components enable ocs --base-url https://ocs.example.org # behind a proxy
chap-devops up # apply the base URL
--read-only changes ocs/climate-service.yaml. OCS reads that file only
when it starts, so the command tells you to restart it:
ocs is now read-only
run `chap-devops restart ocs` to apply
chap-devops up does not apply this change. It leaves OCS as it is, and it prints
this warning on stderr. Until you run chap-devops restart ocs, chap-devops status shows
the same warning:
warning: `ocs/climate-service.yaml` changed after the ocs container started, so ocs may still use the old settings; run `chap-devops restart ocs` to apply it
chap-devops restart ocs returns before OCS answers again. Run chap-devops status until
ocs shows up. It worked when this answers 403:
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://localhost:8790/ingestions -d '{}'
A read-write OCS answers 422 to the same request. The base URL is different:
it is in compose.ocs.yml, so chap-devops up applies it. --base-url says what it
set:
enabled ocs on http://localhost:8790
set the ocs base URL to https://ocs.example.org
run `chap-devops up` to apply
If you give the same base URL again, nothing changes, and the command prints
no chap-devops up line:
ocs was already enabled on http://localhost:8790; nothing changed
It worked when the links in curl http://localhost:8790/stac/catalog.json
start with the base URL.
Both have init spellings too, --ocs-read-only and --ocs-base-url. A
read-only instance refuses every ingestion over HTTP, so ingest first.
Next: Standalone OCS, Data source credentials, Read-only instances, Behind a reverse proxy.
All shapes: Use cases.
A DHIS2 on its own
A DHIS2 instance and its database, nothing else: for trying a DHIS2 version, developing an app, or restoring a database dump to look at it.
chap-devops init dhis --only dhis2
cd dhis
chap-devops up # returns when the containers start
chap-devops status # run it again until the dhis2 line says up
chap-devops open dhis2
You get DHIS2 on http://localhost:8780, with its own PostgreSQL, and no
chap-core, OCS or models.
chap-devops up prints started dhis2, dhis2-db when the containers start. The
first chap-devops up also pulls the images, about 2 GB on disk, and downloads the
demo database, about 7 MB. DHIS2 then needs more time before it answers: less than
a minute on a fast machine, several minutes on a slow one. Until then,
chap-devops status shows this and exits with 1:
dhis2 starting http://localhost:8780
1 of 1 component is still starting; run `chap-devops status` again in a moment
It worked when chap-devops status shows this, and the login admin / district
works in the browser:
dhis2 up http://localhost:8780
dhis2 is up
DHIS2 needs about 4 to 5 GB of memory; give Docker 8 GB or more.
Picking the version
--dhis2-tag picks the DHIS2 version, and --dhis2-seed what its database
starts from:
| Version | Command | Database it starts with |
|---|---|---|
| 2.42 (the default) | chap-devops init dhis --only dhis2 | the Laos climate demo |
| 2.43 | chap-devops init dhis --only dhis2 --dhis2-tag 2.43 | empty |
| 2.41 | chap-devops init dhis --only dhis2 --dhis2-tag 2.41 | empty |
| the next, unreleased | chap-devops init dhis --only dhis2 --dhis2-image dhis2/core-dev --dhis2-tag master | empty |
Each command makes a new deployment dhis, and so do the commands in the
next section. If dhis already exists, init stops with already contains a chap-devops deployment; choose another directory, or use --force to overwrite it. Use a different name.
A full version such as --dhis2-tag 2.42.6 works too. chap-devops has a demo
database for 2.42 only, so 2.42 and every 2.42.x get it. Every other version
starts empty, and init tells you so:
warning: chap-devops knows no DHIS2 demo dump for 2.43, so `dhis2_db` starts empty; name one with `seed:` in `.chap-devops/components.yaml` (a URL or a path) and run `chap-devops sync`
An empty DHIS2 still has the admin / district login, as a superuser. A
version that is not on the machine yet adds a pull of 1 to 1.5 GB to the first
chap-devops up.
Picking the data
chap-devops init dhis --only dhis2 --dhis2-seed none # empty, whatever the version
chap-devops init dhis --only dhis2 --dhis2-seed dumps/mine.sql.gz # your own dump, a path
chap-devops init dhis --only dhis2 --dhis2-seed dumps/mine.sql.gz \
--dhis2-seed-password # every user logs in with district
chap-devops init dhis --only dhis2 --dhis2-tag 2.43 \
--dhis2-seed https://databases.dhis2.org/sierra-leone/2.43/dhis2-db-sierra-leone.sql.gz
A relative path is relative to the deployment directory, not to the directory
you run init in. For the commands above, put the dump at
dhis/dumps/mine.sql.gz before chap-devops up. Until the dump is there, init
tells you:
warning: the dhis2 seed names dumps/mine.sql.gz, which is not in /path/to/dhis; copy the dump there before `chap-devops up`, or run `chap-devops components enable dhis2 --seed none`
The seed is applied once, when the database is first created; after that the
database is DHIS2’s. DHIS2’s Sierra Leone demo is published for 2.41, 2.42 and
2.43 at that address pattern, and the first chap-devops up downloads about 90 MB
for it. It has no climate data, and its admin is not a superuser: it cannot create the route the Modeling App uses, so pick it for a
DHIS2 on its own, not for one you will connect to Chap.
A dump of a real DHIS2 keeps the passwords of its users.
--dhis2-seed-password gives every user the password district, turns on
every account and turns off two-factor login, so you can log in as any of
them. See
One password for every user.
Next
chap-devops dhis2 connect refuses here, since there is no chap-core to connect to:
error: chap-core is not a component of this deployment, so the route would point at a service that is not there; run `chap-devops components enable chap-core` first
chap-devops dhis2 show works here. Its target row says
none (this deployment has no chap-core), and it names no chap-devops dhis2 connect. When the apps or the analytics tables are missing, its last line is:
run `chap-devops dhis2 apps` for the apps, then `chap-devops dhis2 analytics` for the analytics tables; the `chap` route needs chap-core; `chap-devops components enable chap-core` adds it
chap-devops components enable chap-core adds one, then chap-devops up starts it, and
Chap with a local DHIS2 is that shape from the
start. More: The seed,
Changing the DHIS2 version,
Memory.
All shapes: Use cases.
A chapkit model service on its own
One forecasting model, running and answering on its own port, with no
chap-core in front of it. For developing or checking a model: calling its API
directly, reading its /docs, running chapkit test against it, or putting it
behind something other than chap-core.
The quickest way needs no folder at all:
chap-devops run chapkit_simple_multistep_model # a marketplace model
chap-devops run https://github.com/chap-models/chapkit_ghr_model # by repository
chap-devops run ghcr.io/my-org/chapkit_dengue_model:sha-1eb8cf1 # by image: use your own
chap-devops ps
chap-devops stop chapkit_simple_multistep_model
chap-devops stop --all
The image line is an example: my-org does not publish it, so that chap-devops run
stops with chapkit-dengue-model did not start: the registry answered `denied`.
Put an image of your own there. See
did not start: the registry answered denied.
Each of the other two commands prints a starting line, and one more line
when the model answers:
starting chapkit-simple-multistep-model (chapkit_simple_multistep_model in ~/.local/share/chap-devops/run/default)
running chapkit_simple_multistep_model on http://localhost:5001 (answered in 4s)
chap-devops prints the full path of the group. The docker compose progress goes to
stderr between the two lines. The repository line also says on stderr that the
URL is the marketplace model chapkit_ghr_model, and it answers on
http://localhost:5002.
chap-devops run keeps the model in a group under chap-devops’s data directory, publishes
it on 127.0.0.1 only, and returns once it answers. A first run pulls the
model image, and the pull can take some minutes. On disk, the image of Simple
Multistep is about 1 GB, and the image of GHRmodel about 7 GB. -a keeps it in the foreground with its log, until Ctrl-C stops it.
chap-devops stop ID stops one model, and chap-devops stop --all stops every model that
chap-devops run started. Both keep the data volume of each model. To remove the
volumes and the groups too, add --purge. See Running one model.
For a deployment directory of your own, with chap-devops status, chap-devops doctor and
everything else:
chap-devops init multistep --only none --models chapkit_simple_multistep_model
cd multistep
chap-devops up --wait
chap-devops status
curl http://localhost:5001/health
chap-devops open chapkit_simple_multistep_model
chap-devops up --wait returns once the model answers its own /health:
waiting up to 300s for the model to answer
started chapkit-simple-multistep-model
ready in 4s
chapkit-simple-multistep-model up http://localhost:5001
Without --wait, chap-devops up returns when the container starts, and for some
seconds chap-devops status says running, not answering. Then chap-devops status
shows:
MODEL STATE REACH LAST PING
chapkit-simple-multistep-model up port 5001 -
1 model up, answering on its own host port
The curl command gets the same /health that chap-devops status asks:
{"status":"healthy","checks":{"database":{"state":"healthy"},"registration":{"state":"healthy"}}}
chap-devops open chapkit_simple_multistep_model opens the model’s API documentation, at
http://localhost:5001/docs, in a browser. With --no-browser, it prints the
address instead:
the model's API documentation is at http://localhost:5001/docs
chap-devops open with no name opens nothing. It lists the components and each
model with a host port, with the address of its /docs.
Your own model works the same way, from its repository or a published image:
chap-devops init mymodel --only none
cd mymodel
chap-devops models add https://github.com/my-org/chapkit_dengue_model # adds and enables it
chap-devops up --wait
Replace the URL with your model’s repository. The repository must publish a
sha-<commit> image on ghcr, as the model repositories’ publish workflow does.
If the URL names a repository that does not exist, chap-devops models add stops
with GitHub has no repository my-org/chapkit_dengue_model, or it is private.
The chap-devops up --wait after it then says nothing to start. A repository that
the marketplace lists is enabled as that marketplace model; add --id NAME to
keep an entry of your own beside it. See
Models outside the marketplace.
Without chap-core each model gets a host port when it is enabled (the first
free one from 5001; --port N picks one), does not try to register anywhere,
and is judged by chap-devops status and chap-devops doctor through its own /health.
Near the start of its log, after Application startup complete, expect one
registration.missing_orchestrator_url line at error level and a
registration.skipped line. servicekit has nothing to register with, and the
model continues to serve. See
registration.missing_orchestrator_url in a model’s log.
chap-devops models test chapkit_simple_multistep_model (or --all) works here: it runs
chapkit test inside the model’s own container:
testing 1 model (model level; add --backtest to go through chap-core)
chapkit-simple-multistep-model pass 5s 1 training, 1 prediction
1 of 1 model passes
chap-devops models test --backtest and chap-devops jobs go through chap-core, so they
refuse and name chap-devops components enable chap-core.
Next: Model services without chap-core, Models outside the marketplace.
All shapes: Use cases.
Several model services side by side
Two or more chapkit model services in one deployment directory, with no chap-core. Each model gets its own host port and its own data volume, and the models do not depend on each other. For one model, see A chapkit model service on its own.
chap-devops init models --only none --models chapkit_simple_multistep_model,auto_arima_chapkit
cd models
chap-devops up --wait
chap-devops status
chap-devops models list --enabled # the PORT column says where each one answers
chap-devops init gives each model the next free port from 5001 up, and prints it:
created a deployment in /path/to/models
components: none
enabled chapkit_simple_multistep_model v0.1.2 on http://localhost:5001
enabled auto_arima_chapkit v1.0.2 on http://localhost:5002
run `cd /path/to/models && chap-devops up` to start the deployment
A first chap-devops up pulls the model images. On disk, the image of Simple
Multistep is about 1 GB, and the image of Auto-ARIMA about 6 GB. The pull can take some minutes. The images
are amd64 only, so on an arm64 machine they run under emulation.
chap-devops up --wait returns once each model answers its own /health. Without
--wait, chap-devops up returns when the containers start, and for some seconds
chap-devops status can say running, not answering. After compose’s own lines,
chap-devops up --wait ends with:
waiting up to 300s for the 2 models to answer
started auto-arima-chapkit, chapkit-simple-multistep-model
ready in 4s
auto-arima-chapkit up http://localhost:5002
chapkit-simple-multistep-model up http://localhost:5001
Then chap-devops status shows:
MODEL STATE REACH LAST PING
auto-arima-chapkit up port 5002 -
chapkit-simple-multistep-model up port 5001 -
all 2 models up, each answering on its own host port
It worked when chap-devops status shows every model as up, each on its own port.
Each model answers on http://localhost:<port>, with its API documentation at
/docs. The ids for --models are in the ID column of chap-devops models list.
That command also works outside a deployment directory.
Compose publishes these ports on every address of the machine, and the models have no login. To keep a model on this machine only, see Which address a model port is published on.
Without a folder, chap-devops run does the same one model at a time:
Any number of model services, no folder.
All shapes: Use cases.
Any number of model services, no folder
One chapkit model or ten, each answering on its own port, with no chap-devops init,
no deployment directory and no chap-core. For calling models’ APIs directly,
comparing several side by side, or a tool (a UI, a notebook, a test harness)
that only needs “start this model and tell me where it answers”.
From any directory that is not a deployment:
chap-devops run chapkit_simple_multistep_model
chap-devops run auto_arima_chapkit
chap-devops run https://github.com/chap-models/chapkit_ghr_model
chap-devops ps # every model, its state and its URL
Each chap-devops run returns once the model answers on its /health and prints
where it answers. Between the two lines, docker compose shows its progress:
starting chapkit-simple-multistep-model (chapkit_simple_multistep_model in /home/me/.local/share/chap-devops/run/default)
...
running chapkit_simple_multistep_model on http://localhost:5001 (answered in 4s)
The first run of a model pulls its image. A marketplace model image is several gigabytes, so this can take many minutes; a later run starts in seconds.
The GitHub URL above is a model that the marketplace lists. Before the
starting line, chap-devops says so, then reads the user of the image (this pulls
the image), and then starts that marketplace model:
https://github.com/chap-models/chapkit_ghr_model is the marketplace model chapkit_ghr_model; enabling that (pass `--id <other>` to add it beside it instead)
asking ghcr.io/chap-models/chapkit_ghr_model:sha-dfb2e3f what uid `app` is (this pulls the image)
Each model gets the next free port from 5001 up, its own container and its own
data volume, published on 127.0.0.1 only. The models do not depend on each
other. A marketplace id, a GitHub repository URL, a ghcr image and a local
name:tag are all accepted; see
Running one model.
It worked when chap-devops ps lists every model as up, each with its own URL:
GROUP ID SERVICE STATE URL
default auto_arima_chapkit auto-arima-chapkit up http://localhost:5002
default chapkit_ghr_model chapkit-ghr-model up http://localhost:5003
default chapkit_simple_multistep_model chapkit-simple-multistep-model up http://localhost:5001
chap-devops top shows the same, live.
Starting many at once
--no-wait returns as soon as the container starts, so a whole set can start
in parallel. Runs into the same group take turns where they have to and never
step on each other; a run that waits for its turn says another chap-devops command is changing this deployment; waiting for it to finish.
for m in chapkit_simple_multistep_model auto_arima_chapkit chapkit_ghr_model; do
chap-devops run "$m" --no-wait &
done
wait
chap-devops ps # `running, not answering` until each one is ready
With --no-wait, each run prints started in place of running. The runs
take their turns in no fixed order, so the ports can go to the models in a
different order each time. Read each URL from chap-devops ps or from --json.
Running a model that already runs starts nothing new: it reports the same id
and port. A repository or image you ran before is that same model again, not a
copy; --id auto adds a second copy beside it when you want one. A run by the
GitHub URL of a marketplace model works as a run by its id: the second run
does not read the image again, recreate the container, or move the port.
Keeping sets apart: groups
Without a folder, chap-devops keeps the models in a group under its data
directory, default unless you name one. A group per experiment keeps sets
apart, and every group can be listed and stopped on its own:
chap-devops run chapkit_simple_multistep_model --group dengue
chap-devops run auto_arima_chapkit --group dengue
chap-devops run chapkit_simple_multistep_model --group malaria # one more, separate copy
chap-devops ps --group dengue
GROUP ID SERVICE STATE URL
dengue auto_arima_chapkit auto-arima-chapkit up http://localhost:5005
dengue chapkit_simple_multistep_model chapkit-simple-multistep-model up http://localhost:5004
The same model in two groups is two containers with two ports and two data
volumes. Group names are lowercase letters, digits, - and _.
A group is an ordinary chap-devops deployment that chap-devops keeps for you, so the other
commands reach it with -C. The starting line of chap-devops run names the
group directory, and chap-devops -v run prints the full log command:
chap-devops -C ~/.local/share/chap-devops/run/dengue logs chapkit-simple-multistep-model
The path is the default data directory; $CHAP_DEVOPS_DATA_DIR or $XDG_DATA_HOME
moves it (Where it runs: groups).
For a group that does not exist, chap-devops ps --group says
there is no `chap-devops run` group X; `chap-devops ps` lists every group.
From a script
Under --json each command prints one JSON document on stdout, and docker’s
progress goes to stderr:
chap-devops --json run chapkit_simple_multistep_model --group dengue
The fields a tool needs are url, port, id, group and project_dir; a
failure is {"ok": false, "error": ..., "hint": ...} with a non-zero exit. See
Running one model for every field and
--json for scripts and tools.
Stopping
chap-devops stop chapkit_ghr_model # one model, in whichever group has it
chap-devops stop chapkit_simple_multistep_model --group malaria # one model in one group
chap-devops stop --group dengue # every model in one group
chap-devops stop --all # every model in every group
chap-devops stop --group dengue --purge # and its data, and the group itself
An id alone works only when one group has that model. After the steps above,
chapkit_simple_multistep_model runs in three groups, so chap-devops stop chapkit_simple_multistep_model
stops nothing and says:
error: chapkit_simple_multistep_model runs in the groups default, dengue, malaria; name one with `--group default`
A stop keeps the model’s data volume, so the next chap-devops run of it continues
with the same data. The port can be a different one. --purge deletes the
data too, and removes a group it leaves empty: its network, its directory and
every volume it had, including those of models stopped earlier. To remove
every group and all its data, use chap-devops stop --all --purge.
In the order above, dengue has no running model when --purge comes, so
chap-devops says:
nothing was running to stop
removed group dengue
chap-devops -v stop also names each volume that it removed.
When a folder is the better fit
If you often reach a group with -C, or want chap-core, OCS or DHIS2 next to
the models, use a folder of your own instead:
Several model services side by side.
To reach the models from other machines, start them with --bind 0.0.0.0.
All shapes: Use cases.
A new model of your own
For starting a new forecasting model: chap-devops models new makes the project,
Docker builds its image, and a chap-devops deployment trains it, predicts with it
and scores it through chap-core. Python, uv, R and the chapkit CLI are not
necessary on the machine.
Make the project
chap-devops models new dengue-lags
created dengue-lags in dengue-lags (fn-py template, service id dengue-lags)
next: build the image as `dengue-lags/README.md` says, then run `chap-devops run dengue-lags:dev`
The default type, fn-py, is a chapkit service with the model in Python, in
main.py. For a model in R scripts, add --template shell-r. For a model in
the MLproject format, use --template mlproject-py or --template mlproject-r. The types lists all nine.
The example model predicts, for each location, the mean of disease_cases in
the last window periods. Replace it with your model. README.md in the
project lists the files to change.
Build the image
In the project:
cd dengue-lags
git init && git add . && git commit -m "feat: first version"
docker build --platform linux/amd64 --build-arg GIT_REVISION=$(git rev-parse HEAD) -t dengue-lags:dev .
cd ..
chap-core stores a model only if the image has a git revision, so build from
a commit with GIT_REVISION. The first build pulls the base image,
ghcr.io/dhis2-chap/chapkit-py. A rebuild takes some seconds.
Run it with chap-core
chap-devops init lab --models none
cd lab
chap-devops models add dengue-lags:dev
chap-devops up --wait
chap-devops status
chap-devops up --wait returns when the model has registered, and it creates the
configured model default. chap-devops status then shows:
chap-core up http://localhost:8700 2.4.0 auth: off
MODEL STATE REACH LAST PING
dengue-lags registered via chap-core 3s ago
1 model registered
Test it through chap-core
chap-devops models test dengue_lags --backtest --seed 7
testing 1 model (through chap-core: a dataset, a backtest and its scores)
dengue-lags pass 16s crps 35.5 mae 35.5 rmse 38.8
configured model: default (id 14)
1 of 1 model passes
The test makes a dataset from the model’s own sample data, runs a backtest in
chap-core, and prints its scores. --seed makes the data the same on every
run, so two runs can be compared.
Give an option another value
window is an option of the model, with 3 as the default. A configured model
gives it another value:
chap-devops models configs add dengue_lags --name long --set window=6
chap-devops models test dengue_lags --backtest --seed 7 --config long
created configured model long of dengue_lags
testing 1 model (through chap-core: a dataset, a backtest and its scores)
dengue-lags pass 13s crps 41.4 mae 41.4 rmse 43.2
configured model: long (id 15)
1 of 1 model passes
The Modeling App shows long as a model to pick, next to default. See
Configured models.
After a change
chap-core stores each version of a model only once. If you build a new commit under the same version, the model registers, but its backtests fail:
dengue-lags FAIL 8s ValueError: Model template 'dengue-lags' version '0.1.0' is stored from revision 'db139b6b09185e0be0971d31cafb39f126c910...
So, before you build again, raise version in main.py (in MLproject for
an MLproject type), for example to 0.1.1. Then commit, and build with the
same docker build line. In lab:
chap-devops restart
chap-devops models test dengue_lags --backtest --seed 7
dengue-lags pass 13s crps 39.5 mae 39.5 rmse 42.3
configured model: default (id 16)
created configured models in chap-core for dengue_lags (default)
1 of 1 model passes
In this run, the change was window: int = 4 in main.py, so the score is
different. The backtest created the configured model default of version 0.1.1. A configured
model belongs to one version, so long is not there for 0.1.1: add it again
with chap-devops models configs add.
To run the model service alone, with no deployment: chap-devops run dengue-lags:dev. To publish the image, push the project to GitHub: the
workflow in .github/workflows/publish.yml builds it and publishes it to GHCR.
Then chap-devops models add takes the repository URL, as in
Models outside the marketplace.
These lines come from chap-devops 0.101.0 with this change, chap-core 2.4.0 and chapkit 2.3.1 on macOS (arm64, the image under amd64 emulation), on 2026-10-08.
More: A new model project.
All shapes: Use cases.
An MLproject model, with no image to build
For a model in the MLproject format: a directory with an MLproject file and
the train and predict scripts. chap-devops chapkit mlproject run serves it as a
chapkit service from the directory, so you can evaluate it, test it through
chap-core and change it without a docker build. When the model is ready,
chap-devops chapkit mlproject migrate makes it a chapkit service with an image of
its own, and chap-devops chapkit mlproject export goes the other way. Python, uv,
R and the chapkit CLI are not necessary on the machine.
mlproject run uses chapkit images of its own, so it works with chap-core
v2.4.0 and later. export, --run-config.as-chapkit and the up-to-date
migrate need chapkit 2.4.0 in the chap-core image, which chap-core v2.5.0
has.
Make or bring the model
A model from chap-models, or your own, works as it is. For a new one:
chap-devops models new rain-model --template mlproject-r
cd rain-model
git init && git add . && git commit -m "feat: first version"
cd ..
created rain-model in rain-model (mlproject-r template, service id rain-model)
next: run `chap-devops chapkit mlproject run rain-model` to serve it with no build, or build the image as `rain-model/README.md` says
For Python, use --template mlproject-py. The commit is not necessary to
serve the model, but chap-core stores a model only with a git revision. See
In a deployment.
Serve it
chap-devops chapkit mlproject run rain-model
serving /home/me/rain-model with `chapkit mlproject run` in ghcr.io/dhis2-chap/chapkit-r-tidyverse-cli:latest (from the train command `Rscript scripts/train.R --data {train_data} --model {model} --config {model_config}`; --image picks another)
starting it on http://localhost:5001; the logs follow, and Ctrl-C stops it
the MLproject service answers at http://localhost:5001
It worked when the last line says the MLproject service answers at. The
service runs in the foreground, and the logs of chapkit follow. Leave it
running, and use a second terminal for the next steps. Ctrl-C stops it:
stopped the MLproject service on http://localhost:5001
The train command selects the image: python or uv gets py-data, and
Rscript gets r-tidyverse. A model that needs INLA gets
--image r-inla. To see the images, run chap-devops chapkit --image:
NAME IMAGE PLATFORMS LOCAL WHAT IT HAS
py ghcr.io/dhis2-chap/chapkit-py-cli:latest amd64, arm64 yes Python
py-data ghcr.io/dhis2-chap/chapkit-py-data-cli:latest amd64, arm64 yes Python, numpy, pandas, scipy, scikit-learn, statsmodels, ...
r ghcr.io/dhis2-chap/chapkit-r-cli:latest amd64, arm64 no R, renv, pak
r-tidyverse ghcr.io/dhis2-chap/chapkit-r-tidyverse-cli:latest amd64, arm64 yes R, tidyverse, fable, tsibble, forecast, xgboost, ...
r-inla ghcr.io/dhis2-chap/chapkit-r-inla-cli:latest amd64 yes R, tidyverse, INLA, sf, spdep, dlnm, ...
chap-devops does not install the packages that uv_env or renv_env names. If
the model needs one that the image does not have, give an image that has it.
See Serve an MLproject model.
Evaluate it
The chap CLI runs in a container, so it reaches the service at
host.docker.internal, not at localhost:
chap-devops chap eval --model-name http://host.docker.internal:5001 \
--dataset-csv laos_subset.csv --output-file rain-model.nc
model: http://host.docker.internal:5001 answers
chap finished; it wrote rain-model.nc
`chap-devops chap plot-backtest rain-model.nc --output-file rain-model.html` plots it
It worked when the line says chap finished; it wrote rain-model.nc. The
data comes from
Evaluating a model on your own data.
chap can also run the directory itself, without the service. With
--run-config.as-chapkit, chap serves it with chapkit in its own container,
and installs what uv_env or renv_env names:
chap-devops chap eval --model-name ./rain-model --run-config.as-chapkit \
--dataset-csv laos_subset.csv --output-file rain-model.nc
In a deployment
In a deployment that runs chap-core, the service registers with chap-core,
and the commands of the deployment use it by its id, the name in
MLproject:
chap-devops init lab --models none
cd lab
chap-devops up --wait
chap-devops chapkit mlproject run ../rain-model
the MLproject service answers at http://localhost:5001; it registers with chap-core in this deployment, which reaches it at http://chap-devops-mlproject-18970-0:8000
In a second terminal, in lab:
chap-devops models configs sync rain-model
chap-devops models test rain-model --backtest
rain-model: created configured model default
testing 1 model (through chap-core: a dataset, a backtest and its scores)
rain-model pass 13s crps 31.4 mae 31.4 rmse 37.5
configured model: default (id 21)
1 of 1 model passes
It worked when the last line says 1 of 1 model passes.
With DHIS2 in the deployment, the Modeling App shows the configured model
while the service runs. Select model lists it by its name, with the
commit that chap-devops gave chapkit:
An evaluation runs as for any other model. For rain-model, map only
Disease cases, because the model has no covariates. See
Your first forecast in the Modeling App. This was tested
with Modeling App 7.3.0, DHIS2 2.42.6 and chap-core v2.5.0, after a
chap-devops update from v2.4.0: the evaluation of the 18 provinces, 2019 to 2024,
took 19 seconds.
chap-devops gives chapkit the git commit of the model directory, and chap-core
stores the version of the model with it. After a change, commit it, and
raise version in MLproject, before you serve the model again. If the
directory has changes that are not committed, chap-devops warns.
chap-devops status lists the service as unmanaged, because chap-devops does not
manage it:
MODEL STATE REACH LAST PING
chapkit-simple-multistep-model registered via chap-core 11s ago
rm1 unmanaged http://chap-devops-mlproject-85595-0:8000 6s ago
Make it a chapkit service
When the model is ready to publish, make it a chapkit service with an image
of its own. Commit first: the migration moves files to _old/. Then, in the
model directory:
cd rain-model
chap-devops chapkit mlproject migrate . --yes
docker build --platform linux/amd64 --build-arg GIT_REVISION=$(git rev-parse HEAD) -t rain-model:dev .
chap-devops run rain-model:dev
chap-devops -C ~/.local/share/chap-devops/run/default models test rain_model
It worked when the last line says 1 of 1 model passes. chapkit finds the
scripts in scripts/ through the train and predict commands, and picks the
base image from them. chap-core v2.5.0 and older need --base-image. See
Migrate an MLproject model.
And back to an MLproject
A chapkit service that runs scripts, such as one from the shell-py or
shell-r template, can write the MLproject of the same model:
cd my-service
chap-devops chapkit mlproject export main.py -o MLproject
chap-devops chapkit mlproject run .
chapkit finished; it wrote MLproject
The exported MLproject runs with chap-devops chapkit mlproject run and with
--run-config.as-chapkit, not with chap-devops chap alone. See
Export a service as an MLproject.
What was tested
On chap-core master before v2.5.0 (chapkit 2.4.0), macOS arm64, each row
was served with chap-devops chapkit mlproject run and evaluated with
chap-devops chap eval. On v2.5.0, the mlproject-py template was migrated and
tested again, and the shell-r template was exported, served and evaluated
again, with no --tag:
| Model | Image | Result |
|---|---|---|
mlproject-py template | py-data (default) | pass |
mlproject-r template | r-tidyverse (default) | pass |
shell-py and shell-r templates, exported | the default | pass |
minimalist_configurable_model | py-data (default) | pass |
minimal_template_example | py-data (default) | pass |
Xiang_SVM | py-data (default) | pass |
mean | r-tidyverse (default) | pass |
XGBoost_for_Malawi | r-tidyverse (default) | pass |
INLA_baseline_model | --image r-inla | pass |
ewars_per_district | --image r-inla | pass |
chap_pymc | py-data (default) | fails: the image has no cyclopts, which its uv_env names |
IMPA-TECH-SARIMA | py-data (default) | fails in the model, also without chap-devops |
rwanda_random_forest | py-data (default) | fails in its train script |
Your model from its checkout, with Chap
For developing a chapkit model: chap-devops runs chap-core, and you run the model
from its own checkout on this machine (with uv run, a debugger, hot reload),
registered with that chap-core like any deployed model. Nothing about the model
has to be built into an image.
chap-devops init mychap --models none
cd mychap
chap-devops up --wait
chap-devops status
chap-devops up --wait returns once chap-core answers. Without --wait, chap-devops up
returns when the containers start, and for some seconds chap-devops status says
starting. Then chap-devops status shows chap-core up and no models yet:
chap-core up http://localhost:8700 2.4.0 auth: off
no models enabled; run `chap-devops models enable ID` to add one
Start the model
Then, in the model’s checkout, start it on a port of its own and tell it where chap-core is and where chap-core can call it back:
export SERVICEKIT_ORCHESTRATOR_URL='http://localhost:8700/v2/services/$register'
export SERVICEKIT_HOST=host.docker.internal
export SERVICEKIT_PORT=8001
export GIT_REVISION=$(git rev-parse HEAD)
uv run uvicorn main:app --host 0.0.0.0 --port 8001
main:app is the app in a main.py at the top of the checkout, which is the
layout chapkit models use; a model whose main.py is inside a package runs as
PACKAGE.main:app instead. The first uv run in a checkout makes its .venv
and installs the dependencies of the model.
SERVICEKIT_ORCHESTRATOR_URLis where the model registers. It runs on this machine, solocalhostand the API port reach chap-core. The single quotes keep the shell from reading$registeras a variable.SERVICEKIT_HOSTandSERVICEKIT_PORTare the address the model registers itself under, which is where chap-core calls it. chap-core runs in a container, wherelocalhostis the container itself;host.docker.internalis this machine. The chap-core compose file maps that name for the chap-core containers on Linux too.GIT_REVISIONis the commit the model reports as itsgit_revision. chap-core v2.4.0 stores the model template only for a service that reports a revision. Without it, the model registers, but chap-core has no template for it.chap-devops statuswarns:SERVICE_ID: it reports no git revision, so chap-core stores no model template for it; set `GIT_REVISION` where it runs, then start it again.chap-devops models configs syncfails withanswered HTTP 409 Conflictand gives the same way out. The chap-core log (chap-devops logs chap) saysis stored from revision None, but its source now reports revision None.--host 0.0.0.0makes the model listen on more than the loopback, which is what a call from a container arrives on.
It worked when chap-devops status lists the model’s service id with a recent LAST
PING. For the minimalist example in ~/dev/chap-models, the status is:
chap-core up http://localhost:8700 2.4.0 auth: off
MODEL STATE REACH LAST PING
chapkit-minimalist-example-py unmanaged http://host.docker.internal:8001 7s ago
no models enabled here; the unmanaged one above registered from outside this deployment
Its state is unmanaged, because this deployment did not start it; that is
expected.
chap-core stores one template for each model version, with the revision it
had at the first registration. Changes you do not commit keep the revision, so
hot reload needs nothing more. If you commit and export GIT_REVISION again,
the revision changes. chap-core then refuses the backtest with is stored from revision '...', but its source now reports revision '...'. The backtest row
shows the start of that reason, and chap-devops jobs logs JOB_ID shows all of it.
chap-devops status warns: SERVICE_ID: chap-core stores its model template VERSION from another git revision and refuses to run it; set a new version in the model, then start it again. To continue:
- Set a new
versionin theMLServiceInfoinmain.py. - Start the model again.
- Run
chap-devops models configs sync SERVICE_IDagain.
The configured model from before belongs to the old version, and chap-core
does not run it. Without step 3, chap-devops status warns: SERVICE_ID: chap-core has configured models of it of other versions only, not of VERSION; run `chap-devops models configs sync SERVICE_ID`. The backtest skips with chap-core has no configured model for SERVICE_ID VERSION.
Test it through chap-core
The model level of chap-devops models test (chapkit test in a container) is for
models that chap-devops runs. For this model, chap-devops models test SERVICE_ID skips,
names --backtest, and exits with status 1:
testing 1 model (model level; add --backtest to go through chap-core)
chapkit-minimalist-example-py skip 0s chap-devops does not run it, so there is no container to test it in
run `chap-devops models test chapkit-minimalist-example-py --backtest` to test it through chap-core
0 pass, 1 skipped
warning: no model was tested, because every one was skipped; the line under each row says why
A backtest uses a configured model: a template with a set of option values.
chap-core v2.4.0 makes no configured model from a registered service, so make
one. Replace SERVICE_ID with the id that chap-devops status shows:
chap-devops models configs sync SERVICE_ID
chap-devops models test SERVICE_ID --backtest
chap-devops models configs sync stores the template of the service and makes one
configured model, default, with the defaults of the service. chap-core names
it SERVICE_ID:
SERVICE_ID: created configured model default
If the model has options, or reads covariates that it does not declare, make
the configured model through the API instead (this example uses jq):
TEMPLATE_ID=$(chap-devops api GET /v1/crud/model-templates | jq '.[] | select(.name == "SERVICE_ID") | .id')
chap-devops api POST /v1/crud/configured-models --data "{\"name\": \"dev\", \"modelTemplateId\": $TEMPLATE_ID, \"additionalContinuousCovariates\": [\"rainfall\", \"mean_temperature\"]}"
chap-core names that configured model SERVICE_ID:dev. Add the options as
"userOptionValues". If the template id is empty, chap-core has no template
for the model; make sure that GIT_REVISION was set when the model started.
The backtest uses the configured model SERVICE_ID if there is one, and else
SERVICE_ID:dev. To test dev when both exist, add --config dev. The line
under the backtest row names the configured model that the backtest used.
It worked when the backtest passes and prints its scores:
testing 1 model (through chap-core: a dataset, a backtest and its scores)
chapkit-minimalist-example-py pass 13s crps 16.7 mae 16.7 rmse 18.0
configured model: default (id 15)
1 of 1 model passes
The scores change from run to run, because the sample data is random. Add
--seed N to get the same data each time.
Without the configured model, the backtest skips with chap-core has no configured model for SERVICE_ID VERSION, and names chap-devops models configs sync. The backtest uses only a configured model of the version that the model
registered with.
A registration key
If the deployment has a registration key (chap-devops auth show says
Registration key on), export SERVICEKIT_REGISTRATION_KEY before you
start the model. Its value is the line of that name in the deployment’s .env:
export $(grep '^SERVICEKIT_REGISTRATION_KEY=' /path/to/mychap/.env)
Without the key, chap-core answers 401 Unauthorized. The model log shows
registration.attempt_failed five times, then registration.failed, and the
model does not try again until you start it again. chap-devops status does not
list the model.
Restarts
The model keeps its registration alive with a ping every 10 seconds. chap-core
keeps the registrations in redis, so a restart of chap alone keeps them.
If chap-core loses the registration, for example when redis restarts too,
the model log shows keepalive.service_not_found. The model registers again
30 seconds after that, so about 40 seconds after the restart, with nothing to
do on your side. When you are done with the model, the image route is
Models outside the marketplace.
All shapes: Use cases.
chap-core from its checkout, with the models
For developing chap-core: you run chap-core from its own checkout on this machine, and chap-devops runs the model services around it, registered with your chap-core instead of one of its own.
-
Start chap-core from its checkout as you normally do. Make sure that it listens on
0.0.0.0:8000, so that the model containers can reach it. -
Make the deployment and start the models:
chap-devops init models --chap-core-url http://localhost:8000 --models chapkit_simple_multistep_model cd models chap-devops up
--chap-core-url leaves out chap-devops’s own chap-core, with its worker, Valkey
and PostgreSQL. Your chap-core uses the database and the Valkey that you give
it. chap-devops records the URL of your chap-core, so:
- Each model registers with your chap-core. Its overlay sets
SERVICEKIT_ORCHESTRATOR_URLto your URL, withlocalhostturned intohost.docker.internal, which is this machine from inside a container. - Your chap-core calls each model back on its host port. Every model gets
one (5001 up), and registers itself as
localhost:<port>, which is right for a chap-core running on this machine. chap-devops reads each image’s command, so the host port reaches the app: an image that starts on--port 8000gets its host port mapped to 8000, and one that readsPORTis told the host port. Every marketplace model registers in this shape. - The chap-core commands talk to yours.
chap-devops statusasks it for its health and the registered models, andchap-devops jobs,chap-devops apiandchap-devops models test --backtestsend their requests there.chap-devops models testwithout--backtesttests each model inside its container, and does not need chap-core.
chap-devops status # your chap-core, and the models registered with it
chap-core up http://localhost:8000 2.5.0.dev0 auth: off
MODEL STATE REACH LAST PING
chapkit-simple-multistep-model registered, not configured port 5001 6s ago
1 model registered
chapkit-simple-multistep-model: chap-core has no configured model for it, so nothing can run it; run `chap-devops models configs sync`
It worked when chap-devops status shows chap-core up at your URL and the models
registered. The version is the one that your checkout reports. A model
registers some seconds after chap-devops up. Until then, chap-devops status shows it
running, not registered and says started under two minutes ago; run
chap-devops status again in a moment.
registered, not configured is the normal state after a plain chap-devops up.
chap-core makes no configured model when a model registers. A backtest and the
Modeling App need one. To make them in your chap-core, run chap-devops models configs sync:
chap-devops models configs sync
chap-devops models test --all
chapkit_simple_multistep_model: created configured model monthly_climate
chapkit_simple_multistep_model: created configured model monthly_selfhistory
testing 1 model (model level; add --backtest to go through chap-core)
chapkit-simple-multistep-model pass 6s 1 training, 1 prediction
1 of 1 model passes
After that, chap-devops status shows the model as registered, and
1 model registered. The configured models are in the database of your
chap-core, so they stay when you restart it. See
Configured models.
Start chap-core before chap-devops up. A model tries to register five times in
its first seconds, and then it stops. If chap-core starts after that, the
model stays running, not registered. After two minutes, the first line
under the summary of chap-devops status gives the way out:
chapkit-simple-multistep-model: restart it with `chap-devops restart --all chapkit-simple-multistep-model`
To make every model of the deployment register again, run chap-devops restart --all. A restart of chap-core needs nothing more. Each registered model pings
chap-core about every 15 seconds. If chap-core does not know the model, the
model registers again, in about a minute. See
A model is running but not registered.
Without a deployment directory, chap-devops run --chap-core does the same for
one model at a time:
chap-devops run -a --rm --chap-core http://localhost:8000 https://github.com/chap-models/chapkit_ghr_model
If the image is not on this machine, the first run pulls it, and that can take some minutes: this image is about 7 GB.
running chapkit_ghr_model on http://localhost:5002 (answered in 2s)
registered with http://localhost:8000
following the log of chapkit-ghr-model; Ctrl-C stops it
It worked when the lines after the start say registered with http://localhost:8000. Ctrl-C stops the model, and --rm also removes its
data: chap-devops prints stopped chapkit_ghr_model, and removed its data, then
removed group default when the group has no other model. See
Running one model.
To point an existing deployment at your chap-core, or back:
chap-devops components disable chap-core # stop chap-devops's own, if it has one
chap-devops components enable chap-core --url http://localhost:8000
chap-devops components list # chap-core: external
chap-devops up
chap-devops components disable chap-core # forget it again
chap-devops components enable chap-core # or run chap-devops's own again
chap-devops components disable chap-core stops and removes the containers of
chap-core and of the models. The next chap-devops up starts the models again.
It also gives each model a host port, because your chap-core calls the models
there. When chap-devops runs its own chap-core again, the models keep that port.
chap-devops components enable chap-core says so:
the models keep their host ports (chapkit_simple_multistep_model on http://localhost:5001); `chap-devops models unexpose ID` removes one
The REACH column of chap-devops status then shows the port, not via chap-core.
To remove the port, run chap-devops models unexpose ID.
If your chap-core is itself a container (another compose project, which is
what make restart in chap-core’s checkout starts), it cannot call the models
at localhost. The models must register as host.docker.internal.
chap-devops init --chap-core-url and chap-devops components enable chap-core --url find
this themselves if a container publishes the port of the URL at that time.
chap-devops init and chap-devops components enable tell it in a warning, for example:
warning: chap-core at http://localhost:8700 is the container `own-dd1b2e-chap-1`, so it calls the models back at host.docker.internal; if it is a process on this machine instead, run `chap-devops components enable chap-core --url http://localhost:8700 --models-host localhost`
If chap-devops did not find it, do these steps:
- Run
chap-devops components enable chap-core --url URL --models-host host.docker.internal. - Run
chap-devops up.
Without --models-host, the models still register, and chap-devops status shows
them registered, unreachable, with this command on the line under the table.
If chap-core runs on another machine, run chap-devops components enable chap-core --url URL --models-host HOST, with HOST the name or address that machine
reaches this one at.
If your chap-core requires a registration key or an API token, do these steps:
- If your chap-core checks an API token, run
chap-devops auth enable --token TOKENwith its token. If not, runchap-devops auth enable. - In
.env, set the lineSERVICEKIT_REGISTRATION_KEYto the key of your chap-core. - Run
chap-devops up.
For a chap-core elsewhere, chap-devops auth enable says what it does not do:
API authentication is on
run `chap-devops up` to restart the models with authentication
chap-devops does not change the chap-core at http://localhost:8000; set `SERVICEKIT_REGISTRATION_KEY` in `.env` to its registration key
After that, chap-devops status shows auth: on. This means that chap-devops sends a
token. It does not mean that your chap-core checks it.
chap-devops auth enable writes an API token and a registration key to .env, and
every overlay then passes SERVICEKIT_REGISTRATION_KEY from .env. chap-devops
sends the API token to your chap-core in chap-devops status, api, jobs and
models test --backtest. If authentication is already on, chap-devops auth enable --token with another token stops with --token was not used: API authentication is already on with another token, and changes nothing. To give
a different token, run chap-devops auth disable, then chap-devops auth enable --token TOKEN. The registration key in .env stays. See
Authentication.
More: A chap-core elsewhere.
All shapes: Use cases.
chap-core built from its checkout
For testing a chap-core change the way it will ship: chap-devops builds the chap-core
and worker images from your checkout and runs them with everything else it
normally runs (PostgreSQL, Valkey, the models, and OCS or DHIS2 if you add them
with --with).
Replace ~/dev/chap-core with the path of your chap-core checkout:
chap-devops init mychap --source ~/dev/chap-core --models chapkit_simple_multistep_model
cd mychap
chap-devops up # builds both images from the checkout, then starts them
chap-devops status
The images are linux/amd64, so on an Apple Silicon Mac the build runs under
emulation. The first chap-devops up pulls the base images and builds both images,
which took two to five minutes on an Apple Silicon Mac. It prints the full
docker build log, and ends with the services it started:
started chap, chapkit-simple-multistep-model, postgres, redis, worker
chap-core up http://localhost:8700 2.5.0.dev0 auth: off
MODEL STATE REACH LAST PING
chapkit-simple-multistep-model registered, not configured via chap-core 1s ago
1 model registered
chapkit-simple-multistep-model: chap-core has no configured model for it, so nothing can run it; run `chap-devops models configs sync`
It worked when chap-devops status shows chap-core up and the model registered.
The version is the one that your checkout reports.
registered, not configured is the normal state after a plain chap-devops up.
chap-core makes no configured model when a model registers. A backtest and the
Modeling App need one. To make them, run chap-devops models configs sync. After that,
chap-devops status shows the model as registered. See
Configured models.
compose.ymlcomes from the checkout’s owncompose.ghcr.yml, re-read on everychap-devops sync, so the services match the code you are building.- Every
chap-devops upbuilds the checkout as it is now.compose.chap-devops.ymlgiveschapandworkerabuild:from the checkout andpull_policy: build. Docker’s layer cache makes a build with nothing changed take some seconds, andchap-devops upthen saysalready running: .... After a change in the code, the build installs the dependencies again (about a minute on an Apple Silicon Mac), andchap-devops uprecreates only what moved:started chap, worker. - The images are
<project>-chap:checkoutand<project>-worker:checkout, named after the deployment’s compose project (such asmychap-77c0c2-chap:checkout), so a build never overwrites a releasedghcr.io/dhis2-chap/...tag on this machine, and two deployments built from two checkouts never overwrite each other’s. The worker image is about 12 GB. - There is no pin.
chap-devops updatestill moves the models and components, and refuses--chap-tag: check out the version you want in the checkout instead.
The checkout has to have Dockerfile, Dockerfile.worker and
compose.ghcr.yml, which every chap-core clone does; anything else is refused
before a file is written (see
is not a chap-core checkout).
Go back to a released chap-core
.env and the data are kept. Do these steps in the deployment directory:
-
Stop the deployment:
chap-devops down -
Run
initagain without--source, with the same--withand--modelsvalues you used:chap-devops init . --force --models chapkit_simple_multistep_modelinitkeeps.env, and moves itsCHAP_IMAGE_TAGline fromcheckoutto the released tag. It says so, and prints that tag:created a deployment in /home/me/mychap moved CHAP_IMAGE_TAG in the kept .env from checkout to v2.4.0 components: chap-core chap-core: v2.4.0, API on http://localhost:8700 enabled chapkit_simple_multistep_model v0.1.2 at http://localhost:8700/v2/services/chapkit-simple-multistep-model/run/ run `cd /home/me/mychap && chap-devops up` to start the deployment -
Start the deployment:
chap-devops up
It worked when chap-devops status shows the released version, such as 2.4.0.
Some seconds after chap-devops up, chap-devops status can show the model as
running, not registered and exit with 1. If it does, run chap-devops status again
in a minute. If the checkout added a database migration, the released chap-core
may not start on that data.
The images built from the checkout stay on this machine. To get the disk space
back, remove them with docker image rm <project>-chap:checkout <project>-worker:checkout. <project> is the name: line in
compose.chap-devops.yml, such as mychap-77c0c2.
More: chap-core from its checkout, with the models runs chap-core as a process instead, without building images.
All shapes: Use cases.
A DHIS2 you run yourself, with Chap from chap-devops
For DHIS2 or Modeling App development: you run DHIS2 your own way on this
machine (from its source, or its own compose project), and chap-devops runs chap-core
and the models. chap-devops dhis2 connect points the chap route of your DHIS2 at
this chap-core.
chap-devops init mychap --models chapkit_simple_multistep_model
cd mychap
chap-devops up # returns when the containers start
chap-devops status # run it again until the chap-core line says up
The first chap-devops up on a machine pulls the images: about 6 GB to download and
about 20 GB on disk (see Quickstart). When the images are
on the machine, up returns in seconds, and chap-core answers some seconds
later.
The login
chap-devops has no default login for a DHIS2 it did not start. .env has the three
lines as comments at its end. Before you record your DHIS2:
- Remove the
#fromDHIS2_ADMIN_USERNAMEandDHIS2_ADMIN_PASSWORD. - Set the login of a DHIS2 superuser. For a local dev DHIS2, this is often
adminanddistrict.
Or set DHIS2_API_TOKEN to a personal access token of that user. Without a
login, chap-devops dhis2 use ends with this warning, and chap-devops dhis2 connect
stops with the same text as an error::
warning: chap-devops has no credentials for this DHIS2, and did not deploy it, so there is no default to try; set `DHIS2_ADMIN_USERNAME` and `DHIS2_ADMIN_PASSWORD`, or `DHIS2_API_TOKEN`, in `.env`
Record your DHIS2 and connect
Run one of the two use lines, then connect. If your DHIS2 does not use
port 8080, change 8080 to its port.
# DHIS2 runs as a process on this machine (a dev server, Jetty, Tomcat):
chap-devops dhis2 use http://localhost:8080 --chap-url http://localhost:8700
# DHIS2 runs in a container of its own compose project:
chap-devops dhis2 use http://localhost:8080 --chap-url http://host.docker.internal:8700
chap-devops dhis2 connect
The first URL is where chap-devops reaches DHIS2; --chap-url is where DHIS2 reaches
chap-core, which is what the chap route points at:
-
A DHIS2 process on this machine reaches chap-core at
localhostand the API port.dhis2 usegives a warning aboutlocalhost. Here DHIS2 runs directly on this machine, so the address is correct:warning: http://localhost:8700 is right for a DHIS2 running directly on this machine; one running in Docker reaches chap-core at `http://host.docker.internal:8700` instead -
A DHIS2 container has its own
localhost;host.docker.internalis this machine. On Linux, give that containerextra_hosts: ["host.docker.internal:host-gateway"]in its own compose file.
For a DHIS2 2.42 in a container, with the chap route of the demo database and
no apps, connect printed this:
external DHIS2 2.42.6 at http://localhost:8080, as `admin`
repointed the `chap` route at http://host.docker.internal:8700/**; chap-core answered through it
skipped: installing apps on a DHIS2 chap-devops does not run (the Modeling App is not installed, the Climate App is not installed); `chap-devops dhis2 apps` installs them if its admin agrees
skipped: generating analytics tables on a DHIS2 chap-devops does not run; `chap-devops dhis2 analytics` starts a run if its admin agrees
the Modeling App can reach Chap once it is installed; open DHIS2 with `chap-devops open dhis2`
On a DHIS2 with no chap route, the second line starts with created.
The allowlist in dhis.conf
DHIS2 2.42 and later only proxy to origins that its dhis.conf allows. The
DHIS2 default is https://*, which refuses both http:// addresses above. Add
the --chap-url origin, with no path, to route.remote_servers_allowed:
route.remote_servers_allowed = https://*,http://host.docker.internal:8700
Then restart DHIS2. If DHIS2 refuses the route, chap-devops dhis2 connect names
the setting. For the process address, it printed:
error: DHIS2 refused the route: version 42 and later only allow the origins `route.remote_servers_allowed` lists, and http://localhost:8700 has to be one of them; add it, with no path (DHIS2 will not start with one), to that line in the `dhis.conf` of the DHIS2 server and restart DHIS2 there
DHIS2 checks the allowlist when chap-devops writes the route. If the route already points at this chap-core, chap-devops writes nothing. Then a refusal shows only as a warning, and chap-core can be up:
warning: the `chap` route is in place but nothing answered through it: HTTP 503 Service Unavailable; run `chap-devops status` to see whether chap-core is up
If chap-devops status says chap-core is up, check the allowlist line.
The apps and the analytics tables
connect sets only the route on a DHIS2 chap-devops did not start. On your own dev
DHIS2, install the Modeling and Climate apps, and generate the analytics
tables:
chap-devops dhis2 apps
chap-devops dhis2 analytics
chap-devops dhis2 connect
apps needs the network: DHIS2 downloads the apps from the App Hub. On the
demo database, analytics took 30 seconds; a larger database can take hours.
The second connect records the connect in .chap-devops/components.yaml. Until
then, chap-devops up ends with this line:
chap-devops has not connected this DHIS2 to Chap; run `chap-devops dhis2 connect` once DHIS2 answers
It worked when chap-devops dhis2 connect says chap-core answered through it, and
chap-devops dhis2 show lists the route as (healthy) and both apps:
external DHIS2 2.42.6 at http://localhost:8080, as `admin`
route http://host.docker.internal:8700/** (healthy)
target http://host.docker.internal:8700/**
analytics 2026-10-08 01:14 (a run finished on this deployment)
apps Modeling App 7.3.0, DHIS2 Climate App 1.16.2
the Modeling App can reach Chap; open DHIS2 with `chap-devops open dhis2`
chap-devops dhis2 use --clear forgets your DHIS2 again. The chap route stays in
DHIS2.
To try this with no DHIS2 of your own, start one as a second chap-devops deployment: see Try it on one machine.
More: A DHIS2 that runs elsewhere.
All shapes: Use cases.
A DHIS2 from chap-devops, with a chap-core elsewhere
For chap-core development with the Modeling App in front of it: chap-devops runs a DHIS2 (and any models), and chap-core runs somewhere else, usually from its own checkout on this machine.
-
Start chap-core and make sure that it answers at
http://localhost:8000.- From its checkout: make it listen on
0.0.0.0:8000, not only on the loopback. - From a second chap-devops deployment: run the commands below.
chap-devops init core --only chap-core --models none --api-port 8000 cd core chap-devops up chap-devops status # wait for the chap-core line to say up cd .. - From its checkout: make it listen on
-
Make the DHIS2 deployment and connect it to that chap-core:
chap-devops init lab --chap-core-url http://localhost:8000 --with dhis2 cd lab chap-devops up chap-devops status # wait for the dhis2 line to say up chap-devops dhis2 connect chap-devops open dhis2
If chap-core is a container (the second deployment above, or make restart in
the chap-core checkout), init gives this warning:
warning: chap-core at http://localhost:8000 is the container `core-f380cb-chap-1`, so it calls the models back at host.docker.internal; if it is a process on this machine instead, run `chap-devops components enable chap-core --url http://localhost:8000 --models-host localhost`
The warning is about models only. chap-devops found the container, so the models register with the correct address. A deployment with no models can ignore it.
chap-devops up returns after some seconds, before DHIS2 answers. The first chap-devops up pulls the DHIS2 images, about 2 GB, and downloads the seed dump. Then
DHIS2 needs some seconds to some minutes before it answers. Until then,
chap-devops status shows dhis2 starting.
With --chap-core-url, init enables no model unless you name one. Add
--models ID,ID to start models that register with that chap-core. If a model
cannot register with a chap-core elsewhere, init gives a warning that names
it.
--chap-core-url records the chap-core elsewhere (see
chap-core from its checkout, with the models), and
for the DHIS2 beside it:
- The route goes to that chap-core as the DHIS2 container reaches it. A
localhostURL becomeshost.docker.internal, and the DHIS2 service maps that name to the host gateway. dhis2/dhis.confallows it. A scaffolded file allows every http and https target inroute.remote_servers_allowed, so the chap-core elsewhere is covered wherever it runs.- chap-core has to listen on more than the loopback (
0.0.0.0), since the route arrives from a container. A chap-core in a container publishes its port on all addresses already.
On the seed database, chap-devops dhis2 connect printed this:
DHIS2 2.42.6 at http://localhost:8780, as `admin`
repointed the `chap` route at http://host.docker.internal:8000/**; chap-core answered through it
installed Modeling App 7.3.0
installed DHIS2 Climate App 1.16.2
analytics finished in 20 seconds (job AyL1NbEW3ot)
the Modeling App can reach Chap; open DHIS2 with `chap-devops open dhis2`
The seed database has a chap route already, so the second line says
repointed. On a DHIS2 with no chap route, it says created.
If chap-core does not answer, connect still sets the route, but it gives a
warning and ends with another line:
warning: the `chap` route is in place but nothing answered through it: HTTP 500 Internal Server Error: finishConnect(..) failed with error(-111): Connection refused: host.docker.internal/192.168.65.254:8000; run `chap-devops status` to see whether chap-core is up
...
run `chap-devops dhis2 show` to see what is still missing
If you see this warning, start chap-core and run chap-devops dhis2 connect again.
See The route is there but nothing answers through it.
chap-devops open dhis2 opens DHIS2 in the browser; with --no-browser, it prints
the address. Log in as admin with the password district. The Modeling App
is Modeling in the apps menu.
It worked when chap-devops dhis2 connect says chap-core answered through it and
ends with the Modeling App can reach Chap. In the Modeling App, the Settings
page then shows the chap-core version and Connected.
More: DHIS2, A chap-core elsewhere.
All shapes: Use cases.
A model image you built yourself
For testing a model the way it will really run: build its image on this machine and run it in a chap-devops deployment, registered with chap-core, without publishing it anywhere first.
For a new model, chap-devops models new creates a project that builds in this
way, and its README.md has these commands with the names filled in. See
A new model project.
In the model’s checkout:
docker build --platform linux/amd64 --build-arg GIT_REVISION=$(git rev-parse HEAD) -t my-model:dev .
The GIT_REVISION build argument puts the commit in the image. The model
reports it when it registers, and chap-core 2.4 refuses to store the model
template of a service that reports no revision. Without it, the model stays
registered, not configured and nothing can run it.
The first build pulls the model’s base image, such as
ghcr.io/dhis2-chap/chapkit-py or ghcr.io/dhis2-chap/chapkit-r-inla. The
R-INLA base is about 1.4 GB, and its pull can take ten minutes or more. A
rebuild uses the cached base and takes some seconds.
The model registers with chap-core under the id in its MLServiceInfo
(in main.py for a chapkit model). Find that id before you add the image:
grep -A1 'MLServiceInfo(' main.py
Then make a deployment with chap-core, or use one that you have:
chap-devops init lab --models none
cd lab
Add the image with that id as --service-id, and start it:
chap-devops models add my-model:dev --service-id chapkit-minimalist-example-py
chap-devops up --wait
chap-devops status
chap-devops models test my_model
Replace chapkit-minimalist-example-py with the id of your model. The model
id, my_model, comes from the image name. chap-devops models add prints:
added my_model (chapkit-minimalist-example-py)
enabled my_model dev at http://localhost:8700/v2/services/chapkit-minimalist-example-py/run/
the service must register with chap-core as `chapkit-minimalist-example-py`; if its own MLServiceInfo.id differs, `chap-devops status` shows it as unmanaged - run `chap-devops models remove my_model`, then add it again with `--service-id <that id>`
run `chap-devops up` to apply
chap-devops up --wait returns when the model has registered, and it then
creates the configured model that chap-core needs to run it. A plain
chap-devops up returns when the containers start; chap-devops status then says
running, not registered for some seconds, and registered, not configured until chap-devops models configs sync runs. After chap-devops up --wait,
chap-devops status shows:
chap-core up http://localhost:8700 2.4.0 auth: off
MODEL STATE REACH LAST PING
chapkit-minimalist-example-py registered via chap-core 3s ago
1 model registered
chap-devops models test my_model runs chapkit test in the model’s container:
testing 1 model (model level; add --backtest to go through chap-core)
chapkit-minimalist-example-py pass 9s 1 training, 1 prediction
1 of 1 model passes
Without --service-id, the service id is the image name, here my-model.
If the model registers under a different id, chap-devops status shows its row as
unmanaged and names the commands that correct it:
MODEL STATE REACH LAST PING
my-model running, not registered via chap-core -
chapkit-minimalist-example-py unmanaged http://a75691bfa55d:8000 10s ago
1 of 1 model is not registered.
my-model: its container registered as `chapkit-minimalist-example-py`, the unmanaged row above; run `chap-devops models remove my_model`, then `chap-devops models add my-model:dev --service-id chapkit-minimalist-example-py`
A reference with no registry host (my-model:dev, me/my-model:dev) is read
as an image in the local image store:
- It has to be there already.
models addreads the user and data directory from the local image. If the image is missing, it refuses and gives the build command:error: nope-model:dev is not in the local image store for linux/amd64; build it with .... - It is never pulled. The overlay says
pull_policy: never, sochap-devops upstarts what is in the store, andchap-devops updateshows it aspinned, skipped. --platform linux/amd64matches the platform every model overlay pins. On an Apple Silicon Mac the build runs under emulation and takes longer.- Rebuilding with the same tag and running
chap-devops restartstarts the new build.chap-devops restartprintsrecreated chapkit-minimalist-example-py.
A local build of a marketplace model
If the model is one the marketplace already lists (you are working on it),
give the local build the marketplace model’s service id, so it registers
under the name chap-core expects. Build the image in the model’s checkout,
for example chapkit_simple_multistep_model:
docker build --platform linux/amd64 --build-arg GIT_REVISION=$(git rev-parse HEAD) -t multistep:dev .
Then, in the deployment:
chap-devops models disable chapkit_simple_multistep_model # if it is enabled
chap-devops models add multistep:dev --service-id chapkit-simple-multistep-model
chap-devops up
The two cannot run at the same time. If the marketplace model is enabled,
chap-devops models add refuses:
error: the marketplace model chapkit_simple_multistep_model is enabled as the compose service `chapkit-simple-multistep-model`; disable it with `chap-devops models disable chapkit_simple_multistep_model` to run this local build in its place
In the same way, chap-devops models enable chapkit_simple_multistep_model refuses while the
local build holds the service, and names chap-devops models disable multistep. To go
back to the published image:
chap-devops models remove multistep
chap-devops models enable chapkit_simple_multistep_model
chap-devops up
chap-devops models disable and chap-devops models remove keep the model’s data volume
and print the docker volume rm command that removes it. chap-devops down --volumes
also removes that volume, because it has the compose project label of the
deployment. It warns about each volume with the deployment prefix that it did
not remove.
More: Models outside the marketplace.
All shapes: Use cases.
Combinations
Components and models combine freely: --only names exactly the components
you want (and chap-core only if you list it), --with adds components to
chap-core, and --models names the model services either way.
Each example below makes a deployment with its own name. Run each chap-devops init
from the directory where you keep your deployments, not from inside another
deployment. All three use the default host ports, so only one of them can be
up at a time. chap-devops init warns about this, and chap-devops up offers to stop the
other deployment. To run them side by side, see
Several deployments on one machine.
Climate data and a DHIS2, no Chap
OCS with its object store beside a DHIS2, for work on climate data in DHIS2 without Chap in the picture:
chap-devops init lab --only ocs,s3,dhis2
cd lab
chap-devops up # DHIS2's first start takes several minutes
chap-devops status # run it again until every line says up
chap-devops open ocs
chap-devops open dhis2
The first chap-devops up pulls the images that are not on the machine yet: OCS
(about 3 GB on disk), DHIS2 (about 1.2 GB) and PostGIS (about 0.8 GB). With the
images already present, chap-devops up took 14 seconds and DHIS2 answered about 25
seconds later. While DHIS2 starts, chap-devops status ends with
1 of 3 components is still starting; run `chap-devops status` again in a moment.
Then it shows:
ocs up http://localhost:8790 0 datasets 32.0 KB data
s3 up internal
dhis2 up http://localhost:8780
all 3 components are up
To wait in one command instead, run chap-devops up --wait --timeout 1200. It first
says waiting up to 1200s for ocs and dhis2 to answer. It returns only when
OCS answers its health check and DHIS2 answers /api/ping, and ends with:
started dhis2, dhis2-db, ocs, s3
ready in 17s
ocs up http://localhost:8790
dhis2 up http://localhost:8780
The 17 seconds count from the end of the compose step. A first start of DHIS2
can take longer than the default 300 seconds, so keep --timeout 1200.
It worked when chap-devops status shows ocs, s3 and dhis2 as up. OCS is on
http://localhost:8790 and DHIS2 on http://localhost:8780 (login admin /
district). Inside the deployment, DHIS2 reaches OCS at http://ocs:9000.
To put climate data into OCS and get it out per DHIS2 org unit, see
Getting it out.
chap-devops dhis2 connect refuses, because this deployment has no chap-core:
error: chap-core is not a component of this deployment, so the route would point at a service that is not there; run `chap-devops components enable chap-core` first
To add Chap later, do these steps in the deployment directory:
chap-devops components enable chap-core
chap-devops up
chap-devops status # run it again until the chap-core line says up
chap-devops dhis2 connect
For about 10 seconds after chap-devops up, the chap-core line says starting.
chap-devops dhis2 connect took about 30 seconds, most of it for analytics.
chap-core comes without models: chap-devops status says
no models enabled; run `chap-devops models enable ID` to add one. Its image tag
is latest, not a release. If the chap-core images are not on the machine, the
first chap-devops up pulls about 15 GB. See
Growing a deployment for the tag and how to pin a release.
A model beside OCS, no Chap
chap-devops init ocs-model --only ocs --models chapkit_simple_multistep_model
cd ocs-model
chap-devops up --wait
chap-devops status
The first chap-devops up pulls the model image (about 1 GB on disk) and the OCS
image (about 3 GB) if they are not on the machine. chap-devops up --wait returns
once the model answers on its own host port and OCS answers its health check.
Then
chap-devops status shows:
ocs up http://localhost:8790 0 datasets 32.0 KB data
MODEL STATE REACH LAST PING
chapkit-simple-multistep-model up port 5001 -
1 model up, answering on its own host port
ocs is up
If the ocs line says starting, run chap-devops status again in a moment. The
model answers on http://localhost:5001, as in
A chapkit model service on its own, and OCS on
http://localhost:8790.
Everything
chap-devops init full --models chapkit_simple_multistep_model --with ocs,s3,dhis2
cd full
chap-devops up --wait --timeout 1200
chap-devops status
chap-devops dhis2 connect # the route to chap-core, the apps, analytics
chap-core, its models, OCS with the object store and a DHIS2, all in one
deployment. The images take about 28 GB on disk, so a first chap-devops up that
pulls them all can take a long time. The running deployment used about 5 GB of
memory; give Docker 8 GB or more.
chap-devops up --wait returns only when chap-core, OCS, DHIS2 and the model answer.
DHIS2 answers when /api/ping returns 200. A first start of DHIS2 can take
longer than the default 300 seconds, so the command sets --timeout 1200.
With the images already present, the command took 31 seconds and ended with:
ready in 13s
chap-core up http://localhost:8700
chapkit-simple-multistep-model registered http://localhost:8700/v2/services/chapkit-simple-multistep-model/run/
ocs up http://localhost:8790
dhis2 up http://localhost:8780
created configured models in chap-core for chapkit_simple_multistep_model (monthly_climate, monthly_selfhistory)
The 13 seconds count from the end of the compose step. Then chap-devops status
shows:
chap-core up http://localhost:8700 2.4.0 auth: off
ocs up http://localhost:8790 0 datasets 32.0 KB data
s3 up internal
dhis2 up http://localhost:8780
MODEL STATE REACH LAST PING
chapkit-simple-multistep-model registered via chap-core 10s ago
1 model registered
chap-devops has not connected this DHIS2 to Chap; run `chap-devops dhis2 connect`
If you use chap-devops up without --wait, run chap-devops status again until the
component lines say up. The model line then says registered, not configured, because chap-devops up without --wait makes no configured models.
chap-devops dhis2 connect makes them, and so does chap-devops models configs sync.
chap-devops models configs lists them.
chap-devops dhis2 connect took about 35 seconds, most of it for analytics. It
worked when chap-devops dhis2 connect ends with
the Modeling App can reach Chap; open DHIS2 with `chap-devops open dhis2`.
Follow Chap with a local DHIS2 for the DHIS2
side, and Chap with climate data from OCS for the OCS
side.
Settings any of these take
--dhis2-tag (see Picking the version)
and --dhis2-seed (see Picking the data)
for DHIS2, --ocs-read-only
and --ocs-base-url for OCS, and --api-port, --ocs-port, --dhis2-port
for ports other than the defaults (see
Several deployments on one machine).
All shapes: Use cases.
Several deployments on one machine
Each chap-devops init directory is a deployment of its own, with its own
containers, data and ports. Any number of them can live on one machine. They
share only the host ports, and by default every deployment asks for the same
ones: chap-core on 8700, DHIS2 on 8780 and OCS on 8790 (models from 5001 up).
There are two ways to work with that.
One deployment at a time
Keep the default ports and run one deployment at a time. For example, compare a DHIS2 2.42 with a 2.43:
chap-devops init d242 --only dhis2
chap-devops init d243 --only dhis2 --dhis2-tag 2.43 # warns: 8780 is also used by d242
cd d242 && chap-devops up
cd ../d243 && chap-devops up
The second init gives two warnings:
warning: port 8780 is also used by d242 (/home/me/d242), which is not running; run only one of them at a time, or run `chap-devops components enable dhis2 --port 8781`
warning: chap-devops knows no DHIS2 demo dump for 2.43, so a new `dhis2_db` starts empty; name one with `chap-devops components enable dhis2 --seed URL` (a URL or a path)
init warns when another deployment that it can see uses the same port. It
sees a deployment in a directory beside the new one, and a deployment that
Docker started before. If the port is in use now, the warning is the plain
port 8780 is already in use on this machine (needed by dhis2) line and
names no deployment. The warning names a free port. init refuses nothing:
two deployments on one port can take turns. The second warning is about the
data: chap-devops has a demo database for 2.42 only, so d243 starts with an empty
DHIS2.
chap-devops up returns when the containers are started. DHIS2 needs more time to
answer: chap-devops status shows starting until it does, then dhis2 is up.
chap-devops up checks every port before it starts anything. When the port belongs
to another chap-devops deployment that is running, chap-devops up names it. At a
terminal, it prints the lines of the refusal below, then asks:
d242 is using these ports. Stop it and start this deployment instead? Its data is kept. [y/N]
If you answer y, chap-devops runs chap-devops down on the other deployment, then starts
this one:
stopping d242 (/home/me/d242) to free its ports; `chap-devops -C /home/me/d242 up` starts it again
...
started dhis2, dhis2-db
chap-devops deletes nothing. chap-devops up in the other directory starts it again, with
its database. In a script, there is no one to answer. chap-devops up then refuses
and names each way out:
error: 1 host port this deployment needs is already in use; nothing was started
port 8780 (needed by dhis2) is in use, and d242 (/home/me/d242) publishes it too; stop it with `chap-devops -C /home/me/d242 down`, or move this one: run `chap-devops components enable dhis2 --port 8781`
or run `chap-devops up --no-preflight` to hand the conflict to Docker
or run `chap-devops up --replace` to stop d242 first
chap-devops up --replace gives the answer y before the question:
chap-devops up --replace
Only a running deployment is named, and only a running one is stopped by
--replace. A stopped deployment that publishes the same port holds nothing,
so chap-devops up starts this one. If the port is in use for another reason, the
line is the plain port 8780 is already in use on this machine (needed by dhis2) line, and there is no --replace line.
When a program that is not a chap-devops deployment holds the port, chap-devops only
reports it and does not stop that program. chap-devops up names the port and the
setting that moves this deployment to a different port.
All deployments at the same time
Give each deployment its own ports. At init:
chap-devops init a --with ocs,dhis2
chap-devops init b --with ocs,dhis2 --api-port 8701 --ocs-port 8791 --dhis2-port 8781 --port-base 5101
Both deployments also get chap-core, with no model. If d242 and d243 from the section above are in the
same directory, init a also warns about port 8780. Stop them before you
start a. init b prints the ports it uses:
chap-core: v2.4.0, API on http://localhost:8701
OCS: http://localhost:8791
DHIS2: http://localhost:8781
--port-base moves the range that model services get host ports from. It has
an effect only on models with host ports: models in a deployment without
chap-core, and models that chap-devops models expose publishes. In b above, the
model is reached through chap-core, so it has no host port.
On a deployment that already exists:
| What | How |
|---|---|
| chap-core’s API | CHAP_API_PORT=8701 in .env, then chap-devops up |
| OCS | chap-devops components enable ocs --port 8791, then chap-devops up |
| DHIS2 | chap-devops components enable dhis2 --port 8781, then chap-devops up |
| a model | chap-devops models expose ID --port 5101, or chap-devops models unexpose ID |
If the deployment has the port already, the command changes nothing, says so,
and gives no chap-devops up line:
ocs was already enabled on http://localhost:8791; nothing changed
chapkit-simple-multistep-model is already exposed on http://localhost:5101; nothing changed
The two lines come from two commands. With -v, the ocs line has the hint
if it is not running, `chap-devops up` starts it. A model keeps its own port: the
port that its running container publishes does not count as a port in use.
You do not have to find a free port. When a port is in use, the warnings of
chap-devops init and the refusal of chap-devops up name the next free port in the line
to copy.
Inside each deployment, the services keep their container ports
(http://chap:8000, http://ocs:9000). Thus a change to a host port does not
change how the services reach each other. Memory is the other limit: each
DHIS2 can use 4 to 5 GB.
Which deployment a command uses
Each command works on the deployment in the current directory, or on the one
that -C DIR names:
chap-devops -C d242 status
chap-devops -C d243 down
chap-devops status in each directory shows what that deployment runs and where.
More: Ports, The preflight, A host port is already in use.
All shapes: Use cases.
Growing a deployment
Nothing about the first init is final. Components go on and off with
chap-devops components enable NAME / disable NAME, models with chap-devops models enable ID / disable ID, and chap-devops up applies the change.
The move people make most is from a standalone piece to Chap: a model service or an OCS that was running alone gets chap-core added beside it. The examples here start from a model on its own (see A chapkit model service on its own):
chap-devops init multistep --only none --models chapkit_simple_multistep_model
cd multistep
chap-devops up
Add chap-core:
chap-devops components enable chap-core
chap-devops up
chap-devops status # the models now register with chap-core
chap-devops components enable chap-core prints enabled chap-core on http://localhost:8700, a line that says the models keep their host ports, and
run `chap-devops up` to apply. Model services read the
orchestrator URL on that chap-devops up and register.
Registration takes up to a minute after the start, so the first chap-devops status
can show running, not registered and the line
started under two minutes ago and registers once it is ready.
If it does, run chap-devops status again in a minute:
chap-core up http://localhost:8700 2.4.0 latest: running f80b66f755ef, revision 7c95b2a7df62 auth: off
MODEL STATE REACH LAST PING
chapkit-simple-multistep-model registered, not configured port 5001 11s ago
1 model registered
chapkit-simple-multistep-model: chap-core has no configured model for it, so nothing can run it; run `chap-devops models configs sync`
registered, not configured means chap-core has the model but no configured
model of it yet. chap-devops models configs sync, or chap-devops up --wait, creates
them. See Configured models.
The host ports they were given while standalone stay published until you
chap-devops models unexpose ID. The chap-core compose file comes from the copy built
into chap-devops, and the image tag is latest, a moving tag: the latest: running ... cell in chap-devops status names the build that runs. chap-devops update --pin-chap-core moves it to the newest release, which is what chap-devops init
with chap-core records.
The other direction works too: chap-devops components disable chap-core keeps the
models, publishes a host port for each one that had none, and says where each
one now answers:
$ chap-devops components disable chap-core
disabled chap-core
stopped and removed 6 containers (chap, chapkit-simple-multistep-model, chapkit-simple-multistep-model-init, postgres, redis, worker); host ports 5001, 8700 are free again
run `chap-devops up` to apply
A model that had no host port (after chap-devops models unexpose) gets one more
line, for example
chapkit_simple_multistep_model now registers nowhere and is published on http://localhost:5001.
The disable stops the model containers too, so run chap-devops up to start the
models again without chap-core.
chap-core’s own volumes are kept either way. While chap-core is enabled,
chap-devops down --volumes removes them. After the disable it does not, because
the deployment no longer has the compose file that names them. To remove them,
run chap-devops components enable chap-core first, then chap-devops down --volumes.
It worked when chap-devops status shows the new pieces: chap-core up and the
models registered after adding chap-core, or each model up on its own port
after taking it away.
The same goes for any other piece: chap-devops components enable ocs,
chap-devops components enable dhis2 or chap-devops models enable ID, then chap-devops up. A
DHIS2 added this way runs the default version unless you name one:
chap-devops components enable dhis2 --tag 2.43 (or --image dhis2/core-dev --tag master for the unreleased one), or v on its row in chap-devops ui (see Changing the DHIS2 version).
Only the default version has a demo database. Another version starts empty, and
the enable says so in a warning: line. If the deployment has chap-core, run
chap-devops dhis2 connect once DHIS2 answers (see
Connecting the Modeling App to Chap).
All shapes: Use cases.
Taking over a chap-admin deployment
A Chap server that runs chap-core’s own compose files, with models installed
by chap-admin, can move to chap-devops. chap-devops init --adopt writes a
new deployment directory that uses the same database. The old directory is
read and never written, so you can go back to it.
What comes over
| From the old directory | In the new deployment |
|---|---|
The compose project name (the directory name, or COMPOSE_PROJECT_NAME in .env) | The same name, so chap-core uses the same volumes: the database in <project>_chap-db, and the logs and runs. |
POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB in .env | The same values in the new .env, so the password matches the database. |
CHAP_API_TOKEN and SERVICEKIT_REGISTRATION_KEY | The same token and key. A token without a key gets a new key. |
CHAP_IMAGE_TAG | The chap-core tag, unless --chap-tag names another. |
| Port 8000, where chap-core’s compose files publish the API | Port 8000, unless --api-port names another. |
Each model in compose.marketplace.yml | A model of the new deployment, with its data copied (see below). |
Each model comes over the way it ran:
- A marketplace model at the stable version follows the stable channel, as
chap-admin update-allmoved it. - A marketplace model at an older version stays at that version. Run
chap-devops models enable ID --channel stableto follow the marketplace. - A model the marketplace no longer lists, such as CHAP-EWARS, is added with
chap-devops models addat the image it ran. - A custom image (
chap-admin install --image) is added withchap-devops models add, under the template name it registered with.
The data volume of each model is copied to the name chap-devops uses,
<project>_ck_<id>_data. The old volume stays. Chap itself keeps its model
templates and configured models in the database, so the models are configured
as they were.
Other settings of the old .env do not come over. The command names each one,
so that you can copy the line into the new .env.
Steps
-
Stop the old deployment.
downremoves the containers and keeps the volumes:cd /srv/chap docker compose -f compose.yml -f compose.marketplace.yml down -
Write the new deployment:
chap-devops init /srv/chap-devops --adopt /srv/chaptook over the deployment in /srv/chap (compose project chap) copied the data of chapkit_simple_multistep_model from chap_marketplace-chapkit-simple-multistep-model-data to chap_ck_chapkit_simple_multistep_model_data -
Start it, and check it:
cd /srv/chap-devops chap-devops up chap-devops status
It worked when chap-devops status shows chap-core up and each model
registered. The Modeling App shows the same models and the same backtests and
predictions as before.
To go back, run chap-devops down in the new directory, then start the old
deployment again with its own compose command. Both use the same database.
What it refuses
- A deployment that still has containers: the two would use one database.
Run the
docker compose -p <project> downcommand in the message. - A project name with no
<project>_chap-dbvolume: chap-core would start with an empty database. If the deployment ran under another name, setCOMPOSE_PROJECT_NAMEin the old.envto that name. --models,--interactive,--source,--chap-core-url,--api-token,--fresh-envand--no-env: the takeover decides those itself.- A target directory that is a deployment already.
If docker cannot be asked, init --adopt writes the deployment, copies no
model data, and says so. Stop the old deployment before chap-devops up.
Evaluating a model on your own data
This guide goes from an empty machine to a comparison of two forecasting
models on one dataset. It uses chap-core’s own command line, chap, through
chap-devops chap. You do not install Python, uv or R: chap-devops chap runs chap in
a container.
At the end you have:
- two evaluations (
.ncfiles), one for each model; - a plot of an evaluation (
.html) that you open in a browser; - a table that compares the two models (
.csv).
All of these files are in your working directory, and they belong to your user.
What you need
-
Linux or macOS. On Windows, use WSL with the Linux binary.
-
Docker, and it must run. On macOS, start Docker Desktop.
-
About 20 GB of free disk space, for the images that the steps download:
Image Size on disk Step ghcr.io/dhis2-chap/chap-workerabout 12 GB 4, the model from GitHub ghcr.io/dhis2-chap/chap-coreabout 2.4 GB 5 to 7, only if you have no chap-workerghcr.io/chap-models/auto_arima_chapkitabout 6.2 GB 6, the marketplace model chap-devops says before each run when it must download an image first. chap-devops uses the tag of the newest chap-core release,
v2.5.0when this guide was tested. On a slow network, download the images in advance:docker pull --platform linux/amd64 ghcr.io/dhis2-chap/chap-worker:v2.5.0 -
A connection to the internet for the first run.
The first run of each step takes some minutes, because Docker downloads the images. On an Apple silicon Mac, Docker runs the images under emulation, so each step is slower than on Linux. After the first run, an evaluation of the example data takes less than a minute.
Step 1: Install chap-devops
curl -fsSL https://raw.githubusercontent.com/winterop-com/chap-devops/main/install.sh | sh
chap-devops --version
It worked when chap-devops --version prints a version, for example chap-devops 0.100.1.
See Install for other ways to install.
Step 2: Check the machine
chap-devops doctor
It worked when the docker cli, docker daemon and docker compose lines
start with ok. A warn on the os and arch line of an Apple silicon Mac is
correct: it only tells you that the images run under emulation. If a line
starts with fail, do the fix that it names, then run chap-devops doctor again.
Step 3: Make a working directory and get data
Make a directory for this work, and go into it:
mkdir -p ~/chap-eval
cd ~/chap-eval
Download chap-core’s example data. It is monthly dengue cases, with rainfall, temperature and population, for districts of Laos:
curl -fsSO https://raw.githubusercontent.com/dhis2-chap/chap-core/master/example_data/laos_subset.csv
curl -fsSO https://raw.githubusercontent.com/dhis2-chap/chap-core/master/example_data/laos_subset.geojson
It worked when ls shows laos_subset.csv and laos_subset.geojson.
To use your own data instead, put your CSV and its GeoJSON in this
directory. They must have the same name, for example mydata.csv and
mydata.geojson. Use the same columns as the example file:
time_period,rainfall,mean_temperature,disease_cases,population,location
2010-01,37.965,20.04,1.0,75049.56,Bokeo
Each location must be a feature in the GeoJSON. In the steps below, change
laos_subset.csv to the name of your CSV.
Step 4: Evaluate a model from GitHub
This model is a small R model from chap-core’s examples:
chap-devops chap eval \
--model-name https://github.com/dhis2-chap/minimalist_example_r \
--dataset-csv laos_subset.csv \
--output-file minimalist_r.nc \
--backtest-params.n-splits 3 \
--backtest-params.n-periods 3
What the options do:
| Option | What it does |
|---|---|
--model-name | The model: a GitHub repository, a model directory, or the URL of a model service. |
--dataset-csv | Your data. The GeoJSON with the same name is found automatically. |
--output-file | The evaluation that chap writes, a NetCDF (.nc) file. |
--backtest-params.n-splits | How many times the model trains and forecasts, each time from a later point in the data. |
--backtest-params.n-periods | How many periods (here months) each forecast goes ahead. |
chap-devops writes some lines of its own before chap starts:
running `chap eval` in ghcr.io/dhis2-chap/chap-worker:v2.5.0; the first run pulls it, about 12 GB
files: chap reads and writes in /home/me/chap-eval
Then chap writes its own log lines. At the end, chap-devops writes:
chap finished; it wrote minimalist_r.nc
`chap-devops chap plot-backtest minimalist_r.nc --output-file minimalist_r.html` plots it
It worked when you see chap finished; it wrote minimalist_r.nc.
chap-devops selected the large chap-worker image because a model from GitHub can
need R. chap clones the model again for each run. chap-devops keeps the R packages
of a model in its cache, so the second run does not install them again. This
small model has no packages to install, so each run takes about the same
time: about 40 seconds on an Apple silicon Mac, when the image is there.
Step 5: Plot the evaluation
Run the command that the last line of step 4 gave you:
chap-devops chap plot-backtest minimalist_r.nc --output-file minimalist_r.html
It worked when chap-devops writes chap finished; it wrote minimalist_r.html. Open
the file in a browser:
open minimalist_r.html # macOS
xdg-open minimalist_r.html # Linux
The plot shows the real cases and the forecasts of each split, one panel for each location.
Step 6: Evaluate a second model, from the marketplace
A model from the marketplace is a model service: a container
with an HTTP API. Give its id to --model-name, and chap-devops starts it for you:
chap-devops chap eval \
--model-name auto_arima_chapkit \
--dataset-csv laos_subset.csv \
--output-file auto_arima.nc \
--backtest-params.n-splits 3 \
--backtest-params.n-periods 3
chap-devops starts the model, waits until it answers, and then runs chap. After the first line, Docker Compose writes the lines of the network, the volume and the containers that it makes:
starting auto-arima-chapkit (auto_arima_chapkit in /home/me/.local/share/chap-devops/run/default)
...
running `chap eval` in ghcr.io/dhis2-chap/chap-worker:v2.5.0
files: chap reads and writes in /home/me/chap-eval
model: auto_arima_chapkit at http://auto-arima-chapkit:8000 answers
...
chap finished; it wrote auto_arima.nc
`chap-devops chap plot-backtest auto_arima.nc --output-file auto_arima.html` plots it
auto_arima_chapkit keeps running for the next run; `chap-devops stop auto_arima_chapkit` stops it, and `--stop` stops it after a run
It worked when you see chap finished; it wrote auto_arima.nc. The first
time, Docker downloads the image of the model, which takes some minutes.
chap only talks HTTP to a model service, so the smaller chap-core image is
enough. chap-devops uses an image that is already on this machine, so the run
downloads no chap image. After step 4, that is chap-worker. If the
chap-core image at the same tag is also on this machine, chap-devops uses
chap-core, and the first line names it.
The model keeps running, so a second evaluation of it starts at once.
chap-devops ps lists it. Find more ids with chap-devops models list.
Step 7: Compare the two models
chap-devops chap export-metrics minimalist_r.nc auto_arima.nc --output-file comparison.csv
It worked when chap-devops writes chap finished; it wrote comparison.csv. chap can
write some RuntimeWarning lines about log1p before that. They do not stop
the run.
comparison.csv has one row for each model. Open it in a spreadsheet. Some
of the columns:
| Column | What it measures | Better when |
|---|---|---|
crps | How far the forecast distribution is from the real cases | lower |
mae | The mean absolute error of the forecasts | lower |
rmse | The root mean square error of the forecasts | lower |
coverage_10_90 | How often the real value is in the 10-90% forecast interval | near 0.8 |
Step 8: Clean up
Stop the model service, and remove its data:
chap-devops stop auto_arima_chapkit --purge
It worked when chap-devops writes:
stopped auto_arima_chapkit
removed volume default-662918_ck_auto_arima_chapkit_data
removed group default
The number in the volume name is different on your machine. The group
default held only this model, so chap-devops removes it too.
Your .nc, .html and .csv files stay in ~/chap-eval. To get the disk
space of the caches back, delete chap-devops’s chap directory:
rm -rf ~/.local/share/chap-devops/chap
To remove the images too, list them, then remove each one with
docker image rm:
docker image ls 'ghcr.io/dhis2-chap/*'
docker image ls 'ghcr.io/chap-models/*'
docker image rm ghcr.io/dhis2-chap/chap-worker:v2.5.0
If something goes wrong
| chap-devops writes | What to do |
|---|---|
`http://localhost:...` is this machine | Give the model id instead, as in step 6. |
the model server at ... is not running | The URL does not answer. Give a model id, and chap-devops starts the model. |
is not a model of the deployment | In a deployment, add the model first with chap-devops models enable ID. |
runs in docker (`docker_env` in its MLproject) | The model starts a container of its own. Run the same command with chap-devops chap --docker .... See Models that run in docker. |
FileNotFoundError: [Errno 2] No such file or directory | chap cannot find an input file. Make sure that the CSV and the GeoJSON with the same name are there, then run again. |
Rscript: not found | The model needs R. Add --image worker after chap. |
chap exited with status N | chap stopped with an error. The cause is in chap’s own lines above. |
docker could not start ... | Docker could not download or start the image. Run chap-devops doctor. |
More messages are in Troubleshooting.
Next
- To evaluate the models of a deployment, run
chap-devops chapin a subdirectory of it (mkdir eval && cd eval), and give the model id:chap-devops chap eval --model-name auto_arima_chapkit .... The deployment does not need to run: chap-devops starts only that model, without chap-core. - To use another chap-core version, add
--tag, for examplechap-devops chap --tag master eval .... - For chap’s own options, run
chap-devops chap eval --help. - The chap CLI tells how
chap-devops chapmounts your files, selects the image and keeps its caches.
Concepts
The project directory
chap-devops init writes a directory you own; nothing outside it is touched.
mychap/
.chap-devops/
project.yaml intent: schema version, chap-core tag and compose
source, the compose project name, registry URL,
API port, whether the two auth secrets are in
use, the -f list, the model port range, and
which root files sync wrote
models.yaml intent: the enabled models (image, pinned version,
channel, host port or none, data dir, user and
where it was read from, platform, overlay name)
models-manual.yaml intent: the models this deployment defines itself,
added by `chap-devops models add`; only present when it
has added one
components.yaml intent: which components this deployment is made of
(chap-core, ocs, s3, dhis2) and their settings
compose.chap-core.<tag>.yml
chap-core's own compose.ghcr.yml at the pinned tag,
exactly as downloaded; compose.yml is rendered from it
compose.yml artifact: the base services, from the file above
compose.chap-devops.yml artifact: chap-devops-owned overrides on top of it: the
compose project name, the API's host port and the
registration key chap-core checks
compose.ocs.yml artifact: the ocs component, when it is enabled
compose.s3.yml artifact: the s3 component, when it is enabled
compose.dhis2.yml artifact: the dhis2 component, when it is enabled
compose.marketplace.yml artifact: include: list, one line per enabled model
compose.<service_id>.yml artifact: one overlay per enabled model
ocs/climate-service.yaml the OCS instance configuration; scaffolded once by
chap-devops, which changes only two keys in it
dhis2/dhis.conf the DHIS2 instance configuration; likewise, and DHIS2
does not start without it
.env written once by init; only pins, the two auth
secrets and the component settings are ever added
| File | What it is |
|---|---|
compose.yml | The base of a Chap deployment, which is chap-core, its worker and database plus the enabled models: chap-core, worker, Valkey and PostgreSQL, with the models added on top by their overlays. chap-core’s own compose.ghcr.yml at the pinned tag, so upstream stays the source of truth. Rendered by sync from .chap-devops/compose.chap-core.<tag>.yml, or from the copy compiled into the binary when there is none. |
compose.chap-devops.yml | The chap-devops-owned settings that sit on top of the base file: the compose project name:, the API’s host port as ports: !override, SERVICEKIT_REGISTRATION_KEY plus XDG_RUNTIME_DIR in the chap service’s environment, and the chap-devops labels on every service the base file defines (Container labels). It is a separate -f entry because a file in include: cannot override a service the main file defines. Rendered from .chap-devops/project.yaml. |
.chap-devops/compose.chap-core.<tag>.yml | That upstream file as downloaded, one per tag the project has used. Deleting it does not break Chap; it only means sync can no longer re-render compose.yml. |
.env | PostgreSQL credentials (the password is 32 random hex characters generated once), the chap-core image tag, CHAP_API_PORT (an active line even at the default 8700, so chap-core’s published port is discoverable by reading the file), the two authentication secrets (CHAP_API_TOKEN and SERVICEKIT_REGISTRATION_KEY, active lines when the deployment is protected and commented placeholders when it is not), and commented placeholders for CHAP_DATABASE_URL and the per-model image pins. |
compose.ocs.yml, compose.s3.yml, compose.dhis2.yml | One per enabled component other than chap-core, rendered from .chap-devops/components.yaml. They sit in the -f list between compose.chap-devops.yml and the umbrella, and are removed again when the component is disabled. See Components. |
ocs/climate-service.yaml | The Open Climate Service instance configuration, scaffolded when the ocs component is first enabled. It is yours from that moment: chap-devops changes only its read_only and plugins_dir keys, and re-creates it only if it goes missing. |
dhis2/dhis.conf | The DHIS2 instance configuration, on exactly the same terms - scaffolded once when dhis2 is first enabled, never rewritten, re-created only if it goes missing. That last part matters more here: DHIS2 does not start without it. See DHIS2. |
compose.marketplace.yml | An umbrella file whose include: list names one overlay per enabled model. With no models enabled it holds services: {} instead of an empty include. It carries the project name: as well, because it is the one file that is always in the -f list. |
compose.<service_id>.yml | One model service, rendered from its models.yaml entry. |
.chap-devops/project.yaml, .chap-devops/models.yaml, .chap-devops/components.yaml | The intent, as above. All three open with one line saying chap-devops manages them. A deployment created before components.yaml existed reads as chap-core alone, which is what it was. |
.chap-devops/models-manual.yaml | The definitions of the models this deployment added itself, one entry per chap-devops models add: what a marketplace file would have said about each. It is intent like the rest, and it is a definition rather than an enablement - models.yaml still says which models are on. Absent in a deployment that has added none. See Models outside the marketplace. |
Intent and artifacts
.chap-devops/ is the intent. It is what models enable, models disable,
components enable, components disable, the browser and update edit, and it
is small enough to read and to diff.
The compose files at the project root are artifacts rendered from it by
chap-devops sync. They are plain Compose files with nothing chap-devops-specific in
them, so
docker compose -f compose.yml -f compose.chap-devops.yml -f compose.marketplace.yml up -d
works without chap-devops installed, which is the point of generating them.
compose.yml is an artifact too. init downloads chap-core’s own
compose.ghcr.yml at the tag it pinned, keeps the raw copy as
.chap-devops/compose.chap-core.<tag>.yml and records its URL and SHA-256 in
project.yaml; sync renders compose.yml as two header lines plus that copy,
byte for byte. Rendering from the copy rather than the network keeps sync
offline and deterministic, and it is why a hand edit of compose.yml is drift
that chap-devops sync undoes. Edit the copy in .chap-devops/ instead, and sync will
follow it, and say that the checksum no longer matches. When the copy is
missing, sync leaves compose.yml alone rather than guessing.
One change at a time
Every command that changes .chap-devops/ takes an exclusive lock on
.chap-devops/lock before it reads the state and keeps it until it has written it
back: models enable, disable, add, remove, expose, the components
commands, sync, update, restore, auth, dhis2 use, down, and up
while it renders the compose files. A second such command on the same
deployment waits for the first, and says so on stderr:
another chap-devops command is changing this deployment; waiting for it to finish
So two models enable --port auto started together, from two terminals or
from a tool that runs chap-devops for its users, end with both models enabled on
different ports. up lets go of the lock once the files are written, so a
long image pull does not hold up the next models enable. Commands that only
read - status, models list, logs - never wait. The operating system
releases the lock when the command exits, however it exits, so there is no
stale lock file to clear; .chap-devops/lock is left in place and is not part of a
backup.
chap-devops sync
chap-devops sync
in sync: 0 written, 5 unchanged, 0 removed
With -v, a hint line names each file:
$ chap-devops -v sync
hint: compose.yml is unchanged
hint: compose.chap-devops.yml is unchanged
hint: compose.auto-arima-chapkit.yml is unchanged
hint: compose.chapkit-simple-multistep-model.yml is unchanged
hint: compose.marketplace.yml is unchanged
in sync: 0 written, 5 unchanged, 0 removed
chap-devops up runs sync before docker compose up, so the intent and the
artifacts never drift in normal use. Rendering is deterministic and every file
is compared before it is written, so a second sync reports everything as
unchanged.
chap-devops sync --checkreports drift without writing, for CI or a pre-commit hook, and exits non-zero when anything would change. It names each file that would change (would write compose.yml) without-v.- A plain
chap-devops syncre-creates a deleted overlay or picks up a hand edit ofmodels.yaml. - Sync only ever removes overlays it wrote itself;
project.yamlkeeps the list. A hand-writtencompose.custom.ymlnext to them is left alone. Add it to the umbrella by hand if you want it included.
Orphans
Removing a definition does not stop a container: compose only knows about the
services the -f list names, and a container it no longer finds a definition
for is an orphan. chap-devops owns the whole -f list, so an orphan in one of
its projects is exactly that - the container of a model or a component that was
disabled, still running and still publishing its host port.
So chap-devops up and chap-devops restart both pass --remove-orphans, and models disable and components disable go further: they stop and remove the
container of the service they are taking away there and then, while compose
still has the definition to name it by, and say so in their closing line. The
port is free the moment the command returns, rather than at the next chap-devops up.
The flip side is the rule for hand-written compose files. A container is only
an orphan of the compose project it belongs to, and chap-devops renders its -f
list from .chap-devops/, so a service of your own is only at risk if you started it
under this deployment’s project name - which is what
docker compose -f compose.yml -f compose.chap-devops.yml -f compose.marketplace.yml \
-f compose.custom.yml up -d
does, because the name: comes from the chap-devops files in that list. The next
chap-devops up does not have compose.custom.yml in its own list, so it takes
those containers with it. Run a hand-written file as a compose project of its
own (its own directory, or -p) and nothing chap-devops does touches it.
The compose project name
Compose puts a project name in front of every container and every named volume
it creates, and unless a file says otherwise that name is the directory
name. Two deployments in directories both called demo therefore share
demo_chap-db, demo_logs and the rest - on different paths, and even when
one of them was deleted months ago. A fresh chap-devops init demo && chap-devops up then
starts chap-core against a PostgreSQL volume created with a password it has
never seen, the container never becomes healthy, and up ends on
dependency failed to start: container demo-chap-1 is unhealthy
So chap-devops init generates a name of its own and records it:
compose_project: demo-1ab2c3
chap-devops sync renders it as the top-level name: of the files it owns
(compose.chap-devops.yml and compose.marketplace.yml - compose takes the name:
of the last -f file that sets one, and the umbrella is the one file always in
the list). Containers become demo-1ab2c3-chap-1 and volumes
demo-1ab2c3_chap-db, which no other deployment can be holding.
The name never changes once it is written. chap-devops init --force keeps the one
it finds, because a new name would leave the running deployment’s containers
and its data behind under the old one.
chap-devops status --json, chap-devops doctor and the closing line of chap-devops down all
name the project.
COMPOSE_PROJECT_NAME in the environment still wins over the name: key, as
it does for any compose project.
Container labels
Every container chap-devops starts carries labels under com.winterop.chap-devops, so a
tool can find them all on a machine without knowing where any deployment
lives:
docker ps --filter label=com.winterop.chap-devops.role
lists every chap-devops container, of every deployment and every chap-devops run group,
and chap-devops top finds its containers the same way. The one-shot containers of
chap-devops chap and chap-devops chapkit have no role label; label=com.winterop.chap-devops.kind finds it too.
| Label | Value | On |
|---|---|---|
com.winterop.chap-devops.role | chap-core, model, ocs, s3 or dhis2 | Every container except the ones of chap-devops chap and chap-devops chapkit. chap-core covers chap, the worker, Valkey and PostgreSQL; dhis2 covers its database and one-shots; s3 its bucket one-shot. |
com.winterop.chap-devops.model | The marketplace id, such as chapkit_simple_multistep_model | A model service and its -init container. |
com.winterop.chap-devops.kind | run for a chap-devops run group, init for a deployment chap-devops init wrote, cli for the one-shot containers of chap-devops chap and chap-devops chapkit | Every container. |
com.winterop.chap-devops.group | The group name | Every container of a chap-devops run group, and nothing else. |
The model and component files carry the labels themselves. chap-core’s
services get theirs from compose.chap-devops.yml, since compose.yml is upstream’s
file byte for byte and compose merges labels across the -f files; a
chap-core built from a checkout is labelled the same way.
Nothing in a label depends on the chap-devops version or the time, because a
changed label makes compose recreate the container: an upgrade of chap-devops
renders the same labels, and chap-devops up leaves running containers alone.
Pins
A deployment is reproducible because everything it runs is pinned.
-
Models are pinned to an image tag, which for marketplace images is a
sha-<short commit>build:image: <repo>:${<ID>_IMAGE_TAG:-sha-24d58c0}. A model that follows a channel records both the channel and the version the channel resolved to; one enabled with--versionrecords the version alone and never moves. -
chap-core is pinned to a release tag.
initresolves the default--chap-tag latestto the release it points at today,v2.3.1say, and writes that into.envand.chap-devops/project.yaml, so a deployment is reproducible rather than following a tag that changes under it.--chap-tag masterordevkeeps a moving tag on purpose. -
OCS and the object store follow a moving tag, because neither publishes a release feed:
${OCS_IMAGE_TAG:-main}and${S3_IMAGE_TAG:-latest}. Set either variable in.envto pin a build of your own;chap-devops updatesays which of the two is in force. DHIS2 is pinned to a minor line instead,${DHIS2_IMAGE_TAG:-2.42}, so a patch release arrives with a pull and moving to another line is a deliberate edit - DHIS2 migrates its schema forward only. See Components and DHIS2.
Model pins only move in two ways: chap-devops models enable ID (with --channel
or --version) and chap-devops update. Nothing else, up, restart, expose and
unexpose included, changes the version a model runs. update moves the pins
and pulls; chap-devops restart applies what it fetched to the services that are
running. See Updating.
The .env contract
.env is the one file that is written once and then left alone. init writes
it when the directory has none, and never rewrites it afterwards: a second
init keeps the file it finds, --force included, sync only appends missing
pin comments and update only moves the pin comments of models it changed.
One line is the exception. When init --force keeps the file, it moves the
active CHAP_IMAGE_TAG line to the new tag if that line holds the tag that the
deployment recorded (or checkout), as chap-devops update --chap-tag does. It says
moved CHAP_IMAGE_TAG in the kept .env from checkout to v2.4.0. A line with
another value is kept, with a warning that names the line to set.
That is deliberate. The file holds the database password the PostgreSQL volume was created with, plus the API token and the registration key, and a rotated password leaves chap-core unable to authenticate against its own data.
Because of what it holds, .env is readable by its owner only (mode 600), and
every write goes to a temporary file that is renamed over it, so a crash or a
full disk never leaves half a file. If a restored or hand-made .env is
readable by everyone, the next chap-devops sync or chap-devops up closes it.
Those two secrets are the one exception to “written once”, and a narrow one.
chap-devops init --api-token fills them in as it renders the file;
chap-devops auth enable, disable and rotate rewrite exactly those two lines
afterwards, leaving every other line, every comment and every blank line byte
for byte as it found them:
CHAP_API_TOKEN=d1f0... the whole API, as `Authorization: Bearer`
SERVICEKIT_REGISTRATION_KEY=9a3c... what each model sends when it registers
Commented out, as the generated file ships them, means no authentication at all.
The values live only here: .chap-devops/project.yaml records two booleans under
auth and never a secret, so the state directory stays safe to commit and to
paste into a bug report. See Authentication.
Enabling a component appends its own section the same way sync appends a
model’s pin: a commented OCS_IMAGE_TAG, and for the object store a generated
S3_ACCESS_KEY and S3_SECRET_KEY. DHIS2 brings the longest section - a
generated DHIS2_DB_PASSWORD and DHIS2_ENCRYPTION_PASSWORD, plus commented
DHIS2_IMAGE_TAG, DHIS2_DB_DUMP_URL and DHIS2_JAVA_TOOL_OPTIONS carrying the
values the rendered compose file already defaults to. Every generated value there
is written once and never rewritten, for the same reason the database password
is: the volume was created with them.
init --fresh-env is the explicit override: it renders a new .env with a new
password, so an existing volume has to be dropped with
chap-devops down --volumes
(or the role changed with ALTER USER) before Chap will start again.
init --no-env writes no .env at all.
What .env holds
Every line is optional except the ones init writes as active assignments; a
commented placeholder means “not set”, which is the default the compose files
already carry.
| Variable | What it does |
|---|---|
POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB | The database Chap creates. The password is 32 random hex characters, generated once. It has to be URL-safe, because the connection URL is composed from it. |
CHAP_DATABASE_URL | A full percent-encoded URL that replaces the one composed from POSTGRES_*. The way to use a password that is not URL-safe. |
CHAP_IMAGE_TAG | Tag for both chap-core images: a release tag such as v2.3.1 pins one, latest is the newest release and master the branch. |
CHAP_API_PORT | Host port chap-core’s API is published on, and the only one a deployment of chap-core and models publishes. A component publishes a well-known port of its own beside it. See Ports. |
CHAP_API_TOKEN | API token. Commented out means no authentication at all. See Authentication. |
SERVICEKIT_REGISTRATION_KEY | Shared secret each model sends when it registers, and the one chap-core checks. It travels with the API token. |
CHAP_ROOT_PATH | Path prefix, for serving the API behind a reverse proxy. Not written by init; add it when you need it. |
<ID>_IMAGE_TAG | Per-model pin, one commented line per enabled model. Uncomment to run a different build of that model. |
OCS_IMAGE_TAG, S3_IMAGE_TAG, DHIS2_IMAGE_TAG | The same for the components. See Components. |
S3_ACCESS_KEY, S3_SECRET_KEY | Root credentials for the object store, generated once when s3 is enabled. The volume is created with them, so changing them later locks the store’s own data away. |
DHIS2_DB_PASSWORD, DHIS2_ENCRYPTION_PASSWORD | DHIS2’s own database password and the key it encrypts stored credentials with, generated once when dhis2 is enabled. 32 hex characters each, past the 24 DHIS2 demands of the second. Nothing to do with the Chap database: DHIS2 has its own. See DHIS2. |
DHIS2_DB_DUMP_URL, DHIS2_JAVA_TOOL_OPTIONS | The dump the DHIS2 database is seeded from and the JVM options it runs with, both commented with the value the compose file already defaults to. See DHIS2. |
Compose reads .env automatically, so ${CHAP_IMAGE_TAG:-latest} and the
per-model ${<ID>_IMAGE_TAG:-sha-xxxxxxx} pins can be overridden there without
regenerating anything. Because Compose reads .env last, a line in it wins
over what .chap-devops/ records; CHAP_API_PORT is the case that matters most, and
Ports covers it.
How chap-devops reads it
By Compose’s rules, because Compose is the only thing that acts on the file:
- the last active assignment of a variable wins, so a file carrying two
CHAP_API_TOKEN=lines is protected by the second one; export KEY=valueassigns the same thing asKEY=value;- a value wrapped in matching single or double quotes does not include them;
- a line whose first non-blank character is
#assigns nothing, and an emptyKEY=sets nothing (which is how a commented-out token andCHAP_API_TOKEN=both mean “no authentication”).
Writing follows the same rules, so the line chap-devops writes is always the line
Compose reads back: chap-devops auth enable, disable and rotate rewrite the
variable’s first active assignment and remove every later duplicate of it,
leaving exactly one active line. A file with two CHAP_API_TOKEN= lines comes
out of chap-devops auth rotate with one, holding the new token.
Context-sensitive help
chap-devops --help adapts to where you are. Outside a deployment directory it lists
only the commands that can work there:
Commands:
init Create a deployment directory: compose files, .env and .chap-devops/
run Start one model and print where it answers
ps List the models that run, and where each one answers
stop Stop a model and remove its compose overlay; its data stays
top Watch every chap-devops deployment on this machine as a live tree
models Browse and manage marketplace models
registry Inspect and refresh the marketplace registry
update Move the pins to what upstream publishes now, and pull
api Send one authenticated request to chap-core's API
chap Run the chap CLI in a container, with no Python or uv installed
chapkit Run the chapkit CLI in the chap-core image, with no Python installed
doctor Run a checklist over this machine and this deployment
cleanup Delete what deployments whose directory is gone left in docker
self Update chap-devops itself, and report what this build is
completions Print a shell completion script for chap-devops
help Print this message or the help of the given subcommand(s)
Inside one, the deployment commands are listed too: up, down, logs, restart,
status, sync, ui, components, docker, backup, auth, jobs, open and dhis2. The
hiding runs both ways, so init drops out of that listing: the deployment it
would create is already here. It still runs there - chap-devops init --force is how the API
port or the components of a deployment are rewritten, see
How the API port is set, and
chap-devops init sub creates a second deployment in a subdirectory of the first.
The hidden commands still run if you type them; they just tell you there is no
project. A group command typed on its own says the same thing rather than
listing subcommands that cannot run either, so chap-devops components outside a
deployment is the missing-project error, not the components help. chap-devops models is split the same way: list, search and info browse the
marketplace and work anywhere, while add, remove, enable, disable,
expose, unexpose and test need a deployment and are hidden outside one. The
command reference lists all of them, because it is rendered
from the static command tree rather than from where you happen to be.
Project discovery
Every command that operates on a project finds it the way git finds .git:
from the current directory, or from -C DIR, upwards to the nearest .chap-devops/.
So chap-devops up works from any subdirectory of the deployment, and chap-devops -C /srv/mychap status works from anywhere at all.
The wrappers then always run
docker compose -f <dir>/compose.yml -f <dir>/compose.chap-devops.yml -f <dir>/compose.marketplace.yml ...
with one -f per enabled component before the umbrella (and without
compose.yml and compose.chap-devops.yml when chap-core is not a component), from
the project directory, so they behave the same whatever your shell’s
working directory is, and they exit with Compose’s own exit code.
Commands
chap-devops [--json] [-C DIR] [--registry-url URL] [--offline] [--cache-dir DIR] <command>
The groups below are the shape of the tool. The
Command reference is generated from the --help texts and
lists every command, every flag and every default.
Running one model
| Command | What it does |
|---|---|
chap-devops run MODEL [--port N|auto] [--bind ADDR] [--group NAME] [--allow-starter] [--no-wait] [--timeout S] [-a [--rm]] [--chap-core URL] | Start one model - a marketplace id, a GitHub repository URL, an image on any registry or a local image - and print where it answers, once it does. Outside a deployment it runs in a chap-devops run group under the data directory, on 127.0.0.1; inside one, in that deployment. -a keeps it in the foreground with its log, and Ctrl-C then stops the model and keeps its data (--rm removes it). --chap-core URL makes the group’s models register with a chap-core that runs elsewhere. |
chap-devops ps [--group NAME] | Every model in every group (or in the deployment the command is inside), with its state and URL. |
chap-devops stop ID|--all|--group NAME [--purge] | Stop a model, in whichever group has it, or every model in a group, and remove its compose overlay; the data stays unless --purge, which also removes a group left empty. |
chap-devops top [--interval S] | Every chap-devops deployment on the machine as a live tree - init deployments and run groups, every service with its state, CPU, memory and URL - with logs and stop on a key. One snapshot off a terminal. See chap-devops top. |
See Running one model.
The everyday verbs
| Command | What it does |
|---|---|
chap-devops init [DIR] | Create a deployment directory: compose files, .env and .chap-devops/. --api-token protects the API from the start; --source PATH builds chap-core from a checkout; --chap-core-url URL uses a chap-core elsewhere; --adopt DIR takes over a chap-admin deployment with its database (see Taking over a chap-admin deployment); --with ocs[,s3][,dhis2] adds optional components, --only LIST deploys exactly those and nothing else, and --ocs-port, --s3-port, --ocs-read-only, --dhis2-port, --dhis2-seed and --dhis2-seed-password settle how they start. |
chap-devops sync [--check] | Render the compose files from .chap-devops/; --check writes nothing and exits non-zero if anything would change. |
chap-devops up [-a] [--pull] [--no-preflight] [--replace] [--wait [--timeout S]] [EXTRA..] | Sync, check that the host ports are free (offering to stop another chap-devops deployment that holds them; --replace does so without asking), then docker compose up -d --remove-orphans, and end with what started or was recreated. The orphans are the containers of models and components that were disabled; see Orphans for what that means for a compose file of your own. -a (also --attach, --foreground) runs in the foreground and streams the logs instead. A deployment with no components and no models has nothing to start, and up says so and names the commands that add one. --wait holds the command until chap-core answers and every model is registered (or, without chap-core, answers on its own port), and until OCS and DHIS2 answer their health check when each has a host port, then prints where each one answers; after --timeout seconds (300) for all of them together it fails and names what never did. |
chap-devops down [--volumes] [-y] [EXTRA..] | docker compose down, then say what it stopped and that the volumes are still there, naming the compose project they are prefixed with. --volumes removes them too, and the data in them: it names the volumes first and asks, unless -y/--yes says it was meant. --volumes also removes the leftover volumes that docker labels with this deployment’s compose project, such as the data volume of a removed model, and warns about each volume that is still there. Either way it ends on chap-devops up, which brings it back. |
chap-devops logs [-f] [--tail N] [SERVICE..] | docker compose logs; says so instead of printing nothing when the project has no containers, and lists the services when SERVICE is not one of them. --tail N keeps the last N lines of each service. When stdout is not a terminal, or under --no-color or NO_COLOR, the colour codes the containers print themselves are removed too, so a script reads plain text. |
chap-devops restart [SERVICE..] [--all] | Recreate the running services whose image or configuration changed (docker compose up -d --remove-orphans), and say which ones that was. Changes no file and no pin; --all recreates the named services anyway. |
chap-devops status [--url URL] [--timeout SECONDS] | GET /health and /v2/services, check that the answers are chap-core’s, and diff the registered services against the ones this project enabled. Sends the API token from .env when there is one, and says auth: on or auth: off. |
chap-devops open [NAME] | Open a component’s web interface in a browser: chap-core’s API documentation at /docs, the OCS web interface, DHIS2’s own; an enabled model’s id opens that model’s /docs, on its host port or through chap-core. With no name it lists every component and what each one opens. --no-browser prints the address instead of opening one, and --json never opens one. A component this deployment does not have, one that publishes no host port and the object store - which serves no web interface at all - are each refused with what is true instead and the command that changes it. |
chap-devops jobs [list] [--status S].. [--type T] [--limit N] | The backtests, predictions and datasets chap-core has run, newest first, with what each one cost and which of them failed. show, logs, cancel and delete take one job id, or enough of its start to name one. |
chap-devops api METHOD PATH [--data JSON|@FILE|-] [--url URL] [--raw] | One authenticated request to chap-core’s API, with this deployment’s base URL and token filled in. JSON comes back pretty-printed; the exit code is 0 for a 2xx, 1 for a 4xx/5xx and 2 when chap-core is not answering. |
chap-devops update [--dry-run] [--pin-chap-core] [--chap-tag TAG] [--list-tags] [--yes] | Move the pins to what upstream publishes now, pull the images, and end with one line saying what moved and what needs restarting. Never touches a container. --chap-tag moves chap-core to another tag (a release, latest, master or dev) instead, asking first when the move goes backwards, which --yes answers; --list-tags lists where it can go and writes nothing. Outside a deployment it refreshes the marketplace registry instead. |
chap-devops doctor | Run a checklist over this machine and this deployment: Docker, Compose, architecture, disk, the hosts Chap pulls from, and - inside a deployment - the files, the ports, the pins, the images and whether Chap is up. Works anywhere. |
chap-devops cleanup [--dry-run] [--yes] | Delete the volumes and network that deployments whose directory is gone left in docker, after listing them and asking. --dry-run (-n) only lists; --yes deletes without asking. Only deployments chap-devops recorded are considered, and nothing a container still uses. Works anywhere. See Cleaning up after removed deployments. |
chap-devops dhis2 connect | Let the DHIS2 Modeling App reach this deployment’s Chap: the chap route, the apps, then analytics. show, route, analytics and apps are the same work one step at a time; chap-devops dhis2 use points them at a DHIS2 that runs elsewhere. |
chap-devops up starts what is on disk, chap-devops update fetches newer versions, and
chap-devops restart applies them to the services that are running. That is the
whole distinction, and it is why up and update are both safe to run at any
time. See Updating.
chap-devops jobs and chap-devops api are the two that talk to the API rather than to
Docker. A job is one unit of slow work chap-core handed to its worker, and a
failed one says why in chap-devops jobs logs <id> and nowhere else; chap-devops api is
what starts a job from a terminal in the first place. See
Jobs and the API.
chap-devops doctor is the one to run first on a machine you have not deployed on
before, and first again when something is wrong: it asks in one pass what the
other commands assume. It exits non-zero only when a check failed. See
Doctor.
chap-devops down --volumes is the reset that takes the data with it
(docker compose down -v --remove-orphans): it lists the volumes docker holds
under this deployment’s name - with how much the OCS data volume holds next to
it, when that can be measured - asks before removing them and ends with the
ones that are gone, by name. -y (--yes) is how a script says it meant it, and is
required where there is nothing to ask at - a stdin that is not a terminal, or
--json - rather than the question being skipped. Compose removes the volumes
its files still declare, so a volume left over from a disabled model survives;
chap-devops doctor finds those, and chap-devops models disable <id> --purge removes
them. There is no -v for this: -v is the global --verbose flag, so a -v
that reaches the passthrough (chap-devops down -- -v) is refused and told to use
--volumes, rather than being passed on to compose or quietly dropped.
Models
| Command | What it does |
|---|---|
chap-devops models list | List marketplace models (--all, --starters, --enabled). |
chap-devops models search QUERY | Search id, name and summary. |
chap-devops models info ID | Everything known about one model. |
chap-devops models test [ID..] [--all] | Make each model train and predict, and say whether it could: chapkit test in its own container, or --backtest for the whole way round through chap-core (--config NAME, --seed, --timeout, --keep). With --backtest, the line under each row names the configured model it used. |
chap-devops models configs [list] [ID] [--all] | List the configured models that chap-core has of each enabled model: name, covariates, options and source; --all adds the archived ones. See Configured models. |
chap-devops models configs add ID | Add a configured model: --name and --set KEY=VALUE (--covariates a,b), or --from FILE for every configuration in a file of the marketplace format. With no options at a terminal, it opens the form. |
chap-devops models configs update ID NAME | Give a configured model new values (--set, --unset, --covariates), or open the form with its current values. chap-core keeps the old values as an archived configured model. |
chap-devops models configs archive ID NAME | Archive a configured model. chap-core keeps it, and the Modeling App shows it as Archived. |
chap-devops models configs export [ID] [--out FILE] | Write the configured models of one model as the configurations: block of a marketplace entry. |
chap-devops models configs sync [ID..] | Create the configured models that chap-core needs to run each model: one for each configuration of its marketplace entry. A model that has one of its version already is left alone. See Sync. |
chap-devops models new DIR | Create a model project from a template (--template, --with-validation): a chapkit service or an MLproject, in Python or R. The build runs in Docker, so Python and chapkit are not necessary. See A new model project. |
chap-devops models add SOURCE | Add a model the marketplace does not list, from a GitHub repository URL or an image reference on any registry, and enable it (--id, --service-id, --name, --port, --bind, --data-dir, --user, --runtime-amd64). A source the marketplace lists is enabled as that marketplace model unless --id is given; --id auto picks a free id. |
chap-devops models remove ID [--purge] [--keep-in-chap] | Disable such a model if it is on, drop its definition from .chap-devops/models-manual.yaml, and retire it in chap-core; --purge removes its data volume too, --keep-in-chap leaves chap-core alone. |
chap-devops models enable ID [ID..] | Record each model in .chap-devops/models.yaml and write its overlay (--channel, --version, --port, --bind, --data-dir, --user, --allow-starter). |
chap-devops models disable ID [--purge] [--retire] | Drop the model from .chap-devops/models.yaml and remove its overlay, keeping its data volume and naming it; --purge removes that volume too, --retire retires the model in chap-core. |
chap-devops models expose ID [--port N|auto] [--bind ADDR] | Publish a host port for an enabled model, without touching the version it is pinned to; --bind 127.0.0.1 keeps it on this machine. See Ports. |
chap-devops models unexpose ID | Remove the host port of an enabled model. |
chap-devops ui | Open the browser: marketplace models and components, on two pages Tab moves between. One s saves both. |
list, search and info read the catalogue and work outside a project. The
rest need a project; of them, only test, configs list and configs export
change nothing.
Registration is a heartbeat, so chap-devops status and chap-devops doctor can be green
while a model cannot predict at all - chap-devops models test is the check that
settles it. See Testing a model. add is the only one that needs the network for
something other than the catalogue: it reads GitHub and ghcr to resolve what it
was given. See Models and the marketplace.
Components
| Command | What it does |
|---|---|
chap-devops components list | Every component, whether this deployment has it and where it is reached. |
chap-devops components enable NAME [--port N] | Enable a component, or change the settings of an enabled one, then sync. ocs also takes --ocs-name, --ocs-country and --ocs-bbox for the instance config it scaffolds, plus --base-url and --read-only/--read-write. dhis2 also takes --tag, --image and --seed. A port already in use is a warning, not a refusal. |
chap-devops components disable NAME [--purge] | Turn it off, remove its compose file and sync, keeping its data volumes and naming each one; --purge removes them too, and is refused for chap-core. |
A component is a service (or a small group) that chap-devops sync renders one
compose file for: chap-core, which is Chap itself and is on unless you turn it
off, ocs (Open Climate Service), s3 (the object store OCS will use) and
dhis2 (a demo or development DHIS2 with its own database). The set lives in
.chap-devops/components.yaml. chap-devops init --with ocs,s3, --without chap-core and
--only ocs,s3 set it at creation time. chap-devops open NAME opens one of them in a browser, and
o does the same from the browser’s components page. See
Components,
Reaching a component from a browser
and DHIS2.
DHIS2
For a deployment with the dhis2 component, or with an
external DHIS2 recorded by chap-devops dhis2 use. Apart from use, which records a DHIS2 in .chap-devops/components.yaml, and
connect, which records when it ran in the same file, nothing here touches
Docker or a file: every verb is requests to DHIS2’s own API.
| Command | What it does |
|---|---|
chap-devops dhis2 show [--user NAME] | Where the chap route points, what DHIS2’s analytics timestamp is worth, which of the two apps are installed, and each piece that is missing. Writes nothing. |
chap-devops dhis2 route | Create the chap route, or repoint one that points elsewhere, is disabled or has lost the F_CHAP_MODELING_APP authority - then proxy a request through it to prove chap-core answers. |
chap-devops dhis2 analytics [--timeout SECONDS] [--no-wait] | Generate the analytics tables the Modeling and Climate apps read, and wait for the run; a run that is already going is watched rather than queued behind, since DHIS2 runs one at a time. |
chap-devops dhis2 apps | Install the Modeling App and the DHIS2 Climate App from the App Hub, at the newest version this DHIS2 can run. The one verb that needs the network. |
chap-devops dhis2 connect [--timeout SECONDS] [--no-wait] | The route, the apps, then analytics. Under --offline the apps are a reported skip and the other two still happen. On an external DHIS2 only the route; the other two are left to its admin. |
chap-devops dhis2 use [URL] [--chap-url URL] [--clear] | Record a DHIS2 that runs elsewhere, and where it reaches chap-core, then ask it whether it answers and takes the credential. Bare, it says which DHIS2 is used; --clear forgets it. |
Every one of them is idempotent: a second run repoints nothing, reinstalls nothing
and says so. All of them take --user NAME and --wait SECONDS (how long to wait
for DHIS2’s API: 20 minutes by default for the dhis2 component, because a first
start takes minutes, and 60 seconds for an external DHIS2). The credential comes
from DHIS2_API_TOKEN or DHIS2_ADMIN_USERNAME/DHIS2_ADMIN_PASSWORD in
.env, else CHAP_DEVOPS_DHIS2_TOKEN or CHAP_DEVOPS_DHIS2_PASSWORD in the environment,
else the seed password, else admin/district (only on a DHIS2 that chap-devops
deployed). See The
credentials. There is no
--password flag, because a password on a command line is in the shell history. chap-devops up does none of this and
the chapter says why - so
chap-devops up and chap-devops status each end by naming chap-devops dhis2 connect until one
has been recorded in .chap-devops/components.yaml. That record suppresses the hint
and is never evidence that the route is right; chap-devops dhis2 show is what asks
DHIS2. See Connecting the Modeling App to
Chap and connected_at, and
what it is not.
Authentication
| Command | What it does |
|---|---|
chap-devops auth show [--reveal] | Whether the API token and the registration key are set, and the token itself only with --reveal. |
chap-devops auth token | The token alone on stdout, for TOKEN=$(chap-devops auth token); nothing on stdout and exit 1 when authentication is off. |
chap-devops auth enable [--token VALUE] | Write both secrets to .env, record them in .chap-devops/project.yaml and re-render the overlays. |
chap-devops auth disable | Comment both .env lines out, keeping their values, and re-render. |
chap-devops auth rotate | Replace both secrets with new generated ones. |
All four touch only those two .env lines, and none of them restarts anything:
chap-core and the models read .env when Compose creates them, so chap-devops up
has to follow. chap-devops init --api-token does the same thing at creation time.
See Authentication.
The chap CLI
| Command | What it does |
|---|---|
chap-devops chap [--tag TAG] [--image core|worker] [--group NAME] [--stop] [--timeout S] [--docker] CHAP_ARGS.. | Run chap-core’s own chap CLI in a container, with the current directory mounted at the same path, so its file arguments and outputs are on this machine. A model id in --model-name starts that model, without chap-core and without chap-devops up, and --stop stops it after the run. The image follows --model-name; a deployment gives its tag and its network. --docker gives the container the docker socket, for docker_env models. Works anywhere. |
chap-devops chapkit [--tag TAG] [--image IMAGE] CHAPKIT_ARGS.. | Run the chapkit CLI in the chap-core image, with the current directory mounted at the same path. mlproject migrate DIR makes a chapkit service from an MLproject model, and chap-devops then writes its uv.lock. mlproject run DIR serves an MLproject model in the foreground, in a chapkit -cli image or --image, and in a deployment registers it with chap-core. mlproject export main.py writes the MLproject of a chapkit service. Works anywhere. See An MLproject model, with no image to build. |
You need no Python, uv or R for it. See The chap CLI.
Registry
| Command | What it does |
|---|---|
chap-devops registry update | Fetch the catalogue now and refresh the cache. |
chap-devops registry show | Where the catalogue came from and what it holds. |
Docker
The Docker plumbing you only reach for when you already know what Docker is
doing lives under chap-devops docker, so the top-level list stays readable if you
have never used Compose.
| Command | What it does |
|---|---|
chap-devops docker ps [EXTRA..] | List the containers this deployment is running (docker compose ps). |
chap-devops docker pull | Download the pinned images into the local Docker daemon; no files change. |
chap-devops docker exec SERVICE [CMD..] | Run a command in a running container, which has to be up already (chap-devops up). CMD defaults to a shell, and -T is passed for you when there is no terminal, so it works in scripts. |
chap-devops docker run -- ARGS.. | Any docker compose command, behind the deployment’s -f list. |
chap-devops docker config [-- EXTRA..] | The finished configuration: every compose file merged into one document (--json prints it as JSON). |
chap-devops docker config is what Docker actually reads after the -f list, the
include: and the .env substitutions have been applied, which is the fastest
way to find out why a setting is not taking effect.
Backup
| Command | What it does |
|---|---|
chap-devops backup create [--out PATH] [--no-db] [--no-models] [--no-components] | Write the chap-core database (pg_dump), the model data, the component data (the DHIS2 database as a pg_dump) and the deployment files to one tar.gz. |
chap-devops backup restore ARCHIVE [--yes] [--files-only] [--db-only] [--no-models] [--no-components] [--adopt-identity] [--no-start] | Put a deployment back from such an archive, after printing what it overwrites. |
See Backup and restore.
chap-devops itself
The two commands that are about the CLI rather than about a deployment. Both work anywhere, inside a deployment or not.
| Command | What it does |
|---|---|
chap-devops self update [--check] [--version TAG] [--yes] | Replace this binary with the newest release: download the archive for this target, check it against the release’s SHA256SUMS, rename it over the running one. --check only reports. |
chap-devops self version | Version, git revision, target triple, binary path and whether it came from a release archive, cargo install, a cargo build in a checkout, or another local build. |
chap-devops completions SHELL | Print a completion script for bash, zsh, fish, PowerShell or elvish. |
chap-devops update moves a deployment’s pins; chap-devops self update replaces the
chap-devops binary. Nothing in the first touches the second. See
Install for both of these and for the once-a-day notice that a
newer release exists.
Global options
They are accepted before or after the subcommand, on every command, and the reference lists them with their defaults. The last three are the registry options. The help lists them under “Registry options” only for the commands that use them; Global options names those commands. On the other commands they change only the once-a-day notice that a newer release exists.
| Option | What it does |
|---|---|
--json | Machine-readable output: exactly one JSON document on stdout. |
--no-color | Never colour the output; NO_COLOR in the environment does the same. |
-v, --verbose | Show the hints: the next commands and the background. -vv also narrates on stderr what runs: commands, HTTP requests, the registry source, the files sync compared. |
-d, --debug | Everything -vv says, plus response bodies and resolved paths. |
-C, --project-dir DIR | Where to look for the project; found like git finds .git. |
--registry-url URL | A different marketplace index, for a fork or a mirror. Inside a deployment the default is the one init recorded. |
--offline | Never touch the network; use the cache or the embedded snapshot. Shown by the commands that read the registry, and by dhis2 apps, dhis2 connect and self update, which use it for the App Hub and the release feed. |
--cache-dir DIR | Override the cache directory for one invocation: the registry snapshot, and the downloads of self update. |
--json for scripts and tools
Under --json every command prints exactly one JSON document on stdout, and
everything meant for a person - progress, notes, warnings - goes to stderr.
A tool that drives chap-devops reads stdout and branches on one field:
-
Success of a command that changes something (
models enable,disable,add,remove,expose,unexpose,up,models test) carries"ok": true. The read-only commands (status,models list,models info) print their report as it is;statussays how things are in its own exit code. -
Failure of any command is
{"ok": false, "error": "...", "hint": "run `chap-devops ...`", "causes": []}hintis the way out: the message’s last clause when that clause names a command or a flag to use instead (pass \–id auto` … to add this one beside it), andnullwhen the message has no way out to give.erroris the message up to that clause, so the two never say the same thing; the terminal shows them together aftererror:. A message of several lines, for example the port check with more than one busy port, stays whole inerror, withhint: null, because each line has its own way out. A usage error - an unknown flag, a missing argument - is the same document, with clap's message aserrorand the command's–helpashint`, and exits 2. -
models enableandmodels addlist what they enabled undermodels, one object per model withid,service_id,port,bindandurl(the model’s own port, else chap-core’s proxy to it, elsenull), next to the full change report. -
upprintsapi_url, the compose servicesrunningand the ones this runstarted, every enabled model the same way, and under--waitwhat the wait found:wait.ready,wait.waited_sand each model’sstateandurl. -
models testprints{"ok": ..., "models": [...]}, one object per model with itsresult(pass,failorskip), thesummaryand thedetailthat says why and what to do. -
models configs syncprints{"ok": ..., "models": [...]}, one object per model withid,service_idand itsstate:created(with the names inconfigured),already-configured,not-registeredorfailed(with the reason inerror). -
models configs listprints{"models": [...]}, one object per model withid,service_idandconfigs. Each configured model hasid,name,variant,archived,version,covariates,valuesandsource. -
models configs addprints{"ok": true, "model": ..., "configured": [...]}, with the rows that chap-core created. -
models configs updateprintsmodel,changed,configured(the new row) andarchived(the old row). If nothing changed,changedisfalse. -
models configs archiveprints{"ok": true, "model": ..., "archived": ...}. -
models configs exportprintsmodel,configurations(the block, as JSON) andprediction_periods_from, which says where eachprediction_periodscomes from.
The exit code agrees with ok: 0 for true, non-zero for false. Two
commands print "ok": false rather than an error and still exit non-zero:
models test, when a model failed or every model was skipped, and models configs sync, when a model could not be configured.
Command reference
Generated from the --help texts by make docs-reference; edit
src/cli.rs and run that target rather than editing this file.
Every command accepts the options listed under chap-devops, --json
included. The registry options (--registry-url, --offline, --cache-dir)
show only in the help of the commands that use them, and each such command
names them below. Commands that need a deployment directory are hidden from
chap-devops --help outside one, but they are all listed here.
chap-devops
deploy Chap (climate-informed disease forecasting) and its services.
Usage: chap-devops [OPTIONS] <COMMAND>
| Global option | Description |
|---|---|
--json | Print machine-readable JSON instead of human output. |
--no-color | Never colour the output (NO_COLOR does the same). |
-v, --verbose | Show the hints; -vv also shows the commands and requests that run. |
-d, --debug | -vv plus the raw responses. |
-C, --project-dir <DIR> | Deployment directory, or any directory inside one. Default: .. |
--registry-url <URL> | URL of the marketplace registry index. Default: https://raw.githubusercontent.com/dhis2-chap/model-marketplace/main/registry.yaml. |
--offline | Never touch the network; use what is cached or on this machine. |
--cache-dir <DIR> | Cache directory: the registry snapshot and the self-update downloads. |
Subcommands: chap-devops init, chap-devops components, chap-devops models, chap-devops registry, chap-devops sync, chap-devops update, chap-devops auth, chap-devops dhis2, chap-devops up, chap-devops down, chap-devops restart, chap-devops status, chap-devops logs, chap-devops open, chap-devops ui, chap-devops run, chap-devops ps, chap-devops stop, chap-devops jobs, chap-devops api, chap-devops chap, chap-devops chapkit, chap-devops backup, chap-devops doctor, chap-devops docker, chap-devops top, chap-devops cleanup, chap-devops self, chap-devops completions
chap-devops init
Create a deployment directory: compose files, .env and .chap-devops/.
Usage: chap-devops init [OPTIONS] [DIR]
| Argument | Description |
|---|---|
<DIR> | Directory to create. Default: .. |
--models <SPEC> | Models to enable: none, all, or a list of ids. Default: none. |
--interactive | Select the models in the browser instead of with –models. |
--chap-tag <TAG> | chap-core image tag: latest, master, dev or vX.Y.Z. Default: latest. |
--force | Overwrite an existing deployment in the target directory. |
--api-port <PORT> | Host port to publish chap-core’s API on. Default: 8700. |
--port-base <PORT> | Lowest host port a model may be published on. Default: 5001. |
--api-token <TOKEN> | Protect the API with a token (generated when no value is given). |
--no-env | Do not write a .env file. |
--fresh-env | Regenerate .env with a new database password. |
--source <PATH> | Build chap-core from the chap-core checkout at PATH. |
--with <LIST> | Components to add: ocs, s3, dhis2. |
--without <LIST> | Components to omit: chap-core, ocs, s3, dhis2. |
--only <LIST> | Exactly these components and nothing else; none for models alone. |
--adopt <DIR> | Take over the chap-admin deployment in DIR, with its database. |
--chap-core-url <URL> | Register the models with the chap-core at URL instead of running one. |
--ocs-base-url <URL> | Public URL OCS is reached at, for a proxied instance. |
--ocs-port <PORT|none> | Host port to publish OCS on, or none to keep it internal. |
--s3-port <PORT|none> | Host port to publish the object store on, or none. |
--dhis2-port <PORT|none> | Host port to publish DHIS2 on, or none to keep it internal. |
--dhis2-seed <SPEC> | Dump to seed the DHIS2 database from: default, none, URL or path. |
--dhis2-seed-password <PW> | Give every DHIS2 user this password (default: district). |
--dhis2-tag <TAG> | DHIS2 version to run: an image tag such as 2.41, 2.42 or 2.43.1. |
--dhis2-image <REPO> | DHIS2 image repository, e.g. dhis2/core-dev for unreleased versions. |
--ocs-read-only | Refuse ingestion over HTTP on the new OCS instance. |
--ocs-name <NAME> | Country or region the OCS instance covers, e.g. Malawi. |
--ocs-country <CODE> | ISO 3166-1 alpha-3 country code for the OCS extent. |
--ocs-bbox <BBOX> | OCS extent as xmin,ymin,xmax,ymax in degrees. |
Registry options: --registry-url <URL>, --offline, --cache-dir <DIR> (see chap-devops).
chap-devops components
Show and change what this deployment is made of.
Usage: chap-devops components [OPTIONS] <COMMAND>
Subcommands: chap-devops components list, chap-devops components enable, chap-devops components disable
chap-devops components list
List every component and whether this deployment has it.
Usage: chap-devops components list [OPTIONS]
chap-devops components enable
Enable a component, or change the settings of an enabled one.
Usage: chap-devops components enable [OPTIONS] <NAME>
| Argument | Description |
|---|---|
<NAME> | Component name: ocs, s3, dhis2 or chap-core. |
--port <PORT|none> | Host port to publish on, or none to keep it internal. |
--base-url <URL> | Public URL OCS is reached at, for a proxied instance. |
--read-only | Refuse ingestion over HTTP on this OCS instance. |
--read-write | Allow ingestion over HTTP again. |
--url <URL> | chap-core only: use the chap-core at URL instead of running one. |
--tag <TAG> | dhis2 only: the DHIS2 version to run, an image tag such as 2.43. |
--image <REPO> | dhis2 only: the image repository, e.g. dhis2/core-dev. |
--seed <SPEC> | dhis2 only: dump for a new database: default, none, URL or path. |
--models-host <HOST> | With –url: the host that chap-core calls the models back at. |
--ocs-name <NAME> | Country or region the OCS instance covers, e.g. Malawi. |
--ocs-country <CODE> | ISO 3166-1 alpha-3 country code for the OCS extent. |
--ocs-bbox <BBOX> | OCS extent as xmin,ymin,xmax,ymax in degrees. |
Registry options: --registry-url <URL>, --offline, --cache-dir <DIR> (see chap-devops).
chap-devops components disable
Disable a component and remove its compose file.
Usage: chap-devops components disable [OPTIONS] <NAME>
| Argument | Description |
|---|---|
<NAME> | Component name: ocs, s3, dhis2 or chap-core. |
--purge | Delete the component’s data volume as well. |
Registry options: --registry-url <URL>, --offline, --cache-dir <DIR> (see chap-devops).
chap-devops models
Browse and manage marketplace models.
Usage: chap-devops models [OPTIONS] <COMMAND>
Registry options: --registry-url <URL>, --offline, --cache-dir <DIR> (see chap-devops).
Subcommands: chap-devops models list, chap-devops models search, chap-devops models info, chap-devops models test, chap-devops models configs, chap-devops models new, chap-devops models add, chap-devops models remove, chap-devops models enable, chap-devops models disable, chap-devops models expose, chap-devops models unexpose
chap-devops models list
List marketplace models.
Usage: chap-devops models list [OPTIONS]
| Argument | Description |
|---|---|
--all | Include every entry, starters as well as models. |
--starters | List only starters. |
--enabled | List only the models enabled in this deployment. |
Registry options: --registry-url <URL>, --offline, --cache-dir <DIR> (see chap-devops).
chap-devops models search
Search the marketplace by id, name or summary.
Usage: chap-devops models search [OPTIONS] <QUERY>
| Argument | Description |
|---|---|
<QUERY> | Text to look for; matching is case-insensitive. |
Registry options: --registry-url <URL>, --offline, --cache-dir <DIR> (see chap-devops).
chap-devops models info
Show everything known about one model.
Usage: chap-devops models info [OPTIONS] <ID>
| Argument | Description |
|---|---|
<ID> | Marketplace id or service id. |
Registry options: --registry-url <URL>, --offline, --cache-dir <DIR> (see chap-devops).
chap-devops models test
Make a model train and predict, and say whether it could.
Usage: chap-devops models test [OPTIONS] [ID]...
| Argument | Description |
|---|---|
<ID>... | Marketplace ids or service ids of the models to test. |
--all | Test every model this deployment has enabled. |
--backtest | Run a backtest through chap-core instead of the model’s own test. |
--config <NAME> | Variant name of the configured model to backtest, as in models configs. |
--seed <N> | Seed for the generated data, so a run can be repeated. |
--timeout <SECONDS> | Time limit for the test of one model, in seconds. |
--configs <N> | Configs that chapkit test creates for each model. |
--trainings <N> | Trainings for each config. |
--predictions <N> | Predictions for each trained model. |
--rows <N> | Rows of training data (locations times periods). |
--predict-rows <N> | Rows of the history and of the future data for each prediction. |
--keep | Keep what the test created instead of deleting it. |
Registry options: --registry-url <URL>, --offline, --cache-dir <DIR> (see chap-devops).
chap-devops models configs
List, add, archive and sync the configured models of each model.
Usage: chap-devops models configs [OPTIONS] [ID]
chap-devops models configs <COMMAND>
| Argument | Description |
|---|---|
<ID> | Marketplace id or service id; all enabled models when none is given. |
--all | Show the archived configured models too. |
Subcommands: chap-devops models configs list, chap-devops models configs add, chap-devops models configs update, chap-devops models configs archive, chap-devops models configs export, chap-devops models configs sync
chap-devops models configs list
List the configured models chap-core has of each enabled model.
Usage: chap-devops models configs list [OPTIONS] [ID]
| Argument | Description |
|---|---|
<ID> | Marketplace id or service id; all enabled models when none is given. |
--all | Show the archived configured models too. |
Registry options: --registry-url <URL>, --offline, --cache-dir <DIR> (see chap-devops).
chap-devops models configs add
Add a configured model of a model to chap-core.
Usage: chap-devops models configs add [OPTIONS] <ID>
| Argument | Description |
|---|---|
<ID> | Marketplace id or service id. |
--name <NAME> | Variant name, which the Modeling App shows in brackets. |
--set <KEY=VALUE> | Value of one user option; repeat for more than one. |
--covariates <A,B> | Additional covariates, separated by commas. |
--from <FILE> | Add every configuration in a file of the marketplace format. |
Registry options: --registry-url <URL>, --offline, --cache-dir <DIR> (see chap-devops).
chap-devops models configs update
Change the values of a configured model of a model.
Usage: chap-devops models configs update [OPTIONS] <ID> <NAME>
| Argument | Description |
|---|---|
<ID> | Marketplace id or service id. |
<NAME> | Variant name of the configured model. |
--set <KEY=VALUE> | New value of one user option; repeat for more than one. |
--unset <KEY> | Remove the value of one option, so the model default applies. |
--covariates <A,B> | Additional covariates, separated by commas, in place of the old ones. |
Registry options: --registry-url <URL>, --offline, --cache-dir <DIR> (see chap-devops).
chap-devops models configs archive
Archive a configured model of a model.
Usage: chap-devops models configs archive [OPTIONS] <ID> <NAME>
| Argument | Description |
|---|---|
<ID> | Marketplace id or service id. |
<NAME> | Variant name of the configured model. |
Registry options: --registry-url <URL>, --offline, --cache-dir <DIR> (see chap-devops).
chap-devops models configs export
Write the configured models of a model in the marketplace format.
Usage: chap-devops models configs export [OPTIONS] [ID]
| Argument | Description |
|---|---|
<ID> | Marketplace id or service id; may be left out with one enabled model. |
--out <FILE> | File to write instead of standard output. |
Registry options: --registry-url <URL>, --offline, --cache-dir <DIR> (see chap-devops).
chap-devops models configs sync
Create the configured models that chap-core needs to run each model.
Usage: chap-devops models configs sync [OPTIONS] [ID]...
| Argument | Description |
|---|---|
<ID>... | Marketplace ids or service ids; all enabled models when none is given. |
Registry options: --registry-url <URL>, --offline, --cache-dir <DIR> (see chap-devops).
chap-devops models new
Create a model project from a template.
Usage: chap-devops models new [OPTIONS] <DIR>
| Argument | Description |
|---|---|
<DIR> | Directory to create; its name becomes the model name. |
--template <TYPE> | The kind of project and its base image. Values: fn-py, shell-py, shell-r, shell-r-tidyverse, shell-r-inla, mlproject-py, mlproject-r, mlproject-r-tidyverse, mlproject-r-inla. Default: fn-py. |
--with-validation | Add $validate hooks to main.py (chapkit service types only). |
chap-devops models add
Add a model the marketplace does not list.
Usage: chap-devops models add [OPTIONS] <SOURCE>
| Argument | Description |
|---|---|
<SOURCE> | GitHub repository URL, image reference, or local image name:tag. |
--id <ID|auto> | Identifier to record the model under, or auto for a free one. |
--service-id <ID> | Compose service name, which must match the service’s own id. |
--name <NAME> | Name to show in the listings. |
--port <PORT|auto> | Host port to publish the model on, or auto for a free one. |
--bind <ADDR> | Host address to publish the port on, e.g. 127.0.0.1 for this machine. |
--data-dir <PATH> | Data directory inside the container. |
--user <USER> | User the container runs as, as user:group. |
--runtime-amd64 | Record the image as published for amd64 only. |
Registry options: --registry-url <URL>, --offline, --cache-dir <DIR> (see chap-devops).
chap-devops models remove
Remove a model that was added with models add.
Usage: chap-devops models remove [OPTIONS] <ID>
| Argument | Description |
|---|---|
<ID> | Id of a model added with models add. |
--purge | Delete the model’s data volume as well. |
--keep-in-chap | Leave the model and its configured models live in chap-core. |
Registry options: --registry-url <URL>, --offline, --cache-dir <DIR> (see chap-devops).
chap-devops models enable
Enable a model and write its compose overlay.
Usage: chap-devops models enable [OPTIONS] <ID>...
| Argument | Description |
|---|---|
<ID>... | Marketplace ids or service ids, separated by spaces or commas. |
--channel <CHANNEL> | Follow a release channel instead of pinning a version. Values: stable, latest. |
--version <VERSION> | Pin an exact version from the model’s versions list. |
--port <PORT|auto> | Host port to publish the model on, or auto for a free one. |
--bind <ADDR> | Host address to publish the port on, e.g. 127.0.0.1 for this machine. |
--data-dir <PATH> | Data directory inside the container. |
--user <USER> | User the container runs as, as user:group. |
--allow-starter | Enable a starter even though it is not a model. |
Registry options: --registry-url <URL>, --offline, --cache-dir <DIR> (see chap-devops).
chap-devops models disable
Disable a model and remove its compose overlay.
Usage: chap-devops models disable [OPTIONS] <ID>
| Argument | Description |
|---|---|
<ID> | Marketplace id or service id. |
--purge | Delete the model’s data volume as well. |
--retire | Retire the model in chap-core too, with its configured models. |
Registry options: --registry-url <URL>, --offline, --cache-dir <DIR> (see chap-devops).
chap-devops models expose
Publish a host port for an enabled model.
Usage: chap-devops models expose [OPTIONS] <ID>
| Argument | Description |
|---|---|
<ID> | Marketplace id or service id. |
--port <PORT|auto> | Host port to publish on, or auto for the lowest free one. |
--bind <ADDR> | Host address to publish the port on, e.g. 127.0.0.1 for this machine. |
Registry options: --registry-url <URL>, --offline, --cache-dir <DIR> (see chap-devops).
chap-devops models unexpose
Remove the host port of an enabled model.
Usage: chap-devops models unexpose [OPTIONS] <ID>
| Argument | Description |
|---|---|
<ID> | Marketplace id or service id. |
Registry options: --registry-url <URL>, --offline, --cache-dir <DIR> (see chap-devops).
chap-devops registry
Inspect and refresh the marketplace registry.
Usage: chap-devops registry [OPTIONS] <COMMAND>
Registry options: --registry-url <URL>, --offline, --cache-dir <DIR> (see chap-devops).
Subcommands: chap-devops registry update, chap-devops registry show
chap-devops registry update
Fetch the registry from the network and refresh the cache.
Usage: chap-devops registry update [OPTIONS]
Registry options: --registry-url <URL>, --offline, --cache-dir <DIR> (see chap-devops).
chap-devops registry show
Show where the registry was loaded from and what it holds.
Usage: chap-devops registry show [OPTIONS]
Registry options: --registry-url <URL>, --offline, --cache-dir <DIR> (see chap-devops).
chap-devops sync
Render the compose files from .chap-devops/.
Usage: chap-devops sync [OPTIONS]
| Argument | Description |
|---|---|
--check | Write nothing; exit non-zero if anything would change. |
Registry options: --registry-url <URL>, --offline, --cache-dir <DIR> (see chap-devops).
chap-devops update
Move the pins to what upstream publishes now, and pull.
Usage: chap-devops update [OPTIONS]
| Argument | Description |
|---|---|
--dry-run | Show what would change: no pull, and nothing written. |
--pin-chap-core | Pin a moving chap-core tag to the newest release. |
--chap-tag <TAG> | Move chap-core to this tag: vX.Y.Z, latest, master or dev. |
--list-tags | List the chap-core tags you can move to, newest first. |
--yes | Answer yes to the confirmation a backwards move asks for. |
Registry options: --registry-url <URL>, --offline, --cache-dir <DIR> (see chap-devops).
chap-devops auth
Enable or disable API authentication, and show the token.
Usage: chap-devops auth [OPTIONS] <COMMAND>
Subcommands: chap-devops auth show, chap-devops auth token, chap-devops auth enable, chap-devops auth disable, chap-devops auth rotate
chap-devops auth show
Say whether the API is protected, and by which token.
Usage: chap-devops auth show [OPTIONS]
| Argument | Description |
|---|---|
--reveal | Print the API token. |
chap-devops auth token
Print the API token alone, for a script to capture.
Usage: chap-devops auth token [OPTIONS]
chap-devops auth enable
Enable authentication: write both secrets and sync.
Usage: chap-devops auth enable [OPTIONS]
| Argument | Description |
|---|---|
--token <TOKEN> | Use this API token instead of generating one. |
Registry options: --registry-url <URL>, --offline, --cache-dir <DIR> (see chap-devops).
chap-devops auth disable
Disable authentication; keep both values as comments.
Usage: chap-devops auth disable [OPTIONS]
Registry options: --registry-url <URL>, --offline, --cache-dir <DIR> (see chap-devops).
chap-devops auth rotate
Replace both secrets with new generated ones.
Usage: chap-devops auth rotate [OPTIONS]
Registry options: --registry-url <URL>, --offline, --cache-dir <DIR> (see chap-devops).
chap-devops dhis2
Let the DHIS2 Modeling App reach this deployment’s Chap.
Usage: chap-devops dhis2 [OPTIONS] <COMMAND>
Subcommands: chap-devops dhis2 show, chap-devops dhis2 route, chap-devops dhis2 analytics, chap-devops dhis2 apps, chap-devops dhis2 connect, chap-devops dhis2 use
chap-devops dhis2 show
Say what DHIS2 has: the chap route, analytics and the apps.
Usage: chap-devops dhis2 show [OPTIONS]
| Argument | Description |
|---|---|
--user <NAME> | DHIS2 user to authenticate as, with a password rather than a token. |
--wait <SECONDS> | Seconds to wait for DHIS2’s API; 1200 local, 60 external by default. |
chap-devops dhis2 route
Point DHIS2’s chap route at this deployment’s chap-core.
Usage: chap-devops dhis2 route [OPTIONS]
| Argument | Description |
|---|---|
--user <NAME> | DHIS2 user to authenticate as, with a password rather than a token. |
--wait <SECONDS> | Seconds to wait for DHIS2’s API; 1200 local, 60 external by default. |
chap-devops dhis2 analytics
Generate DHIS2’s analytics tables and wait for them.
Usage: chap-devops dhis2 analytics [OPTIONS]
| Argument | Description |
|---|---|
--user <NAME> | DHIS2 user to authenticate as, with a password rather than a token. |
--wait <SECONDS> | Seconds to wait for DHIS2’s API; 1200 local, 60 external by default. |
--timeout <SECONDS> | Seconds to wait for the analytics run to finish. Default: 3600. |
--no-wait | Start the run and return without a wait for it to finish. |
chap-devops dhis2 apps
Install the Modeling and Climate apps from the App Hub.
Usage: chap-devops dhis2 apps [OPTIONS]
| Argument | Description |
|---|---|
--user <NAME> | DHIS2 user to authenticate as, with a password rather than a token. |
--wait <SECONDS> | Seconds to wait for DHIS2’s API; 1200 local, 60 external by default. |
Registry options: --offline (see chap-devops).
chap-devops dhis2 connect
The route, the apps, then analytics; the route only on an external DHIS2.
Usage: chap-devops dhis2 connect [OPTIONS]
| Argument | Description |
|---|---|
--user <NAME> | DHIS2 user to authenticate as, with a password rather than a token. |
--wait <SECONDS> | Seconds to wait for DHIS2’s API; 1200 local, 60 external by default. |
--timeout <SECONDS> | Seconds to wait for the analytics run to finish. Default: 3600. |
--no-wait | Start the run and return without a wait for it to finish. |
Registry options: --registry-url <URL>, --offline, --cache-dir <DIR> (see chap-devops).
chap-devops dhis2 use
Use a DHIS2 that runs elsewhere, or show which DHIS2 is used.
Usage: chap-devops dhis2 use [OPTIONS] [URL]
| Argument | Description |
|---|---|
<URL> | DHIS2’s URL, as this machine reaches it. |
--chap-url <URL> | chap-core’s URL, as that DHIS2 reaches it; the route points here. |
--clear | Forget the external DHIS2 and use the dhis2 component again. |
chap-devops up
Sync, then start Chap (docker compose up).
Usage: chap-devops up [OPTIONS] [EXTRA]...
| Argument | Description |
|---|---|
-a, --attach, --foreground | Run in the foreground and stream all logs (Ctrl-C stops Chap). |
--pull | Pull every image first (docker compose up –pull always). |
--no-preflight | Do not check the host ports first. |
--replace | First stop the other chap-devops deployments that hold these ports. |
--wait | Return only once chap-core, the models, OCS and DHIS2 answer. |
--timeout <SECONDS> | How long –wait waits before it fails, in seconds. Default: 300. |
<EXTRA>... | Extra arguments passed through to docker compose up. |
Registry options: --registry-url <URL>, --offline, --cache-dir <DIR> (see chap-devops).
chap-devops down
Stop Chap (docker compose down).
Usage: chap-devops down [OPTIONS] [EXTRA]...
| Argument | Description |
|---|---|
--volumes | Also remove this deployment’s volumes, destroying its data. |
-y, --yes | Skip the confirmation. |
<EXTRA>... | Extra arguments passed through to docker compose down. |
chap-devops restart
Recreate the services whose image or configuration changed.
Usage: chap-devops restart [OPTIONS] [SERVICE]...
| Argument | Description |
|---|---|
--all | Recreate the named services even when nothing changed. |
<SERVICE>... | Services to restart; the whole deployment when omitted. |
chap-devops status
Show chap-core health and which models registered.
Usage: chap-devops status [OPTIONS]
| Argument | Description |
|---|---|
--url <URL> | Base URL of the chap-core API. |
--timeout <SECONDS> | Request timeout in seconds. Default: 5. |
chap-devops logs
Show container logs (docker compose logs).
Usage: chap-devops logs [OPTIONS] [SERVICE]...
| Argument | Description |
|---|---|
-f, --follow | Show new output when it arrives. |
--tail <N> | Show only the last N lines of each service’s log. |
<SERVICE>... | Services to show logs for; all of them when omitted. |
chap-devops open
Open a component’s web interface in a browser.
Usage: chap-devops open [OPTIONS] [NAME]
| Argument | Description |
|---|---|
<NAME> | Component to open (chap-core, ocs, dhis2), or an enabled model’s id. |
--no-browser | Print the address instead of opening a browser. |
chap-devops ui
Open the browser: marketplace models and components.
Usage: chap-devops ui [OPTIONS]
Registry options: --registry-url <URL>, --offline, --cache-dir <DIR> (see chap-devops).
chap-devops run
Start one model and print where it answers.
Usage: chap-devops run [OPTIONS] <MODEL>
| Argument | Description |
|---|---|
<MODEL> | Marketplace id, GitHub repository URL, image reference, or local image:tag. |
--port <PORT|auto> | Host port to publish the model on, or auto for a free one. Default: auto. |
--bind <ADDR> | Host address to publish the port on, e.g. 0.0.0.0 for the network. |
--id <ID|auto> | Identifier to record an added model under, or auto for a free one. |
--group <NAME> | Group to start it in, outside a deployment; ps and stop take it too. |
--allow-starter | Start a marketplace starter, which is not a deployable model. |
--no-wait | Return once the container started, without waiting for it to answer. |
-a, --attach, --foreground | Run in the foreground and stream its log (Ctrl-C stops the model). |
--rm | With –attach, Ctrl-C also removes the model’s data volume. |
--chap-core <URL> | A chap-core elsewhere for the group’s models to register with. |
--models-host <HOST> | Host that chap-core calls the models back at; detected if omitted. |
--timeout <SECONDS> | How long to wait for the model to answer, in seconds. Default: 300. |
Registry options: --registry-url <URL>, --offline, --cache-dir <DIR> (see chap-devops).
chap-devops ps
List the models that run, and where each one answers.
Usage: chap-devops ps [OPTIONS]
| Argument | Description |
|---|---|
--group <NAME> | List this group only; every group when omitted. |
chap-devops stop
Stop a model and remove its compose overlay; its data stays.
Usage: chap-devops stop [OPTIONS] [ID]
| Argument | Description |
|---|---|
<ID> | Id or service id of the model. |
--group <NAME> | Group to stop in; every model in it when no id is given. |
--all | Stop every model in the group, or in every group. |
--purge | Delete the data volumes too, and a group left empty with them. |
Registry options: --registry-url <URL>, --offline, --cache-dir <DIR> (see chap-devops).
chap-devops jobs
List the backtests and predictions chap-core has run.
Usage: chap-devops jobs [OPTIONS]
chap-devops jobs <COMMAND>
| Argument | Description |
|---|---|
--status <STATUS> | Only jobs in this status; repeat for more than one. |
--type <TYPE> | Only jobs of this type, such as create_backtest. |
--limit <N> | Show at most this many jobs. |
Subcommands: chap-devops jobs list, chap-devops jobs show, chap-devops jobs logs, chap-devops jobs cancel, chap-devops jobs delete
chap-devops jobs list
List the jobs chap-core knows about, newest first.
Usage: chap-devops jobs list [OPTIONS]
| Argument | Description |
|---|---|
--status <STATUS> | Only jobs in this status; repeat for more than one. |
--type <TYPE> | Only jobs of this type, such as create_backtest. |
--limit <N> | Show at most this many jobs. |
chap-devops jobs show
Show everything chap-core records about one job.
Usage: chap-devops jobs show [OPTIONS] <ID>
| Argument | Description |
|---|---|
<ID> | Job id, or enough of its start to name one job. |
chap-devops jobs logs
Print one job’s log, which is where a failure says why.
Usage: chap-devops jobs logs [OPTIONS] <ID>
| Argument | Description |
|---|---|
<ID> | Job id, or enough of its start to name one job. |
--tail <N> | Print only the last N lines of the log. |
chap-devops jobs cancel
Ask chap-core to stop a job that is still running.
Usage: chap-devops jobs cancel [OPTIONS] <ID>
| Argument | Description |
|---|---|
<ID> | Job id, or enough of its start to name one job. |
chap-devops jobs delete
Remove a finished job from chap-core’s list.
Usage: chap-devops jobs delete [OPTIONS] <ID>
| Argument | Description |
|---|---|
<ID> | Job id, or enough of its start to name one job. |
chap-devops api
Send one authenticated request to chap-core’s API.
Usage: chap-devops api [OPTIONS] <METHOD> <PATH>
| Argument | Description |
|---|---|
<METHOD> | GET, POST, PUT, PATCH or DELETE; case does not matter. |
<PATH> | Request path starting with /, query string and all. |
--data <JSON|@FILE|-> | JSON body: inline, @file, or - to read stdin. |
--url <URL> | Base URL of the chap-core API. |
--raw | Print the body exactly as it arrived. |
--timeout <SECONDS> | Request timeout in seconds. Default: 30. |
chap-devops chap
Run the chap CLI in a container, with no Python or uv installed.
Usage: chap-devops chap [OPTIONS] [CHAP_ARGS]...
| Argument | Description |
|---|---|
--tag <TAG> | chap-core image tag; the deployment’s tag or the newest release if omitted. |
--image <IMAGE> | Image to run in; chosen from –model-name when omitted. Values: core, worker. |
--group <NAME> | chap-devops run group whose network the container joins. |
--docker | Give the container the docker socket, for docker_env models. |
--stop | Stop the model this run started, once chap is done. |
--timeout <SECONDS> | How long a model this run starts may take to answer, in seconds. Default: 300. |
<CHAP_ARGS>... | Arguments for chap, such as eval --model-name URL .... |
Registry options: --registry-url <URL>, --offline, --cache-dir <DIR> (see chap-devops).
chap-devops chapkit
Run the chapkit CLI in the chap-core image, with no Python installed.
Usage: chap-devops chapkit [OPTIONS] [CHAPKIT_ARGS]...
| Argument | Description |
|---|---|
--tag <TAG> | chap-core image tag; the deployment’s tag or the newest release if omitted. |
--image <IMAGE> | Image for mlproject run, a short name or a name; alone, lists them. |
<CHAPKIT_ARGS>... | Arguments for chapkit, such as mlproject migrate DIR. |
chap-devops backup
Create or restore a backup of this deployment.
Usage: chap-devops backup [OPTIONS] <COMMAND>
Subcommands: chap-devops backup create, chap-devops backup restore
chap-devops backup create
Write a tar.gz of the database, the data and the files.
Usage: chap-devops backup create [OPTIONS]
| Argument | Description |
|---|---|
--out <PATH> | Where to write the archive: a file, or a directory. |
--no-db | Do not put the chap-core database in the archive. |
--no-models | Do not put the model data volumes in the archive. |
--no-components | Do not put the component data volumes in the archive. |
chap-devops backup restore
Put a deployment back from a backup archive.
Usage: chap-devops backup restore [OPTIONS] <ARCHIVE>
| Argument | Description |
|---|---|
<ARCHIVE> | The tar.gz written by chap-devops backup create. |
--yes | Skip the confirmation. |
--files-only | Restore only the deployment files; no Docker needed. |
--db-only | Restore only the chap-core database. |
--no-models | Leave the model data volumes as they are. |
--no-components | Leave the component data volumes as they are. |
--adopt-identity | Use the compose project name from the archive. |
--no-start | Do not start the deployment at the end. |
Registry options: --registry-url <URL>, --offline, --cache-dir <DIR> (see chap-devops).
chap-devops doctor
Run a checklist over this machine and this deployment.
Usage: chap-devops doctor [OPTIONS]
Registry options: --registry-url <URL>, --offline, --cache-dir <DIR> (see chap-devops).
chap-devops docker
Use Docker directly: containers, images, compose.
Usage: chap-devops docker [OPTIONS] <COMMAND>
Subcommands: chap-devops docker ps, chap-devops docker pull, chap-devops docker exec, chap-devops docker run, chap-devops docker config
chap-devops docker ps
List the containers this deployment is running.
Usage: chap-devops docker ps [OPTIONS] [EXTRA]...
| Argument | Description |
|---|---|
<EXTRA>... | Extra arguments passed through to docker compose ps. |
chap-devops docker pull
Download the pinned images into the local Docker daemon.
Usage: chap-devops docker pull [OPTIONS]
chap-devops docker exec
Run a command inside one of the running containers.
Usage: chap-devops docker exec [OPTIONS] <SERVICE> [CMD]...
| Argument | Description |
|---|---|
<SERVICE> | Service to run the command in. |
<CMD>... | Command to run; a shell (sh) when omitted. |
chap-devops docker run
Run any docker compose command against this deployment.
Usage: chap-devops docker run [OPTIONS] [ARGS]...
| Argument | Description |
|---|---|
<ARGS>... | Arguments passed to docker compose after the -f list. |
chap-devops docker config
Print every compose file merged into one document.
Usage: chap-devops docker config [OPTIONS] [EXTRA]...
| Argument | Description |
|---|---|
<EXTRA>... | Extra arguments passed through to docker compose config. |
chap-devops top
Watch every chap-devops deployment on this machine as a live tree.
Usage: chap-devops top [OPTIONS]
| Argument | Description |
|---|---|
--interval <SECONDS> | Seconds between two looks at docker. Default: 2. |
chap-devops cleanup
Delete what deployments whose directory is gone left in docker.
Usage: chap-devops cleanup [OPTIONS]
| Argument | Description |
|---|---|
-n, --dry-run | List what would be deleted and change nothing. |
--yes | Delete without asking first. |
chap-devops self
Update chap-devops itself, and report what this build is.
Usage: chap-devops self [OPTIONS] <COMMAND>
Subcommands: chap-devops self update, chap-devops self version
chap-devops self update
Replace this binary with the newest release.
Usage: chap-devops self update [OPTIONS]
| Argument | Description |
|---|---|
--check | Report what an update would do and change nothing. |
--version <TAG> | Install this release tag instead of the newest one. |
-y, --yes | Do not ask before replacing the binary. |
Registry options: --offline, --cache-dir <DIR> (see chap-devops).
chap-devops self version
Show what this build is: version, revision, target and path.
Usage: chap-devops self version [OPTIONS]
chap-devops completions
Print a shell completion script for chap-devops.
Usage: chap-devops completions [OPTIONS] <SHELL>
| Argument | Description |
|---|---|
<SHELL> | Shell to generate for. Values: bash, elvish, fish, powershell, zsh. |
Models and the marketplace
Where the catalogue comes from
The catalogue is the YAML in the model-marketplace repository, read from
https://raw.githubusercontent.com/dhis2-chap/model-marketplace/main/registry.yaml
--registry-url points somewhere else, for a fork or a mirror. chap-devops init --registry-url URL records it as registry_url in .chap-devops/project.yaml, and
every later command in that deployment uses the recorded one; typing the flag
again overrides it for that one run. The
marketplace has no JSON API, so the index is one request and each model file it
lists is one more; the requests are sequential, which is fast enough for a
handful of small files and makes a failure easy to attribute.
Resolution order for every command that needs the catalogue:
- a cached copy younger than 24 hours,
- the network,
- a stale cached copy, when the network fails,
- the snapshot compiled into the binary.
chap-devops registry show prints which of those was used:
url https://raw.githubusercontent.com/dhis2-chap/model-marketplace/main/registry.yaml
source cache (54 minutes old)
models 6
cache /home/me/.cache/chap-devops/registries/https_raw_githubusercontent_com_dhis2_chap_model_marketplace_mai-f6e5270ab795f1bd
chapkit_rwanda_malaria_bym_model
chapkit_simple_multistep_model
auto_arima_chapkit
chapkit_ghr_model
chapkit_minimalist_example_py
chapkit_minimalist_example_r
6 models in the catalogue (cache (54 minutes old))
With -v, a hint names the next command:
hint: `chap-devops models info ID` describes one
chap-devops registry update forces a fetch and refreshes the cache, as does
chap-devops update. Both say what changed against the cached copy: the new
and removed models, and each changed model with its stable version before and
after. If nothing changed, the line says so. --offline never touches the network, so a laptop on a plane
still resolves the catalogue from the cache or the embedded snapshot;
chap-devops update and chap-devops registry update refuse to run that way, because a
refresh needs the network.
The cache lives in $CHAP_DEVOPS_CACHE_DIR, else $XDG_CACHE_HOME/chap-devops, else
~/.cache/chap-devops; --cache-dir overrides it for one invocation. A private
marketplace and the public one never share a cache entry, because the entry is
keyed by the registry URL. Every cache read failure is a warning, never an
error: a missing, truncated or hand-edited cache just means the fetch or the
embedded snapshot answers instead.
The embedded snapshot is the last resort, so chap-devops works on a machine that
has never had network access. It is refreshed with make vendor.
Channels and versions
Each marketplace entry lists versions, and the channels stable and latest
each point at one of them.
chap-devops models enable chapkit_simple_multistep_model # stable, the default
chap-devops models enable chapkit_simple_multistep_model --channel latest
chap-devops models enable chapkit_simple_multistep_model --version 0.1.1 # an exact pin
A model that follows a channel records both the channel and the version it
resolved to, and chap-devops update re-resolves the channel. A model enabled with
--version is listed as pinned by update and never moved. --channel and
--version cannot be combined.
The stable channel takes only a version with status: verified, as
chap-admin install does. If the stable version of an entry is unstable or
deprecated, enabling it on the channel is refused, and the message names the
--version that takes it anyway. A yanked version is refused in every form.
Every version pins one commit of the model’s own repository, and the image
built from it; whether that commit is still the newest build the repository
has published is what chap-devops doctor reports as
one registry pin <id> line per enabled model, because a pin that lags is for
the marketplace to move and not something a deployment can fix.
Starters
Entries with kind: starter are scaffolding for writing your own model, not
forecasting models. They are hidden from chap-devops models list and from the
browser by default, --starters and --all show them, and enabling one needs
--allow-starter.
The marketplace calls them starters, not templates, because a model template
is something else in chap-core: what a model service registers as. The
--template of chap-devops models new is another meaning again: the type of
a new model project, as in A new model project.
Looking at the catalogue
chap-devops models list # models enabled in this deployment are marked
chap-devops models list --enabled # only this deployment's
chap-devops models list --all # starters too
chap-devops models search malaria
chap-devops models info chapkit_simple_multistep_model
ID SERVICE NAME STATUS STABLE LATEST PORT
chapkit_simple_multistep_model chapkit-simple-multistep-model Simple Multistep limited data 0.1.2 0.1.2 via chap-core
auto_arima_chapkit auto-arima-chapkit Auto-ARIMA experimental 1.0.2 1.0.2 -
chapkit_ghr_model chapkit-ghr-model GHRmodel experimental 0.1.3 0.1.3 -
chapkit_rwanda_malaria_bym_model chapkit-rwanda-malaria-bym-model Rwanda Malaria BYM not for use 0.1.3 0.1.3 -
4 listed, 1 enabled in this deployment
Every listing is in the same order: by STATUS, from the models that have
been validated down to the ones nobody should run, and by name inside each
step of that scale. Starters come after models when --all or --starters
asks for them, and a search narrows the catalogue without reordering it.
The PORT column is the host port a model publishes, via chap-core for one
this project enables without a host port of its own - chap-core’s proxy is
then the only way in - or - for one this project does not enable.
What the STATUS column means
STATUS is the model author’s own
chapkit assessment of the model, not
a marketplace verdict and not a measure of whether it runs. It is the one
column to read before enabling anything:
| Status | Colour | What the author is saying |
|---|---|---|
production | green | Validated and ready for production use. |
testing | yellow | Ready for more rigorous testing on diverse data. |
limited data | orange | Shows promise on limited data; needs manual configuration and careful evaluation. |
experimental | red | A highly experimental prototype, not validated, only for early experimentation. |
not for use | gray | Not intended for use: deprecated, or kept for backwards compatibility. |
--json keeps emitting the colour as assessed_status, so a script that
matches on orange goes on working, and adds the words above as
assessed_status_label. chap-devops models info ID prints the colour and the
whole sentence:
status orange, shows promise on limited data, needs manual
configuration and careful evaluation
chap-devops models info ID prints everything the marketplace says about one entry:
its image and runtime, the period types and forecast horizon it supports, the
covariates it requires and defaults to, the channels and the version table,
the maintainers and the attribution, and any named configurations. For a model
this project has enabled with no host port, it also prints the chap-core proxy
URL that reaches it.
Both ID forms work everywhere an id is taken: the marketplace id
(chapkit_simple_multistep_model, with underscores) and the service id
(chapkit-simple-multistep-model, with hyphens, which is also the Compose service name
and the DNS name).
Enabling and disabling
chap-devops models enable ID [ID..] [--channel stable|latest | --version X]
[--port N|auto] [--bind ADDR] [--data-dir PATH]
[--user USER:GROUP] [--allow-starter]
chap-devops models disable ID [--purge] [--retire]
chap-devops models expose ID [--port N|auto] [--bind ADDR]
chap-devops models unexpose ID
chap-devops up # apply any of them
models enable takes several ids, separated by spaces or commas, as
chap-devops init --models does. --channel, --port auto, --bind and
--allow-starter then apply to each model. --version, --port N,
--data-dir and --user are for one model, and enable refuses them with
more than one id.
--bind ADDR sets the host address of the model port; see
Which address a model port is published on.
enable and disable edit .chap-devops/models.yaml and then re-render the compose
files; expose and unexpose only rewrite the overlay’s ports:, so they
never re-resolve the version and are safe on a deployment running a build you
do not want moved. The browser is the same: moving a port there keeps the
version the model is pinned to, and only a toggle or a new channel resolves the
marketplace again.
disable also stops and removes the model’s container when one is running,
before the definition goes away, and says so:
disabled chapkit_simple_multistep_model
stopped and removed 2 containers (chapkit-simple-multistep-model, chapkit-simple-multistep-model-init); host port 5001 is free again
kept volume mychap-1ab2c3_ck_chapkit_simple_multistep_model_data; remove it with `chap-devops models disable chapkit_simple_multistep_model --purge` or `docker volume rm mychap-1ab2c3_ck_chapkit_simple_multistep_model_data`
The port is therefore free straight away, rather than at the next chap-devops up,
which would have removed the container as an orphan but only once it was run.
There is nothing left for chap-devops up to apply. With -v, two hints follow:
the overlay that was removed (removed compose.chapkit-simple-multistep-model.yml) and
`chap-devops status` shows what runs now.
Retiring a model in chap-core
A plain disable does not change chap-core. The model template and its
configured models stay live, so the Modeling App still lists the model. This
keeps a disable easy to undo: enable and up bring the model back as it was.
disable --retire also retires the model in chap-core, which is what
chap-admin uninstall does. chap-core then archives every version of the
template and every configured model of it. The rows stay for the backtests and
predictions that use them.
disabled chapkit_simple_multistep_model
retired chapkit_simple_multistep_model in chap-core; its configured models are archived
If you enable the model again, chap-devops creates the configurations of the
marketplace entry again. A configured model that you made with
chap-devops models configs add stays archived. Use --retire when the model
leaves the deployment for good.
--retire needs chap-core to answer. chap-devops looks up the template before
it changes anything. If chap-core does not answer, the command stops and the
model stays enabled.
The data volume
What disable does not take is the model’s data: the named volume
<compose project>_ck_<id>_data stays exactly as it was, so enabling the model
again finds everything it had. That is why the line above names it - when
docker has it: a model that was never started has no volume, and then there
is no line, because nothing was kept. Nothing
else does any more - the overlay that declared the volume has just been
removed, so chap-devops down --volumes no longer knows about it, and a volume
nobody names again is kept for as long as the machine lasts.
--purge is how the data goes with the model:
chap-devops models disable chapkit_simple_multistep_model --purge
disabled chapkit_simple_multistep_model
stopped and removed 2 containers (chapkit-simple-multistep-model, chapkit-simple-multistep-model-init)
removed volume mychap-1ab2c3_ck_chapkit_simple_multistep_model_data
The volume is removed after the container, which is the only order docker
allows: a volume a container still has mounted cannot be removed. Everything
about it is best-effort and reported rather than fatal - volume <name> not found for a deployment that was never started, and volume <name> could not be removed: <docker's own words> for anything else. --json carries the same
answer as two lists, purged and kept_volumes.
--purge also works on a model that is not enabled any more, which is the
state the kept-volume line leaves you in:
chapkit_simple_multistep_model is not enabled here, so only its data volume was looked for
removed volume mychap-1ab2c3_ck_chapkit_simple_multistep_model_data
chap-devops doctor’s volumes line finds the ones that were forgotten: a volume
under this deployment’s name that belongs to no enabled model and no enabled
component is reported as a leftover, with the same two ways to remove it. See
Doctor.
expose and unexpose say what changed:
exposed chapkit-simple-multistep-model on http://localhost:5001
run `chap-devops up` to apply
unexposed chapkit-simple-multistep-model; it stays registered with chap-core and reachable at http://localhost:8700/v2/services/chapkit-simple-multistep-model/run/
run `chap-devops up` to apply
If the model is already where the command puts it, nothing changes, and there
is no chap-devops up line:
chapkit-simple-multistep-model is already exposed on http://localhost:5001; nothing changed
The port that the running container of the model publishes is not a port in
use for this check. So --port 5001 or --port auto on the port of the model
itself keeps the port, and does not fail or move the model. If the record has
the port and the running container does not publish it yet, a second line
names the step:
chapkit-simple-multistep-model is already exposed on http://localhost:5001; nothing changed
its running container does not publish this port yet; run `chap-devops up` to apply it
chapkit-simple-multistep-model was already unexposed; nothing changed, and it is reachable at http://localhost:8700/v2/services/chapkit-simple-multistep-model/run/
An enable of a model that is already enabled at the same version and port
also changes nothing:
chapkit_simple_multistep_model v0.1.2 is already enabled at http://localhost:8700/v2/services/chapkit-simple-multistep-model/run/; nothing changed
With --json, such a model is in the unchanged list, not in enabled.
Configured models
chap-core does not run a model service directly. It runs a configured model: a model template together with a set of option values and covariates. A backtest, a prediction and the model picker of the Modeling App all name a configured model.
Each configured model has a name, the variant name. The Modeling App shows it
in brackets after the model name: <model name> [<variant name>]. chap-core
stores it after the template: <service id>:<variant name>, and the bare
service id for the variant name default.
chap-devops models configs is the command group for the configured models:
| Command | What it does |
|---|---|
chap-devops models configs [list] [ID] | Lists the configured models that chap-core has. |
chap-devops models configs add ID | Adds a configured model. |
chap-devops models configs update ID NAME | Gives a configured model new values. |
chap-devops models configs archive ID NAME | Archives a configured model. |
chap-devops models configs export [ID] | Writes the configured models in the marketplace format. |
chap-devops models configs sync [ID..] | Creates the configured models of the marketplace entry. |
In each command, ID is the marketplace id or the service id of an enabled
model, and NAME is the variant name. Each command asks chap-core, so
chap-core must run. If chap-core does not answer, the command stops and names
chap-devops status.
Listing
chap-devops models configs # every enabled model
chap-devops models configs list chapkit_simple_multistep_model # one model
chap-devops models configs --all # with the archived ones
chapkit_simple_multistep_model
NAME COVARIATES OPTIONS SOURCE
monthly_climate rainfall,mean_temperature,mean_relative_humidity n_samples=100 n_target_lags=6 rf_max_depth=10... marketplace
monthly_selfhistory - n_samples=100 n_target_lags=6 rf_max_depth=10... marketplace
short_lags rainfall n_target_lags=3 rf_max_depth=5 by hand
3 configured models of 1 model
Each model gets a table. NAME is the variant name. OPTIONS shows the option
values that chap-core stores; an option without a value uses the model
default. A long list of values ends in ...; --json has all of them. SOURCE says where the configured model comes from:
marketplace: a configuration of the marketplace entry.service default: the configurationdefaultof a model without a marketplace entry.by hand: a configured model that you added.models test: a configured model that a test left.
--all adds the archived configured models and the column ARCHIVED. --json
gives every field, with the option values whole. If chap-core has no
configured model of a model, the line names the way out:
auto_arima_chapkit: chap-core has no configured model of it; `chap-devops models configs sync auto_arima_chapkit` creates them
Adding
chap-devops models configs add chapkit_simple_multistep_model --name short_lags \
--set n_target_lags=3 --set rf_max_depth=5 --covariates rainfall
created configured model short_lags of chapkit_simple_multistep_model
--nameis the variant name.--set KEY=VALUEgives the value of one option. Repeat it for more than one. Write a list with commas, or as a JSON list. The booleans aretrueandfalse.nullis a value only where the option allows it.--covariatesgives the additional covariates, separated by commas.
chap-devops checks each key against the options of the template, and each value against the kind of the option, before it asks chap-core. chap-core does not check the values of a chapkit model, and chapkit accepts keys that it does not know. So a mistake would otherwise show only when a run fails. A bad key or value stops the command with exit code 2:
error: `lag` is not an option of chapkit_simple_multistep_model; its options are n_samples (integer), n_target_lags (integer), rf_max_depth (integer), rf_min_samples_leaf (integer)
error: `two` is not a value of `n_target_lags`, which takes an integer, such as `3`
chap-devops also checks the covariates. A required covariate is refused, because every run gets it. A model that does not allow free covariates takes only the defaults of its marketplace entry.
chap-devops refuses a variant name that a configured model has and that is not archived. chap-core would show the new configured model and hide the old one, and give no message.
With no options at a terminal, add opens the form of chap-devops ui: one field
for the variant name, one field for each option with its kind, default and
description, and one field for the covariates. An empty field uses the
default. Esc closes the form and writes nothing:
left the form; nothing was changed
Without a terminal (in a pipe or with --json), add with no options stops
with exit code 2 and names --name and --set.
--from FILE adds every configuration in a file of the marketplace format:
an export, or a complete marketplace entry. chap-devops checks all of
them before it creates the first. chap-core sets prediction_periods for
each run, so chap-devops does not store the value in the file.
Changing
chap-devops models configs update chapkit_simple_multistep_model short_lags --set rf_max_depth=8
chap-devops models configs update chapkit_simple_multistep_model short_lags --unset rf_max_depth
chap-devops models configs update chapkit_simple_multistep_model short_lags # the form
updated configured model short_lags of chapkit_simple_multistep_model; chap-core keeps the old values as an archived configured model
chap-core has no request that changes a configured model. chap-devops creates a
configured model with the same variant name and the new values, then archives
the old one. chap-core keeps the old row for the backtests and predictions
that use it, but it does not list it again, so list --all does not show the
old values.
--setgives a new value. The other values stay.--unset KEYremoves a value, so the model default applies.--covariatesreplaces the old list.
With no options at a terminal, update opens the form with the current
values. If the new values are the same as the current values, chap-devops writes
nothing:
configured model short_lags of chapkit_simple_multistep_model has these values already; nothing changed
If the archive step fails, the new values are live, and a warning gives the
chap-devops api DELETE command that archives the old row.
Archiving
chap-devops models configs archive chapkit_simple_multistep_model short_lags
archived configured model short_lags of chapkit_simple_multistep_model
chap-core does not delete a configured model. It archives it: chap-core
keeps the row for the backtests and predictions that use it, list --all
shows it, and the Modeling App shows it as Archived. If you add the same
variant name with the same values again, chap-core makes the archived row
live again.
You can also archive a configuration of the marketplace entry. chap-devops models configs sync creates the marketplace configurations of a model only when
chap-core has no live configured model of the version the model runs. While
another one is live, sync does not change the model. When you archive the
last one, chap-devops says so:
chapkit_simple_multistep_model has no other configured model now, so `chap-devops models configs sync` and `chap-devops up --wait` create its configured models again
Exporting
chap-devops models configs export chapkit_simple_multistep_model > configurations.yaml
chap-devops models configs export chapkit_simple_multistep_model --out configurations.yaml
The output is the configurations: block of a marketplace entry. For each
configured model it has the variant name, a description and a config.
config is the flat object that chapkit takes: the option values,
additional_continuous_covariates and prediction_periods.
This is the configured model short_lags of the export, after the
update above:
configurations:
short_lags:
description: The configured model short_lags of chapkit_simple_multistep_model.
config:
additional_continuous_covariates:
- rainfall
n_target_lags: 3
prediction_periods: 12
rf_max_depth: 8
chap-core does not store prediction_periods. chap-devops takes it from the
marketplace configuration with the same name. If there is none, chap-devops takes
the default from the config schema of the service, and else the chapkit
default of 3. -v says which source each value comes from. The keys of
config are in alphabetical order.
An export is for one model, because the block goes under one marketplace
entry. If the deployment runs more than one model, give the ID. With --out,
chap-devops reports the file:
wrote 3 configurations of chapkit_simple_multistep_model to configurations.yaml
To use the configurations in another deployment, run chap-devops models configs add ID --from configurations.yaml there. To publish them, put the block in
the marketplace entry of the model.
Sync
Up to chap-core 2.3, a registration made a configured model. chap-core 2.4 and later makes none. A model that chap-devops starts registers, and without a configured model nothing can run it.
chap-devops models configs sync does what chap-admin install does after a model
service has registered:
- It stores the model template from the service
(
POST /v1/crud/model-templates/from-service). - It creates one configured model for each entry under
configurationsin the marketplace entry of the model. The option values come from theconfigof that entry. The covariates come from the config, or fromcovariates.defaultswhen the config has none.prediction_periodsis removed, because chap-core sets it for each run. - A marketplace entry without configurations gets one configured model,
default. A model added withchap-devops models add, or a model that runs an image other than its marketplace entry, also gets onedefaultwith the defaults of the service.
chap-devops models configs sync # every model this deployment enables
chap-devops models configs sync chapkit_simple_multistep_model # one model, by marketplace id or service id
auto_arima_chapkit: created configured model monthly
chapkit_simple_multistep_model: chap-core has a configured model of it already
The command is idempotent for each registered version. A model that has a
configured model of the version it registered with is left alone, so a second
run adds nothing. An older chap-core that still makes configured models itself
gets no second set. A model that chap-core has not registered yet gets a line
that says so. Directly after chap-devops up, expect this line for a few seconds:
chapkit_simple_multistep_model: not registered with chap-core; run `chap-devops models configs sync` again once `chap-devops status` shows it registered
A model that could not be configured gets a warning with the reason, and the command exits non-zero.
If a configured model fails after the template was stored, chap-devops undoes
the step, as chap-admin install undoes a failed registration. It retires the
template version that it stored, and with it the configured models that the
step made. Only that version is retired. If an earlier version can run,
chap-core makes it live again, so a failed update leaves the model as it was.
The next run starts again from the template. Without the undo, one configured
model of three counts as configured, and no later run adds the other two.
warning: chapkit_ewars_model: could not create its configured models: ... (after monthly_climate); the template version it stored is retired again, with the configured models made so far; run `chap-devops models configs sync chapkit_ewars_model` again, and `chap-devops logs chap` says why
In --json, the model has the state failed and an undo object: retired,
or not-retired with the error.
You do not usually run the command yourself. chap-devops up --wait, chap-devops dhis2 connect and chap-devops models test --backtest do the same step, and say which
configured models they created:
created configured models in chap-core for chapkit_simple_multistep_model (monthly_climate, monthly_selfhistory)
chap-devops status shows a model without a configured model as registered, not configured, and adds a line with the way out:
auto-arima-chapkit: chap-core has no configured model for it, so nothing can run it; run `chap-devops models configs sync`
Testing a configuration
chap-devops models test --backtest runs a backtest of one configured model of each
model. To test a configured model that you added, give its variant name with
--config:
chap-devops models configs add chapkit_simple_multistep_model --name short_lags \
--set n_target_lags=3 --set rf_max_depth=5 --covariates rainfall
chap-devops models test chapkit_simple_multistep_model --backtest --config short_lags
testing 1 model (through chap-core: a dataset, a backtest and its scores)
chapkit-simple-multistep-model pass 13s crps 6.4 mae 8.7 rmse 10.3
configured model: short_lags (id 17)
1 of 1 model passes
Without --config, the backtest takes the configured model default, or else
the lowest-numbered one. See The backtest level. The
line under the row always names the configured model that it used.
--config counts only a configured model of the version that the model
registered with, and not an archived one. If the model has no configured model
of that name, the test skips it and names the ones that it has:
chapkit-simple-multistep-model skip 0s chap-core has no configured model weekly for chapkit-simple-multistep-model 0.1.2; it has monthly_climate, monthly_selfhistory, short_lags
run `chap-devops models configs list chapkit_simple_multistep_model` to see its configured models
With --all or more than one id, --config applies to each model. A model
that has a configured model of that name is tested with it. Each other model
is a skip with the names that it has. --config without --backtest stops
with exit code 2.
Testing a model
chap-devops status and chap-devops doctor can both be entirely green while a model
cannot produce a single prediction. Registration is a heartbeat: it says the
service is alive and talking to chap-core, not that its runtime works, that the
account it runs as can write, or that the covariates it declares are the ones
it reads. chap-devops models test is the check neither of them can make, because it
is the only one that makes the model do the work.
chap-devops models test ID.. # one or more, by marketplace id or service id
chap-devops models test --all # every model this deployment enables
chap-devops models test --all --backtest # the whole way round, through chap-core
chap-devops models test SERVICE_ID --backtest # a model registered from outside, such as one run from its checkout
The model level needs no chap-core, so it also tests a model in a deployment
without one; --backtest needs one.
Models are tested one after another. A model that could not be tested at all is a skip, and a skip is counted rather than treated as a bad answer. The exit code is non-zero when one of the models failed, or when every model was skipped: a run that tested no model proved nothing, and it says so in a warning.
The model level
The default. For each model, chap-devops runs chapkit’s own end-to-end test inside
that model’s container:
docker compose exec -T <service> chapkit test --url http://127.0.0.1:8000 --timeout 300
chapkit reads the service’s own configuration schema, creates a config from it, generates data matching the covariates, period type and geometry the service declares, and then validates, trains and predicts against it.
testing 4 models (model level; add --backtest to go through chap-core)
auto-arima-chapkit pass 12s 1 training, 1 prediction
chapkit-ghr-model pass 24s 1 training, 1 prediction
chapkit-rwanda-malaria-bym-model pass 20s 1 training, 1 prediction
chapkit-simple-multistep-model pass 5s 1 training, 1 prediction
4 of 4 models pass
What it proves: the image runs, the service answers, the account the container runs as can write to its data directory and its workspace, the runtime has the libraries the model needs, and the model can turn a config and a frame into a prediction. What it does not touch: chap-core. This is therefore the level to reach for when chap-core is the thing that is broken, and the level that answers in seconds rather than minutes.
A failure names the phase and the first sentence of the model’s own error, out of the stderr chapkit quotes back:
chapkit-rwanda-malaria-bym-model FAIL 14s predict: Error in file(file, "rb") : cannot open file 'model.rds': Permission denied
run `chap-devops models test chapkit_rwanda_malaria_bym_model -vv` for the full output, and `chap-devops logs chapkit-rwanda-malaria-bym-model` for the service's own
-vv prints the whole output of chapkit test. -v shows only the hints.
More data for a test
By default, chapkit test creates 1 config, trains once and predicts once.
The training data has 250 rows: 50 periods for 5 locations, which is about 4
years of monthly data, or 1 year of weekly data. Some models need more:
- A weekly model often needs more than 1 year of training data.
- A model with a long context window, for example many lags, needs a long history for each prediction.
For these models, give more rows. chap-devops gives the options to chapkit test:
chap-devops models test chapkit_simple_multistep_model --rows 600 --predict-rows 300
chap-devops models test chapkit_simple_multistep_model --configs 2 --trainings 2
With --configs 2 --trainings 2, each config trains twice, and each trained
model predicts once:
testing 1 model (model level; add --backtest to go through chap-core)
chapkit-simple-multistep-model pass 18s 4 trainings, 4 predictions
1 of 1 model passes
These options are for the model level only; --backtest refuses them.
A model that could not be tested is skipped. The line under the row gives the way out:
testing 1 model (model level; add --backtest to go through chap-core)
auto-arima-chapkit skip 0s its container is not running
run `chap-devops up`
0 pass, 1 skipped
warning: no model was tested, because every one was skipped; the line under each row says why
| Skip | What to do |
|---|---|
its container is not running | chap-devops up. |
the image has no `chapkit test`; the service reports chapkit 1.0.0 | The image predates the command. chap-devops update moves the pin; --backtest tests it through chap-core instead. |
an MLproject service has no `chapkit test` | The image serves an MLproject file with chapkit mlproject run, and chapkit has chapkit test only in a chapkit service project. Use --backtest, or make the model a chapkit service with chap-devops chapkit mlproject migrate. |
chap-core has no configured model for <service> <version> | --backtest only. Run chap-devops models configs sync ID, then the test again. |
chap-core has no configured model NAME for <service> <version>; it has ... | --config only. Use one of the names, or add the configured model with chap-devops models configs add ID. |
chap-core did not list its configured models, so chap-devops cannot find --config NAME | --config only. Run chap-devops status. |
no answer in 5m | The model is wedged or genuinely slow. The line under the row names --timeout with twice the limit. The same number is handed to chapkit as its per-job deadline. |
The backtest level
--backtest goes the whole way round, the path the Modeling App takes. For
each model, chap-devops fetches sample data from the model itself through
chap-core’s proxy (GET .../api/v1/ml/$generate-sample-data?kind=train,
36 periods over 5 org units, in the period type the service declares),
transposes that frame into chap-core observations, posts it as a dataset,
waits for the dataset job, runs a small rolling backtest over it (3 periods, 2
splits, stride 1) and reads the scores off the result.
Before the first model, --backtest creates the configured models that the
models of this deployment do not have yet, as chap-devops models configs sync does. A model registered from outside the
deployment is not configured by this step.
The backtest names one of chap-core’s configured models by its id, chosen from
GET /v1/crud/configured-models. Only a configured model of the version that
the service registered with counts, and only one that chap-core has not
archived. A service that registers with a new version keeps the configured
models of the old version, but chap-core does not run them. Without
--config, the backtest takes the first of these that exists:
- The configured model named after the service: the variant name
default. - The lowest-numbered
<service>:<variant>. - A
<service>:test_config_...that an earlier test left behind.
--config NAME names the configured model to backtest. NAME is the variant
name, as chap-devops models configs shows it, or the full name <service>:<variant>.
See Testing a configuration.
The backtest first creates the configured models that the marketplace entry
gives, as chap-devops models configs sync does. A service that still has no live
configured model of its version is skipped, not backtested. The skip names
the version, and the line under it names chap-devops models configs sync. It has
this form:
SERVICE skip 0s chap-core has no configured model for SERVICE VERSION
it is registered, and chap-core has nothing to run it with; run `chap-devops models configs sync ID`, then `chap-devops models test ID --backtest`
That configured model’s additionalContinuousCovariates are the columns the
backtest hands the model, and the sample data does not always carry them:
$generate-sample-data makes the service’s required covariates and a pair of
climate ones, but not the ones a configuration adds - the simple multistep
model’s defaults include mean_relative_humidity, and without it the backtest
fails inside the model with KeyError: "['mean_relative_humidity'] not in index". So at least as many feature_N columns are asked for as the
configuration names, and each covariate the frame lacks takes over a spare one:
the same kind of synthetic seasonal series under the name the model reads.
-vv says which column stood in for which.
Geometry is asked for whatever the service declares (include_geo=true).
chap-core’s dataset always carries a GeoJSON collection, so a model that says
it needs no geometry would otherwise be handed features with none in them - and
a model that builds a neighbour graph fails on an empty polygon where it would
have been perfectly happy with no geometry at all. Real polygons are never
worse: a model that ignores geometry ignores these too. chapkit puts each
location’s id in properties.id and nothing at the top level, and chap-core
matches org units on the feature’s top-level id and silently drops the ones
it cannot match, so chap-devops sets it before posting.
testing 2 models (through chap-core: a dataset, a backtest and its scores)
chapkit-rwanda-malaria-bym-model pass 38s crps 16.7 mae 26.2 rmse 30.6
configured model: monthly (id 20)
chapkit-simple-multistep-model pass 13s crps 5.4 mae 7.3 rmse 9.0
configured model: monthly_climate (id 14)
2 of 2 models pass
The line under each row names the configured model that the backtest used:
its variant name and its id in chap-core. So the scores are always of a
configured model that you can see. --json has the same as configured_model,
with id, name and variant. If chap-core did not list its configured
models, the backtest sends the service id, and the line says (by name, as chap-core could not list them). id is then null.
What it proves: everything the model level does, plus that chap-core can
reach the model, that the covariates and period type the model declares are the
ones chap-core builds a dataset from, that the org units survive the round trip,
and that a real forecast comes back with scores on it. The three printed are
chap-core’s crps, mae and rmse; --json hands back all fourteen.
A job that fails prints chap-core’s own reason - the same line
chap-devops jobs logs digs out of the --- stderr --- section - and
the command that shows the whole log:
chapkit-ghr-model FAIL 1m 12s Error in predict_chap() : the inla program crashed
run `chap-devops jobs logs 9a84e02f-6a0e-4a3b-9a63-1f2b5a1f3f21`
Scores are not a verdict on the model: a two-split backtest over generated data says the pipeline works, not that the model is any good.
What it leaves behind, and what it cleans up
Both levels put something in a database, and both delete it again unless
--keep says not to.
| Level | What it creates | What is deleted |
|---|---|---|
| model | one config named test_config_<ulid> and a few artifacts, in the model service’s own database | The service’s configs and artifacts are listed before the run and again after it, and exactly what appeared is deleted. Deleting a config cascades to the artifacts linked to it. |
--backtest | one dataset and one backtest row in chap-core’s database, plus the jobs that made them. From chap-core v2.5.0, also one backtest for each built-in baseline | The backtests first, then the dataset, and nothing else. The jobs stay, because that is the record of what was run; they show up in chap-devops jobs. |
chap-core’s model proxy is read-only by design, so a delete cannot go through
it: the model level lists through the proxy and deletes with the curl every
chapkit image carries for its healthcheck (docker compose exec -T <service> curl -X DELETE ...). If a delete does not work, the command says so and names
what was left, so it can be removed by hand.
--keep keeps it, and says what was kept. At the model level the line names
the config; it goes away with the volume of the model
(chap-devops models disable ID --purge):
chapkit-simple-multistep-model: kept test_config_01M4CVD3GTBZGX2GGQS7131PXQ and 3 artifacts in its database; remove it with `chap-devops docker exec chapkit-simple-multistep-model curl -fsS -X DELETE http://127.0.0.1:8000/api/v1/configs/01M4CVD3GTBZGX2GGQS7131PXQ`
With --backtest, the line names the two deletes:
kept backtest 8 and dataset 7; remove them with `chap-devops api DELETE /v1/crud/backtests/8` then `chap-devops api DELETE /v1/crud/datasets/7`
From chap-core v2.5.0, a backtest also starts one run_baseline job for each
built-in baseline, and each job writes a backtest on the same dataset.
chap-core leaves these out of the backtest lists, but it does not delete a
dataset while they point at it. So chap-devops waits up to a minute for the
baselines of the run, and deletes their backtests with the backtest of the
model, in one request. The line then names them all:
kept backtests 25, 26, 27 and 28 and dataset 7; remove them with `chap-devops api DELETE '/v1/crud/backtests?ids=25,26,27,28'` then `chap-devops api DELETE /v1/crud/datasets/7`
Both lines are closing lines of the command. They go to stdout, and --json
carries them in messages.
A backtest chap-core ran also leaves a configuration in the model service’s own database, the way any backtest started from the Modeling App does. That one is chap-core’s, not the test’s, so it is left alone.
What each level needs
| Level | Requirement |
|---|---|
| model | The model’s container running, and an image with the chapkit CLI in it. Every chapkit-built image has one; the marketplace images ship chapkit 2.0 or 2.1. chap-core need not be up at all - it is only asked for the model’s declared period type, and for which chapkit the service reports when there is none to run. |
--backtest | chap-core up, the model registered with it, and chapkit 1.1.0 or newer in the image, which is where $generate-sample-data arrived. An older one is skipped with the version it reports. |
Flags
| Flag | What it does |
|---|---|
--all | Test every model in .chap-devops/models.yaml. Cannot be combined with an id. |
--backtest | Run the backtest level instead of the model level. |
--config NAME | Backtest the configured model with this variant name. Needs --backtest. With more than one model, it applies to each model. |
--seed N | Seed the generated data, so two runs compare. Without it every run is fresh data. |
--timeout SECONDS | How long one model gets: 300 at the model level, 900 with --backtest. At the model level the same number is chapkit’s per-job deadline. |
--configs N, --trainings N, --predictions N | The model level only. How many configs chapkit test creates, how many trainings each config gets, and how many predictions each trained model gets. The default of each is 1. |
--rows N, --predict-rows N | The model level only. The rows (locations times periods) of the training data, 250 by default, and of the history and the future data of each prediction, 150 by default. See More data for a test. |
--keep | Do not delete what the run created. |
-vv | Stream chapkit test’s whole output as it runs, and narrate every request. This is what to add to a failure. |
--json | One object per model: id, service_id, level, result, seconds, summary, detail, and - for a backtest - job_id, backtest_id, configured_model and the whole metrics object. |
A new model project
chap-devops models new DIR creates a model project in a new directory. The project
builds into an image that chap-devops models add and chap-devops run take. Python, uv
and the chapkit CLI are not necessary on the machine: the templates are in
chap-devops, and the build runs in Docker.
chap-devops models new dengue-lags
chap-devops models new rain-model --template mlproject-r
created dengue-lags in dengue-lags (fn-py template, service id dengue-lags)
next: build the image as `dengue-lags/README.md` says, then run `chap-devops run dengue-lags:dev`
With -v, two hints follow: the files it wrote, and the base image.
The types
There are two kinds of project. A chapkit service has a main.py that builds
the service with chapkit. An MLproject has an MLproject file and scripts, and
no chapkit code: the image serves it with chapkit mlproject run.
--template | Kind | The model is in | Base image |
|---|---|---|---|
fn-py (the default) | chapkit service | Python, in main.py | chapkit-py |
shell-py | chapkit service | Python scripts | chapkit-py |
shell-r | chapkit service | R scripts | chapkit-r |
shell-r-tidyverse | chapkit service | R scripts, with tidyverse and fable | chapkit-r-tidyverse |
shell-r-inla | chapkit service | R scripts, with INLA | chapkit-r-inla |
mlproject-py | MLproject | Python scripts | chapkit-py-data-cli |
mlproject-r | MLproject | R scripts | chapkit-r-cli |
mlproject-r-tidyverse | MLproject | R scripts, with tidyverse and fable | chapkit-r-tidyverse-cli |
mlproject-r-inla | MLproject | R scripts, with INLA | chapkit-r-inla-cli |
The base images are ghcr.io/dhis2-chap/<name>:latest. A -cli image has
chapkit in it, which chapkit mlproject run needs. chapkit-py-data-cli
also has numpy, pandas, scipy, scikit-learn, statsmodels, joblib, pyyaml and
xarray, so a Python MLproject runs with no install of its own. A chapkit service installs
its own chapkit with uv sync in the build, from the range in its
pyproject.toml (chapkit>=2.4.1,<3). The INLA images are amd64 only, so
those Dockerfiles pin linux/amd64.
--with-validation adds the $validate hooks (on_validate_train and
on_validate_predict) to main.py. An MLproject has no main.py, so
models new refuses the option for the mlproject-* types.
What the project holds
| File | Types | What it holds |
|---|---|---|
main.py | chapkit service | The options (the Config class), the service id and the metadata; for fn-py, also the model |
MLproject | MLproject | The name, the options (user_options), and the train and predict commands |
scripts/train.py, scripts/predict.py | shell-py, mlproject-py | The model |
scripts/train.R, scripts/predict.R | the R types | The model |
pyproject.toml, .python-version | all but the R MLproject types | The Python dependencies |
Dockerfile | all | The image |
README.md | all | The build, run and test commands for this project |
.github/workflows/publish.yml | all | Publishes the image to GHCR on a push to main and on a v*.*.* tag |
.gitignore, .dockerignore | all |
Every type has the same example model: for each location, it predicts the
mean of disease_cases in the last window periods. window is an option,
with 3 as the default. With the same --seed, every type gives the same
backtest scores, so a change of type does not change the result.
The names come from the directory name:
- The service id is the name in lowercase, with
-between the words, such asdengue-lags. The model registers with chap-core under this id. It is theidinmain.py, or thenameinMLproject. The README uses it as the image name too, sochap-devops models addneeds no--service-id. - The model id is the same with
_, such asdengue_lags. This is the id thatchap-devops models addgives the image.
A name can have letters, digits, spaces, -, _ and .. A name that starts
with a digit gets model- in front of its service id.
The author in the model metadata comes from git: user.name is the
author, and user.email is the contact_email, in main.py or in
MLproject. If git has no value, the project gets a placeholder (Your Name,
your.email@example.com) for you to edit. With -v, models new says which
of the two it used.
From the project to a backtest
In the project:
cd dengue-lags
git init && git add . && git commit -m "feat: first version"
docker build --platform linux/amd64 --build-arg GIT_REVISION=$(git rev-parse HEAD) -t dengue-lags:dev .
chap-core stores a model only if the image has a git revision, so the build
needs a commit and GIT_REVISION. Without uv.lock, the build resolves the
Python dependencies itself. Run uv lock and commit the file to make every
build get the same versions.
Then, in a deployment with chap-core:
chap-devops models add dengue-lags:dev
chap-devops up --wait
chap-devops models test dengue_lags --backtest
chap-devops models configs add dengue_lags --name long --set window=6
chap-devops models test dengue_lags --backtest --config long
A model-level test (chap-devops models test without --backtest) runs
chapkit test in the container. An MLproject image does not have it, and the
row says skip with an MLproject service has no `chapkit test` . To make an
MLproject a chapkit service, use chap-devops chapkit mlproject migrate DIR; see
The chapkit CLI.
After a change, raise the version (version in main.py, or in MLproject)
before you build again. chap-core stores each version of a model only once,
and refuses a new git revision under a version it has. Then build, run
chap-devops restart, and test again. See
is stored from revision ..., but its source now reports revision ....
To run the model service alone, with no deployment: chap-devops run dengue-lags:dev.
Models outside the marketplace
A model the catalogue does not list - a new one, a private one, a fork of your
own - is added to one deployment with chap-devops models add:
chap-devops models add https://github.com/my-org/chapkit_dengue_model
chap-devops models add ghcr.io/my-org/chapkit_dengue_model:sha-1eb8cf1
chap-devops models add ghcr.io/my-org/chapkit_dengue_model@sha256:31163f6a...
chap-devops models add quay.io/my-org/dengue_model:v1.2 # any other registry
chap-devops models add docker.io/my-org/dengue_model:v1.2 # Docker Hub, with its host
chap-devops models add my-dengue-model:dev # built on this machine, never pulled
An image reference can name any registry: ghcr.io, Docker Hub, quay.io, or a
registry of your own. Write Docker Hub images with docker.io/ in front. A name
with no registry host is a local image (see below). The repository URL form
uses ghcr, because that is where the publish workflow of the model
repositories writes its builds.
A reference with no registry host is an image in the local image store, such as
one just built with docker build --platform linux/amd64 -t my-dengue-model:dev ..
It is read from the local store rather than a registry, rendered with
pull_policy: never, and may share a marketplace model’s service id while that
model is disabled. See
A model image you built yourself.
The two forms differ in one thing, and it is the important one:
- A repository URL follows the default branch.
chap-devops models addasks GitHub for that branch and its newest commits, and ghcr for the newest of those commits that has a publishedsha-<short commit>build - which is the tag the model repositories’ publish workflow writes.chap-devops updatemoves the pin to whatever the newest published build is then. - An image reference pins exactly what it names, tag or
@sha256:digest, andchap-devops updatereports it as pinned and leaves it alone.
Either way the entry is a full definition, so everything else treats it like a
marketplace model: it is enabled by the same write path, rendered into the same
kind of overlay, listed by models list, described by models info, moved (or
not) by chap-devops update, seen by chap-devops doctor and shown in the browser.
$ chap-devops -v models add https://github.com/my-org/chapkit_dengue_model --port 5001
added chapkit_dengue_model (chapkit-dengue-model)
hint: source: https://github.com/my-org/chapkit_dengue_model
hint: image: ghcr.io/my-org/chapkit_dengue_model:sha-b1d6c31
hint: pin: sha-b1d6c31 (commit b1d6c31)
hint: follows: main (`chap-devops update` moves the pin)
hint: data dir: /work/data (from the image config)
hint: user: 10001:10001 (from a docker probe)
enabled chapkit_dengue_model sha-b1d6c31 on http://localhost:5001
hint: chapkit_dengue_model is in `compose.chapkit-dengue-model.yml`
the service must register with chap-core as `chapkit-dengue-model`; if its own MLServiceInfo.id differs, `chap-devops status` shows it as unmanaged - run `chap-devops models remove chapkit_dengue_model`, then add it again with `--service-id <that id>`
run `chap-devops up` to apply
Every line says where its answer came from, because none of them came from a reviewed catalogue entry:
| Value | Where it comes from |
|---|---|
| id | --id, else the repository or image name in snake_case. Not persistence, global_median or seasonal_median: chap-core v2.5.0 and later has models of these names, and refuses a service that uses one. |
| service | --service-id, else the id with hyphens. Not one of the names chap-devops uses itself - chap, worker, redis, postgres, ocs, s3, s3-init, dhis2, dhis2-db, dhis2-prep, chap-devops, marketplace - which a model would be merged into or written over; models add and sync refuse them. |
| name | --name, else the repository or image name. |
| pin | The newest published sha- build of the branch, or the tag or digest that was named. |
| data dir | --data-dir, else <WorkingDir>/data from the image config, else /work/data. A --data-dir is an absolute path of letters, digits, /, ., _ and -: it goes into the rendered YAML and the init container’s chown as it is. |
| user | --user (a name or an id, with a group after a :, such as 1000:1000), else the image’s own User when it is numeric or an account chap-devops knows, else the numbers a docker run ... id -u probe reads out of the image, else the name with a warning. |
| amd64 | Whether ghcr publishes the image for amd64 alone; --runtime-amd64 records it either way. |
The data directory and the user are resolved the same way for a marketplace
model, and from the same two fields of the same image config; see
Data directories and users. The difference is
only where the answer is recorded: .chap-devops/models-manual.yaml holds a manual
model’s definition, .chap-devops/models.yaml holds every enabled model’s.
--port N|auto and --bind ADDR publish a host port, exactly as on
models enable, and are the flags about the deployment rather than about the
model.
A source the marketplace already lists
models add is for models the catalogue does not have, but a URL copied from
a model’s page usually names one it does. Without --id, such a source is
enabled as the marketplace model, with the catalogue’s reviewed pin:
chap-devops models add https://github.com/chap-models/chapkit_simple_multistep_model
chap-devops models add ghcr.io/chap-models/chapkit_simple_multistep_model:sha-6c1d9ee # the same, pinned to 0.1.1
https://github.com/chap-models/chapkit_simple_multistep_model is the marketplace model chapkit_simple_multistep_model; enabling that (pass `--id <other>` to add it beside it instead)
enabled chapkit_simple_multistep_model v0.1.2 at http://localhost:8700/v2/services/chapkit-simple-multistep-model/run/
run `chap-devops up` to apply
A repository URL matches the entry with that repository (case, a trailing
slash and .git aside) and follows its stable channel; an image matches only
when its tag is one the entry’s versions publish, and pins that version. Any
other tag of the same image is a build the catalogue has not reviewed, so it is
refused:
error: the marketplace already lists chapkit_simple_multistep_model (Simple Multistep); run `chap-devops models enable chapkit_simple_multistep_model`, or pass `--id auto` (or `--id <other>`) to add this one beside it
A local image (my-model:dev) is never matched:
running your own build of a listed model is what models add is for.
To keep a separate entry anyway - a fork’s builds, a branch the catalogue does
not follow - pass --id. --id auto takes the source’s own id, or that id
with the lowest free _2, _3, … suffix when the marketplace or an earlier
models add already uses it.
$ chap-devops models add https://github.com/chap-models/chapkit_simple_multistep_model --id auto
added chapkit_simple_multistep_model_2 (chapkit-simple-multistep-model-2)
enabled chapkit_simple_multistep_model_2 sha-10b2bc7 at http://localhost:8700/v2/services/chapkit-simple-multistep-model-2/run/
run `chap-devops up` to apply
warning: this is the image of the marketplace model chapkit_simple_multistep_model, which registers with chap-core as `chapkit-simple-multistep-model`, so `chapkit-simple-multistep-model-2` may never register; to run chapkit_simple_multistep_model, run `chap-devops models remove chapkit_simple_multistep_model_2`, then `chap-devops models enable chapkit_simple_multistep_model`
--id auto changes the id and the service id here, not the code in the
image. The image above still registers as chapkit-simple-multistep-model, so chap-devops status never shows chapkit-simple-multistep-model-2 as registered. Any --id on the
repository or an image of a marketplace model gives the same warning. --service-id chapkit-simple-multistep-model is
refused, because the marketplace model uses that name. Use --id for a fork
that sets its own MLServiceInfo.id.
The service id has to match the id the service registers with chap-core
(chapkit’s MLServiceInfo.id). It is the Compose service name, the DNS name
and the name the registration resolves to, all at once. Where a model’s own id
differs from its repository name, pass --service-id; chap-devops status shows the
mismatch as a registered service nobody manages next to a model that never
arrived.
The uid probe is the one step that needs Docker: an image that runs as an
account name (USER app) says nothing about what that name resolves to, and
the overlay’s init container chowns the data volume from busybox, which
resolves no names at all. So chap-devops models add pulls the image once and asks
it. Pass --user <uid>:<gid> to skip that.
Where it is recorded
The definition goes in .chap-devops/models-manual.yaml, beside the enabled set:
# Managed by chap-devops; change it with the chap-devops commands, not by hand.
chapkit_dengue_model:
service_id: chapkit-dengue-model
display_name: chapkit_dengue_model
repository: https://github.com/my-org/chapkit_dengue_model
image: ghcr.io/my-org/chapkit_dengue_model
tag: sha-b1d6c31
commit: b1d6c312a83f07aa1a4e66fce05ae7f4eccb8188
follow: main
data_dir: /work/data
user: 10001:10001
runtime_amd64: true
added: 2026-09-24
That file is the definition; .chap-devops/models.yaml still says which models are
on. So chap-devops models disable chapkit_dengue_model keeps the definition and
chap-devops models enable chapkit_dengue_model brings the model back at the recorded
tag, data directory and user, without asking the network anything. The host
port is not in the definition: if the model had one, give --port again. It is
carried over by chap-devops init --force and included in chap-devops backup create.
A deployment that has added nothing has no such file.
Both listings mark these entries, because a local definition is not a reviewed catalogue entry:
ID SERVICE NAME STATUS STABLE LATEST PORT KIND
chapkit_simple_multistep_model chapkit-simple-multistep-model Simple Multistep limited data 0.1.2 0.1.2 via chap-core model
chapkit_simple_multistep_model_2 chapkit-simple-multistep-model-2 chapkit_simple_multistep_model not for use sha-10b2bc7 sha-10b2bc7 via chap-core manual
chap-devops models info chapkit_simple_multistep_model_2 says the same in its own words - a
kind of manual, a source line naming chap-devops models add and the day it
was added, and a follows line naming the branch or reading
nothing (pinned) - and leaves out every field a
marketplace entry would have filled in (the horizon, the covariates, the
period types, the assessment) rather than reporting them as zeroes. The
browser shows manual in its KIND column.
Removing one
chap-devops models remove ID [--purge] [--keep-in-chap]
remove is the way back: it disables the model if it is enabled - stopping its
container and removing its overlay, exactly as models disable does - and then
drops the definition. --purge takes the data volume as well, and means the
same thing here as it does there.
remove also retires the model in chap-core, as models disable --retire
does (see Retiring a model in chap-core).
The removal is permanent, so the template goes as well. --keep-in-chap
leaves chap-core as it is. If chap-core does not answer, remove stops before
it changes anything. Start chap-core, or pass --keep-in-chap.
A marketplace model has no local definition to remove, and says so:
error: chapkit_ghr_model is a marketplace model, so there is no local definition to remove; run `chap-devops models disable chapkit_ghr_model`
What it needs, and what it refuses
chap-devops models add reads GitHub’s REST API and the registry of the image;
both are public for the Chap model repositories, so neither needs a credential.
A registry other than ghcr gives its pull token through the challenge on
/v2/, and chap-devops asks it for an anonymous one. A private registry that
wants credentials for every pull is not read. Run docker login and
docker pull --platform linux/amd64 the image first; then the image config
comes from the local image. GitHub allows 60
requests an hour to an address that sends none, and chap-devops sends
GITHUB_TOKEN (or GH_TOKEN) where the environment names one, for 5000 an
hour instead - it is never stored and never printed, and chap-devops doctor’s
github api line says how much of the hour is left. The
repository form cannot work under --offline and says so, naming the image
reference to pass instead. The image form works offline as long as the image’s
linux/amd64 variant is in the local image store, because then docker image inspect can answer what the registry would have; the error names the docker pull --platform linux/amd64 that puts it there.
Refused rather than guessed at: a bare image name (chapkit_dengue_model), an
image reference with no tag or digest,
an id or service name the marketplace already uses (--id auto picks a free
id), and an id this deployment
has already added. The marketplace always wins a collision, so an id it
publishes later shadows nothing: the local entry is reported and ignored until
it is removed or renamed.
The browser
chap-devops ui opens two pages: models, the marketplace catalogue, and
components, what this deployment is made of. Tab goes to the next page
and shift-Tab back; one s saves both and leaves the browser, so the two
are a single sitting rather than two commands.
The models page is the catalogue as one table across the width of the terminal, with a one-line summary of the row under the cursor under it:
chap-devops · models registry: cache · 59 min 6 models · 1 enabled · 0 pending
╭ Marketplace ─────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ MODEL ID STATUS VERSION PORT │
│──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ ▸ ✓ Simple Multistep chapkit_simple_multistep_model ● limited data 0.1.2 via chap-core │
│ Auto-ARIMA auto_arima_chapkit ● experimental 1.0.2 │
│ GHRmodel chapkit_ghr_model ● experimental 0.1.3 │
│ Rwanda Malaria BYM chapkit_rwanda_malaria_bym_model ● not for use 0.1.3 │
│──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ Simple Multistep ● limited data 0.1.2 (sha-10b2bc7) enabled, via chap-core requires - i for details│
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
[j/k] move [tab] page [space] toggle [i] info [p] port [s] save [ctrl+k] commands [?] help [q] quit
The summary line names the model, what the author’s assessment means, what the
row is pinned to, whether this deployment runs it and what data it needs, with
i for details held at the right-hand edge. A terminal too narrow for all of
it cuts the covariates first, marked with ~; the hint never goes. Everything
the line leaves out - the summary text, the defaults, the period types, the
forecast horizon and the service user - is behind i.
The first column is the cursor, the second what the row will be: ✓ for a
model this deployment runs, + for one you have switched on, - for one you
have switched off. STATUS is the author’s own chapkit assessment, the same
five words the table above explains; VERSION
is what the row’s channel resolves to; and PORT is how the model is reached.
A KIND column appears when the list holds something that is not a plain
marketplace model: a starter, or an entry chap-devops models add created. The
rows are in the same maturity order chap-devops models list uses, with starters
last when t shows them; nothing done in the browser reorders them, so the
cursor stays on the row it was on.
The keys, none of which need Alt on a Norwegian keyboard:
j / k / arrows move space enable or disable
g / G first / last i / Enter the full details
PageUp / PageDown jump a page p publish a host port: a number, auto, or none
ctrl-u / ctrl-d jump a page P take the host port away
Tab / shift-Tab change page v pick the channel (stable, latest)
s save t show or hide starters
u discard / filter (Enter keeps, Esc clears)
q quit Esc clear the filter, or quit
? help ctrl-k / ctrl-p the command palette
o open in a browser: the model's repository, or the component's web interface
c the model's image reference, on the status line
m the configured models of the model in chap-core
y / n answer the quit confirmation
ctrl-n also moves down and ctrl-c always leaves. While filtering, the
arrow keys still move the cursor and ctrl-u empties the filter without
leaving it. An active filter shows in the title as · filter <text> and in
the box title as how many entries it left. The key bar gives up its entries
from the least useful end when the terminal is too narrow for all of them.
The PORT column reads via chap-core for an enabled model with no host port
of its own, the port number for one that publishes one, and auto for a row
that has asked for a port that is not picked until you save. A row you have
switched on or off reads will be enabled or will be disabled until you
save.
The configured models page
m on a model row opens the configured models of the
model in chap-core, with the columns NAME, COVARIATES, SOURCE and OPTIONS.
The keys on this page:
j / k / arrows move
a add a configured model
e change the values of the configured model under the cursor
d archive the configured model under the cursor
r ask chap-core again
Esc / q / m go back to the models page
a and e open the form. It has one field for each user option of the
model, with its kind, its default and its description, and one field for the
covariates. The form of a also has a field for the variant name. The form
of e starts with the current values. An empty field uses the default.
Taband the arrows move between the fields.Entersaves the form.Esccloses the form and writes nothing.
chap-devops writes nothing to chap-core until you answer y to the question. For
d, the question says that chap-core keeps the configured model, and that the
Modeling App shows it as Archived.
The page needs a chap-core that runs and a model that the deployment runs. If chap-core does not answer, the page says:
chap-core at http://localhost:8700 is not running; start it with `chap-devops up`, then press r
If the model is not enabled, or the deployment has no chap-core, the footer line gives the way out.
Publishing a host port
p opens a dialog over the list, prefilled with the port the row has today or
with auto:
╭ Host port for Simple Multistep ────────────────────────────────────────╮
│ port › 80_ │
│ now: via chap-core · range 5001-5999 · api port 8140 is taken │
│ port 80 is outside this deployment's range 5001-5999 │
│ │
│ [enter] apply [esc] cancel [auto] any free port [none] no host port │
╰────────────────────────────────────────────────────────────────────────╯
It takes a port number, auto for the lowest free port in the project’s
range, or an empty line or none to take the port away again. A number it
cannot accept - outside the range, chap-core’s own API port, or one another
model in the list is already asking for - keeps the dialog up with the reason
in it, as above, so the number is corrected rather than typed again. The two
checks only a save can make - a port another compose file claims, and a port
something on this machine is listening on - happen when the selection is
applied, exactly as they do for chap-devops models expose. P takes the port away
without the dialog.
Picking a channel
v opens the same kind of dialog, listing both channels with the version each
one resolves to and a ✓ on the one the row follows today:
╭ Channel for Simple Multistep ───────────────────────────╮
│ ▸ ✓ stable 0.1.2 │
│ latest 0.1.2 │
│ │
│ [enter] apply [esc] cancel [j/k] move [s/l] pick one │
╰─────────────────────────────────────────────────────────╯
j and k move between them, s and l jump straight to one, Enter takes
it and Esc leaves the row alone. Changing the channel re-resolves the
version when the selection is saved; changing only the port does not.
A row pinned with --version follows no channel. The VERSION column shows
the pin, and the dialog puts no ✓ on stable or latest. A third line,
which the cursor cannot reach, shows the pin:
│ ✓ pinned 0.1.1 │
The summary strip shows the pin too. A save keeps the pin unless you pick a channel.
The components page
Tab from the models page opens what this deployment is made of, with the same
columns chap-devops components list prints:
chap-devops · components registry: embedded 4 components · 2 enabled · 0 pending
╭ Components ──────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ COMPONENT STATE REACH WHAT IT IS │
│──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ ▸ ✓ chap-core enabled http://localhost:8700 Chap itself: chap-core, its worker, Valkey and PostgreSQL │
│ ✓ ocs enabled http://localhost:8790 Open Climate Service: climate data, reachable at http://ocs:9000 │
│ s3 off - RustFS, an S3-compatible object store OCS will keep objects in │
│ dhis2 off - DHIS2 and its own database, for a deployment that wants one │
│ │
│ │
│ │
│──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ chap-core enabled here http://localhost:8700 compose.yml + compose.chap-devops.yml Chap itself: chap-co~ i for details│
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
[j/k] move [tab] page [space] toggle [i] info [o] open [p] port [s] save [ctrl+k] commands [?] help [q] quit
The marker column reads the same way it does on the models page: ✓ for a
component this deployment has, + for one you have switched on this session,
- for one you have switched off. STATE follows it - enabled, off, and
adding or removing while a change is pending - so the row says both what is
recorded and what saving would do to it. REACH is where the component answers
from this machine, or internal for one that publishes no host port, and -
for one that is off. The strip under the table is the row the cursor is on:
whether this deployment has it, where it answers, which compose file it is
rendered into, and what it is, with the same i for details held at the
right-hand edge.
chap-devops · components registry: embedded 4 components · 3 enabled · 1 pending
╭ Components ──────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ COMPONENT STATE REACH WHAT IT IS │
│──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ ✓ chap-core enabled http://localhost:8700 Chap itself: chap-core, its worker, Valkey and PostgreSQL │
│ ✓ ocs enabled http://localhost:8790 Open Climate Service: climate data, reachable at http://ocs:9000 │
│ ▸ + s3 adding internal RustFS, an S3-compatible object store OCS will keep objects in │
│ dhis2 off - DHIS2 and its own database, for a deployment that wants one │
│ │
│ │
│──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ 1 pending change nothing is written until you save │
│ + s3 enable, on the compose network │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
[j/k] move [tab] page [space] toggle [i] info [o] open [s] save 1 change [u] discard [?] help [q] quit
space toggles the component under the cursor, p sets its host port and P
takes it away, o opens its web interface in a browser, and i or Enter
opens the details. The pending counter in the
header, the summary strip and the quit confirmation all count both pages, so a
component change cannot be lost by saving from the models page - and u
discards both pages, because it means “nothing I did this session” rather than
“nothing on this page”.
The port prompt here is narrower than the models one: it takes a port number,
or an empty line or none to take the port away, and not auto. auto
means “the lowest free port in this project’s model range”, and a component
publishes a well-known port of its own that is not in that range, so the prompt
says what to type rather than picking a number nobody asked for. p on
chap-core is refused outright, because its host port is the API port, which
CHAP_API_PORT in .env sets: the footer names that line instead. p
on a component that is off asks you to enable it first.
o opens the row’s web interface in a browser - chap-core’s API documentation,
the OCS web interface, DHIS2’s own - and says in the footer when there is none
to open: a component this deployment does not have, one that publishes no host
port (naming the address it is reached at inside the deployment), one this
session has only just enabled and not saved, and the object store, which speaks
the S3 API and serves no interface a browser is useful for. It opens what this
deployment publishes now, so a port changed in this session and not yet
saved is not the port it opens. Open web interface in the palette does the
same thing. It is the same command as
chap-devops open, which is
the one that can also say whether the container is running.
i opens the details, which say where the component’s compose file and data
volume are, and - for OCS - which of its settings this page deliberately does
not edit:
╭ ocs ────────────────────────────────────────────────────────────────────────────────────────── enabled here ╮
│ Open Climate Service: climate data, reachable at http://ocs:9000 │
│ │
│ state enabled │
│ reach http://localhost:8790 │
│ compose compose.ocs.yml · rendered from .chap-devops/components.yaml by `chap-devops sync` │
│ volume ocs_data · kept when the component is disabled │
│ config ocs/climate-service.yaml · yours to edit; chap-devops changes only its read_only and plugins_dir keys │
│ │
│ not on this page: │
│ `chap-devops components enable ocs --base-url URL` │
│ the public origin OCS builds its STAC and openEO links from │
│ `chap-devops components enable ocs --read-only` │
│ refuse ingestion over HTTP (--read-write allows it again) │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
Those two are one-way settings on a file rather than a field on a row:
--base-url is a URL to type correctly rather than pick, and --read-only
edits ocs/climate-service.yaml, which is the operator’s file. A dialog that
looked like it owned them would be a dialog that quietly rewrote your config,
so the overlay names the commands instead. See
Behind a reverse proxy and
Read-only instances.
DHIS2’s overlay is the longest one, because a DHIS2 is the component with the most that cannot be seen from its row:
╭ dhis2 enabled here ╮
│ DHIS2 and its own database, for a deployment that wants one │
│ │
│ state enabled │
│ reach http://localhost:8780 │
│ compose compose.dhis2.yml · rendered from .chap-devops/components.yaml by `chap-devops │
│ sync` │
│ volume dhis2_home, dhis2_db, dhis2_dump · kept when the component is disabled │
│ config dhis2/dhis.conf · yours to edit, and DHIS2 will not start without it │
│ seed https://databases.dhis2.org/climate/laos/2.42/laos.sql.gz · restored │
│ once, into the database the first `chap-devops up` creates │
│ cache dhis2_dump holds the downloaded dump rather than data; the dhis2-dump │
│ one-shot fetches it again when the volume is empty │
│ first start minutes, not seconds: DHIS2 migrates its schema on the way up, and │
│ `chap-devops logs dhis2` is where that shows │
│ │
│ not on this page: │
│ `chap-devops components enable dhis2 --seed SPEC` │
│ the dump a database being created is restored from: default, none, a URL, or a │
│ path in the deployment directory │
│ `v` on this row, or `chap-devops components enable dhis2 --tag TAG` │
│ the DHIS2 version, 2.42 here; it migrates a schema forward only, so run `chap-devops │
│ backup create` first │
╰────────────────────────────────────────────────────────────────────────────────────╯
Four things only the overlay says. That dhis2/dhis.conf is yours and that
DHIS2 does not start without it. That one of the three volumes is a download
cache rather than data. What the database will be restored from, and that a
restore only ever happens to a database being created - so changing the seed on
a deployment that has already started means removing the volume. And that the
first start takes minutes, which is the difference between reading the first
chap-devops status as slow or as broken.
The two settings at the bottom are a key in .chap-devops/components.yaml and a
chap-devops sync. No flag changes the seed after init: it is a one-chance setting.
The image tag also has --tag on chap-devops components enable dhis2, and v on
the dhis2 row picks it, since DHIS2 is the one component with versions to
choose between. See
Changing the DHIS2 version.
The details, and the command palette
i or Enter opens the full entry over the list, in the order the Chap
Modeling App presents a model: what it does, what it is, who wrote it, and
only then how this deployment runs it.
╭ Rwanda Malaria BYM chapkit_rwanda_malaria_bym_model not enabled here ╮
│ Spatio-temporal Bayesian model for malaria incidence in Rwanda at sector (ADM3) │
│ level: BYM spatial effects, RW1 temporal effects and an IID space-time interaction │
│ with lagged climate covariates, fitted with R-INLA. Built for one country's data — │
│ it needs geometry and the full climate covariate set. │
│ │
│ version 0.1.3 (sha-1ee5ea1) · verified · channels stable, latest │
│ 0.1.2 (sha-a7b2892) · verified │
│ author Similien NDAGIJIMANA · HISP Centre, University of Oslo · │
│ knut.rand@dhis2.org │
│ status ● gray, not intended for use, deprecated or kept for backwards │
│ compatibility │
│ period monthly · horizon 1 to 24 periods │
│ target disease cases │
│ covariates population, rainfall, mean_temperature, relative_humidity · defaults - │
│ · free extras allowed · geometry required │
│ │
│ reach via chap-core (through chap-core's /run/ proxy) │
│ image ghcr.io/chap-models/chapkit_rwanda_malaria_bym_model:sha-1ee5ea1 │
│ runtime ghcr.io/dhis2-chap/chapkit-r-inla (amd64 only) │
│ │
│ maintainers mortenoh, edvinstava │
│ repository https://github.com/chap-models/chapkit_rwanda_malaria_bym_model │
│ citation Climate Health Analytics Platform. 2025. "Kigali Malaria BYM Model". │
│ HISP Centre, University of Oslo. │
╰────────────────────────────────────────────────────────────────────────────────────╯
The title carries the name, the marketplace id and whether this deployment
runs it - enabled, via chap-core, enabled on port 5010 or not enabled here; the id is the part that gives way when the three do not fit. version
is the pin, how far that release is trusted and which channels point at it,
with the rest of the published history under it when there is more than one.
target is what chap-core forecasts, the same for every model in the
catalogue. A model this deployment runs also lists its data directory and its
service user with where that user came from. A model with a citation in the
marketplace also gets a citation row, with the attribution as the marketplace
gives it. Long values - the summary, the assessment, the citation - wrap under
their label rather than being cut.
j and k scroll it when it is taller than the terminal, o opens the
repository in a browser, c puts the image reference on the status line, and
esc, i or q closes it again. A component’s overlay binds o too, where it
opens that component’s web interface, and binds no c: a component has no image
reference of its own.
ctrl-k or ctrl-p opens a command palette: type to narrow it, up and
down to move, Enter to run, esc to leave. Its first entry is always the
other page - Go to the components page, or Go to the models page - and the
rest follow whichever page you are on, so on the components page it offers
Enable or disable the ocs component, Set a host port for the ocs component
and Open the web interface of the ocs component where the models page offers
the channel, the starter filter and Open the repository of <model>.
It offers everything the keys do, plus three the keys do not: refreshing the
catalogue from the marketplace without leaving the browser, the way
chap-devops registry update does, opening this documentation - the chapter for the
page you are on - and Save a screenshot (SVG), which writes the frame the
palette just closed over to chap-devops-ui-<date>-<time>.svg in the directory
chap-devops ui was started in and names the file on the status line. The two port
commands are separate, so “set a host port” never takes one away.
Every component is also in the palette by name, from either page - Turn the dhis2 component on or off and one such entry per component - because that is
where somebody looking for OCS, an object store or a DHIS2 finds out the browser
has them at all. Running one turns it on or off and moves the components page’s
cursor to it, without moving the page you are on.
Saving
With something unsaved, the summary strip lists what saving would write, one
line per change, and the key bar grows a filled [s] save N changes and a
[u] discard next to it. Both pages are in that count and in that one save:
s writes the model changes through the same path as chap-devops models enable and
the component changes through the same path as chap-devops components enable. Then
the browser closes and prints what changed:
updated chapkit_simple_multistep_model v0.1.2 on http://localhost:5001
enabled the s3 component
run `chap-devops up` to apply
A save writes the files and starts nothing: run chap-devops up to apply it. The
help line of s says save the changes; `chap-devops up` applies them, and the
palette entry says Save the changes; `chap-devops up` applies them. With nothing pending, s prints no changes to save and closes the browser.
Nothing is written until you save, and quitting with unsaved changes asks
first, on either page and for either kind of change: Quit anyway? y / n.
chap-devops ui owns the terminal, so it rejects --json.
chap-devops init --interactive opens the same browser to pick the initial model
set instead of reading --models.
How a model overlay looks
Each marketplace model is a chapkit
container listening on port 8000 with /health and /api/v1/info. This is the
overlay chap-devops writes for Simple Multistep. The file carries two header lines
and nothing else by way of explanation: what it does is below.
# Generated by chap-devops from .chap-devops/; edit there and run `chap-devops sync`.
# chapkit_simple_multistep_model 0.1.2 (https://github.com/chap-models/chapkit_simple_multistep_model)
services:
chapkit-simple-multistep-model:
labels:
com.winterop.chap-devops.role: model
com.winterop.chap-devops.model: "chapkit_simple_multistep_model"
com.winterop.chap-devops.kind: init
restart: unless-stopped
image: ghcr.io/chap-models/chapkit_simple_multistep_model:${CHAPKIT_SIMPLE_MULTISTEP_MODEL_IMAGE_TAG:-sha-10b2bc7}
platform: linux/amd64
init: true
expose:
- "8000"
environment:
SERVICEKIT_ORCHESTRATOR_URL: http://chap:8000/v2/services/$$register
# Uncomment if chap has SERVICEKIT_REGISTRATION_KEY set:
# SERVICEKIT_REGISTRATION_KEY: ${SERVICEKIT_REGISTRATION_KEY:-}
read_only: true
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
user: 1001:1001
volumes:
- type: tmpfs
target: /tmp
tmpfs:
size: 2000000000
- type: volume
source: ck_chapkit_simple_multistep_model_data
target: /app/data
depends_on:
chapkit-simple-multistep-model-init:
condition: service_completed_successfully
chap:
condition: service_healthy
chapkit-simple-multistep-model-init:
labels:
com.winterop.chap-devops.role: model
com.winterop.chap-devops.model: "chapkit_simple_multistep_model"
com.winterop.chap-devops.kind: init
image: busybox:1.38
command: ["sh", "-c", "chown -R 1001:1001 /app/data"]
user: "0:0"
volumes:
- type: volume
source: ck_chapkit_simple_multistep_model_data
target: /app/data
restart: "no"
volumes:
ck_chapkit_simple_multistep_model_data: {}
What an overlay contains
The overlay does seven things.
- Labels both containers with their role, the marketplace id and the
deployment’s kind, so
docker ps --filter label=com.winterop.chap-devops.modelfinds every model on the machine. See Container labels. - Pins the image.
image: <repo>:${<ID>_IMAGE_TAG:-sha-<commit>}, where the default is theimage_tagof the version the channel resolves to. The deployment stays reproducible, and a different build is one.envline away. - Self-registration. The service registers itself with chap-core through
SERVICEKIT_ORCHESTRATOR_URL: http://chap:8000/v2/services/$$register(the$$is a literal$for Compose). The Compose service name, the DNS name and the marketplaceservice_idare the same string, which is what makes the registration resolvable. SetSERVICEKIT_REGISTRATION_KEYin.envto require a shared secret. In a deployment without chap-core the wholeenvironment:block is left out, so the service registers nowhere; see Model services without chap-core. - Publishes no host port. The service gets
expose: ["8000"]and nothing else, unless the deployment has no chap-core, where a model gets a port by default. See Ports. - Hardening, matching the posture of the base services:
init: true,read_only: true,no-new-privileges,cap_drop: [ALL], a 2 GB tmpfs at/tmp, and a named volume for the model’s data directory (the only writable path besides/tmp). - Runs it as the account its image declares.
user: <uid>:<gid>, in numbers, for an image that drops to an account of its own. An image that runs as root gets nouser:line at all: the line exists to override the image, and overriding root with root only risks taking a permission the image’s own binaries need. See Data directories and users. - Hands the data volume to the model user, through the init container below - again, only where the model is not root.
- Ordering.
depends_on: the init container withcondition: service_completed_successfully(where there is one) andchapwithcondition: service_healthy, so a model only starts once its volume is writable and chap-core can accept its registration. Without chap-core only the init container is waited for.
Three things an overlay deliberately leaves out:
- no
networks:key, so the service joins the compose default network only - which reaches chap-core but not PostgreSQL or Valkey; - no
healthcheck:, because chapkit images ship their own; - no
depends_onbetween models, because they are independent of each other.
The init container, and why
Docker seeds a fresh named volume from whatever the image has at the mount
point, ownership included, so an image that never creates its data directory
yields a root-owned volume the unprivileged model cannot write to, and chapkit
dies on sqlite3.OperationalError: unable to open database file.
Compose cannot chown a volume, so the overlay of a model that runs as an
unprivileged account ships a one-shot <service_id>-init container (busybox,
as root, restart: "no") that chowns the mount point, recursively, to the
model’s numeric uid:gid before the model starts. Busybox resolves no chapkit
account, which is why the numbers matter, and why the user: line above
carries the same two numbers rather than the name. It is the one service in a
deployment that is deliberately not restart: unless-stopped (the "no" is
quoted because bare no is YAML’s false).
Every model overlay gets one, root included, where the chown is to 0:0.
A volume docker has just created is root-owned already, so on a fresh
deployment a root model’s init container does nothing - but a volume that
already exists carries whoever owned it last. If a model that ran as
1000:1000 now runs as root (a new image, or a --user override), its volume
is owned by 1000, and the model cannot write to it: the overlay drops every
capability, and CAP_DAC_OVERRIDE is the one that lets root ignore the
permission bits. One busybox one-shot costs a second on chap-devops up and repairs
that change, which is worth more than the line it saves.
The container has a second job: because it mounts the same named volume at the
same path as the model itself, chap-devops backup reads and writes model data
through it, whether the model is running, stopped, or brought down entirely.
See Backup and restore.
Data directories and users
Both differ per image, and both are read off the image itself when the model
is enabled - chap-devops models enable, chap-devops init --models, the browser’s
toggle. The answers go into .chap-devops/models.yaml, so chap-devops sync renders the
overlay from what is recorded and never asks the network: the same state
renders the same bytes on any machine.
| Value | Where it comes from, in order |
|---|---|
| data dir | --data-dir, else <WorkingDir>/data from the image config, else the built-in table, else /work/data. |
| user | --user, else config.User from the image config on its registry, else the same field from an image already pulled here (docker image inspect --platform linux/amd64), else the built-in table. |
chap-devops models info <id> and the browser’s details overlay name which of those
answered: image config, docker probe, --user or table. A run that could
reach neither the registry nor a local copy says so, because the table is a snapshot
this binary was built with and an image can change what it runs as.
What the marketplace images declare today:
| Image | Data directory | User |
|---|---|---|
| The simple multistep model | /app/data | chap, rendered as 1001:1001 |
| The Rwanda BYM model | /work/data | chapkit, rendered as 1000:1000 |
| GHRmodel | /work/data | app, probed as 10001:10001 |
| Everything else | /work/data | root |
An image that runs as root gets no user: line; its init container is
rendered like any other model’s and chowns the volume to 0:0. Three of the
seven marketplace images end their Dockerfile on USER root, and forcing an
unprivileged uid on one of them takes away a permission its own binaries need:
the Rwanda BYM model up to 0.1.1 kept its INLA binaries root-owned and mode
744, so every prediction failed with inla.run: Permission denied. That is
why the image is asked rather than assumed: 0.1.2 rebuilt it to run as
chapkit, and re-enabling the model is what picks that up. See
Permission denied from a model’s own binaries.
The init container needs an account name as numbers: chapkit is uid/gid 1000
and chap is 1001. GHRmodel’s app is not one of them, so enabling it pulls
the image once and asks it, exactly as chap-devops models add does. A --user that
is already numeric is passed through, and a name nothing could resolve falls
back to 1000:1000 with a warning from chap-devops sync. --data-dir and --user override everything above, for an image
whose config says something the deployment has to contradict.
chap-devops doctor re-checks each enabled model against the image on this machine
and warns when the two have drifted apart, which is what a marketplace model
moved to a new tag by hand looks like.
A model added with chap-devops models add is resolved the same way at the moment it
is added, and carries its answers in .chap-devops/models-manual.yaml. See
Models outside the marketplace.
The amd64 pin, and why
The overlay of every marketplace model pins platform: linux/amd64, and the
overlay of a model added with chap-devops models add pins it only when its image is
published for amd64 alone (or --runtime-amd64 says so). The marketplace images are
published for amd64, as is chap-core itself, so an arm64 host such as Apple
silicon pulls that variant and runs it under emulation instead of failing with
no matching manifest for linux/arm64.
Components
A component is a piece a deployment is made of: a service, or a small group
of services, that chap-devops sync renders one compose file for. chap-core is a
component like the others and is on unless you turn it off; the rest are opt-in.
| Component | What it is | On by default |
|---|---|---|
chap-core | Chap itself: chap-core, its worker, Valkey and PostgreSQL, plus the model overlays on top. | yes |
ocs | Open Climate Service, beside chap-core, reachable inside the deployment at http://ocs:9000. | no |
s3 | RustFS, an S3-compatible object store for OCS to keep its objects in. | no |
dhis2 | A demo or development DHIS2 and its own PostgreSQL, for a deployment that wants one beside Chap. See DHIS2. | no |
chap-devops components list
COMPONENT STATE REACH WHAT IT IS
chap-core enabled http://localhost:8700 Chap itself: chap-core, its worker, Valkey and PostgreSQL
ocs enabled http://localhost:8790 Open Climate Service: climate data, reachable at http://ocs:9000
s3 off - RustFS, an S3-compatible object store OCS will keep objects in
dhis2 enabled http://localhost:8780 DHIS2 and its own database, for a deployment that wants one
Turning one on and off
At creation time:
chap-devops init mychap --with ocs # chap-core and OCS
chap-devops init mychap --with ocs,s3 # both, plus the object store
chap-devops init mychap --with ocs,s3 --ocs-port 9010 --s3-port 9002
chap-devops init mychap --with ocs --ocs-port none --ocs-read-only
chap-devops init mychap --with dhis2 --dhis2-port 18080 --dhis2-seed none
chap-devops init mychap --with dhis2 --dhis2-tag 2.41 # another DHIS2 version
chap-devops init mychap --with dhis2 --dhis2-image dhis2/core-dev --dhis2-tag master # the next, unreleased
--ocs-port, --s3-port and --dhis2-port take a port number or none,
--ocs-read-only starts the instance refusing every write over HTTP, and
--dhis2-seed says what the DHIS2 database is restored from the first time it is
created. Each one needs its component: a setting for something --with did not
ask for is refused rather than quietly dropped. There is no --ocs-read-write to
match,
so a --force over a directory whose ocs/climate-service.yaml already says
read_only: true has no init-side way back - chap-devops components enable ocs --read-write is the route, and the file is the operator’s either way. See
Read-only instances.
Afterwards, in the deployment directory:
chap-devops components enable ocs # publishes it on 8790
chap-devops components enable ocs --port 9010
chap-devops components enable ocs --port none # no host port; see below
chap-devops components enable s3 # internal only
chap-devops components enable dhis2 # publishes it on 8780
chap-devops components enable dhis2 --tag 2.43 # and picks its version
chap-devops components disable ocs
chap-devops components disable ocs --purge # and its data
Both commands edit .chap-devops/components.yaml and then run chap-devops sync, so the
compose files on disk always match. chap-devops up applies them. disable also
stops and removes the containers of the component it is taking away, while
compose still has the definition to name them by, and says so in a line of
its output - so the host port it published is free straight away.
Enabling a component that is already on is how its settings change: --port
moves the host port it publishes, --port none takes it away, and nothing else
is touched. --port none reads the same on every component that takes a port, so
there is one flag to remember rather than a verb per component. dhis2 has two
settings that are not --port: the seed and the image. --tag and --image
change the image (see
Changing the DHIS2 version). --seed SPEC
changes the seed (default, none, a URL or a path); it applies only to a
new dhis2_db (see The seed).
What enable tells you
Every run says what it did and what to do next. A problem comes as a
warning: line on stderr. With -v, the files it wrote and the background
come as hint: lines (see Levels). Turning OCS on for
the first time is the noisiest of them:
$ chap-devops -v components enable ocs
enabled ocs on http://localhost:8790
warning: port 8790 is already in use on this machine (needed by ocs); free it, or run `chap-devops components enable ocs --port 8791`
hint: wrote ocs/climate-service.yaml; it is yours to edit, and chap-devops changes only its `read_only` and `plugins_dir` keys
hint: OCS will soon need an S3-compatible object store; `chap-devops components enable s3` adds one, and the OCS service then gets the S3_* variables it will read
hint: the OCS data source variables are now in `.env`, commented out: ERA5-Land needs one or both of ECMWF_DATASTORES_* and EDH_API_KEY, per dataset; WorldPop and CHIRPS3 need none. `chap-devops auth show` reports which are set
hint: wrote compose.ocs.yml
hint: wrote .env
run `chap-devops up` to apply
Without -v, the same run prints the first line, the warning and the last
line.
The port line is the same probe chap-devops init makes, and a warning for the same
reason: nothing is being started here, the listener is often something you are
about to stop, and chap-devops up is where a taken port becomes a refusal. A port
one of this deployment’s own running services already publishes is not a
conflict. See Component ports.
The data source line appears on the run that appends the credential block to
.env, because until then the only sign of it was written .env and which
dataset needs which key is the one thing the variable names do not say. See
Data source credentials.
The other two are the soft dependency between OCS and the store, in both
directions. Enabling s3 on a deployment with no OCS says so, because nothing
else in a chap-devops deployment writes to it:
hint: the object store is for OCS to keep its objects in, and this deployment has no OCS; `chap-devops components enable ocs` adds one, and nothing else here writes to the store
and disabling s3 while OCS is on says what the next sync takes off the OCS
service, which is a change to a service you did not name:
hint: the OCS service loses its S3_* variables on this sync; OCS does not read them yet, and `chap-devops components enable s3` puts them back
Neither is a refusal. A store with nothing to put in it and an OCS with no store are both states an operator may well be passing through on purpose.
Turning dhis2 on says three more things, because none of them is visible
from the compose file:
$ chap-devops -v components enable dhis2
enabled dhis2 on http://localhost:8780
hint: wrote dhis2/dhis.conf; it is yours to edit, and chap-devops never rewrites it
hint: the first `chap-devops up` restores https://databases.dhis2.org/climate/laos/2.42/laos.sql.gz into `dhis2_db`, once, on the database it creates; after that only `chap-devops components disable dhis2 --purge` makes it happen again; the restore empties the data values that analytics cannot read as a number, and `chap-devops logs dhis2-db` lists them
hint: the first `chap-devops up` takes minutes before DHIS2 answers - it migrates its schema on the way up - and `chap-devops logs dhis2` is where that shows
the Modeling App reaches chap-core through a DHIS2 route, and this deployment has none yet; once DHIS2 answers, `chap-devops dhis2 connect` adds it, generates analytics and installs the apps
hint: wrote compose.dhis2.yml
hint: wrote .env
run `chap-devops up` to apply
The first is the restore happening once, on a data directory PostgreSQL has
just created, which is the half that decides whether changing the seed later does
anything. The second is the first start taking minutes rather than seconds, which
is the half that decides whether the first chap-devops status reads as a broken
deployment. The third comes only on a deployment with chap-core: the Modeling App
needs a DHIS2 route to chap-core, and chap-devops dhis2 connect adds it (see
Connecting the Modeling App to Chap).
See DHIS2.
The data volume
disable keeps the component’s data, exactly as it keeps a disabled model’s,
and names the volume it kept:
$ chap-devops -v components disable ocs
disabled ocs
kept volume mychap-1ab2c3_ocs_data; remove it with `chap-devops components disable ocs --purge` or `docker volume rm mychap-1ab2c3_ocs_data`
hint: the ocs/ directory is left alone; it is yours
hint: removed compose.ocs.yml
hint: `chap-devops status` shows what runs now
ocs keeps ocs_data, s3 keeps s3_data, and dhis2 keeps dhis2_home,
dhis2_db and dhis2_dump, each prefixed with the compose project name. A
component is not limited to one volume, so every one it declares is named on a
line of its own - dhis2_dump included, which is a download cache rather than
data but is still a name someone would otherwise have to find by hand. Naming
them is the whole point: the compose file that declared the volume has just been
removed, so chap-devops down --volumes no longer reaches it. A volume docker does
not have - the component was never started - is not named, since nothing was
kept. --purge removes them
with the component, after the containers, and
reports removed volume <name> or volume <name> not found per volume; --json carries
purged and kept_volumes. --purge works on a component that is already
off, so a volume that was forgotten can still be removed by name.
chap-devops components disable chap-core --purge is refused: chap-core’s volumes -
the database and its own data - are declared by upstream’s compose file, which
this CLI renders but does not author, so there is no one volume --purge could
mean. chap-devops down --volumes removes every volume of the deployment, and is the
honest way to ask for that.
chap-devops doctor reports a component volume whose component is off as a leftover;
see Doctor.
Re-initialising with --force
chap-devops init --force over an existing deployment sets the component set from
the flags it is given, exactly as --models sets the model set: --with and
--without (or --only) are the whole answer, and a component the directory had but the new
run does not ask for is going. It is reset-from-flags, not a merge, so the way
to keep a component across a --force is to name it again.
Each one that goes gets its own warning before anything is written:
warning: this directory had the ocs component and this run does not ask for it, so compose.ocs.yml is removed and its data volume is left behind; re-run with `--with ocs` to keep it
warning: this directory had the s3 component and this run does not ask for it, so compose.s3.yml is removed and its data volume is left behind; re-run with `--with s3` to keep it
So chap-devops init . --force --with ocs keeps OCS and drops the store, and
chap-devops init . --force --with ocs,s3 keeps both. A component named in
--without is not warned about: you said so.
What a dropped component loses is its compose file and its containers, which are stopped and removed while the compose file that names them is still on disk
- a service whose definition has just been deleted cannot be stopped by name
afterwards, and one left running would hold its host port against a deployment
that no longer asks for it. What it keeps is its data volume, on the same terms
disablekeeps one: nothing declares it any more, sochap-devops down --volumesno longer reaches it anddocker volume rm <name>orchap-devops components disable <name> --purgeis what removes it. Theocs/directory is yours and is never touched.
.env is kept as it always is, so the database password, the API token and any
OCS credentials survive a --force whatever happens to the components.
The files
mychap/
.chap-devops/
components.yaml intent: which components are on, and their settings
compose.ocs.yml artifact: the ocs service and its data volume
compose.s3.yml artifact: the object store, its health check and the
one-shot that creates the bucket
compose.dhis2.yml artifact: DHIS2, its own PostgreSQL, two one-shots and
three volumes
ocs/
climate-service.yaml the OCS instance configuration - yours, written once
plugins/ optional dataset plugins - yours, mounted if present
dhis2/
dhis.conf the DHIS2 instance configuration - yours, written once,
and DHIS2 does not start without it
.chap-devops/components.yaml is intent, like project.yaml and models.yaml:
chap-core:
enabled: true
ocs:
enabled: true
port: 8790
base_url: null
read_only: false
image_tag: main
s3:
enabled: true
port: null
dhis2:
enabled: true
port: 8780
image_tag: '2.42'
image: dhis2/core
seed: default
connected_at: null
Every field has a default, so a missing field, or a missing file, loads as “chap-core on, nothing else”.
Two keys in there are records rather than intent, and each is marked as such
where it appears. ocs.read_only mirrors what ocs/climate-service.yaml says,
because OCS reads that file and not this one; dhis2.connected_at notes when
chap-devops dhis2 connect last finished, and its only job is to stop chap-devops up and
chap-devops status asking for a connect that has already happened. Neither is ever
read as evidence - see connected_at, and what it is
not, which names the command that
does ask DHIS2.
The component compose files sit in the -f list between compose.chap-devops.yml
and compose.marketplace.yml:
docker compose -f compose.yml -f compose.chap-devops.yml -f compose.ocs.yml \
-f compose.s3.yml -f compose.dhis2.yml -f compose.marketplace.yml up -d
Reaching a component from a browser
chap-devops components list says where each component answers. chap-devops open NAME
takes you there:
chap-devops open dhis2
chap-devops open ocs
chap-devops open chap-core
chap-devops open # what is there to open, and at which address
opening the DHIS2 user interface at http://localhost:18080
Before that line, chap-devops sends one request to that address, with a
three-second limit. Any HTTP answer counts, a 401 from a protected /docs
included. With -v, a hint gives the result:
hint: dhis2 answered at that address; `chap-devops status` reports the rest of this deployment
A container that is up and not serving yet gives the warning the dhis2 container is running and did not answer yet instead.
The address is not always the component’s origin, because the origin is not always the useful page:
| Component | What chap-devops open opens | Why |
|---|---|---|
chap-core | /docs on the API port | The API answers JSON everywhere else; /docs is its interactive documentation, and the one page on that port a person reads. |
ocs | the root of its host port | OCS serves a web interface there as well as its API. |
dhis2 | the root of its host port | That is the DHIS2 login page, and the whole of its UI hangs off it. |
s3 | nothing | The object store speaks the S3 API and serves no web interface; a browser aimed at it gets an XML error document. |
chap-core’s address follows CHAP_API_PORT and CHAP_ROOT_PATH in .env, so a
deployment served under a path prefix opens /master/docs rather than /docs:
the prefix moves every route chap-core serves. See
How the API port is set and
What .env holds. A deployment with an API token
also gets a line that says that /docs is behind it, and that
chap-devops auth show --reveal prints the token to paste into the page’s Authorize
button; see Authentication.
With no name it lists every component and what each one opens, which is both the answer to “what is there” and a reminder of the addresses. A second table lists each enabled model that publishes a host port:
COMPONENT OPENS WHAT IT IS
chap-core http://localhost:8700/docs chap-core's API documentation
ocs http://localhost:8790 the OCS web interface
s3 - an S3 API; no web interface to open
dhis2 - not a component of this deployment
MODEL OPENS WHAT IT IS
chapkit_simple_multistep_model http://localhost:5001/docs the model's API documentation
3 of them can be opened: run `chap-devops open NAME`
What it refuses, and what it only warns about
Three answers open nothing, and each names what is true instead:
error: ocs is not a component of this deployment, so there is nothing to open; run `chap-devops components enable ocs` to add it
error: ocs publishes no host port, so there is nothing to open from this machine: it is reached at http://ocs:9000 inside the deployment; run `chap-devops components enable ocs --port N` to publish one
error: the object store speaks the S3 API and serves no web interface, so there is nothing a browser can open; run `chap-devops components enable s3 --port N` to publish it for an S3 client of your own, and `chap-devops status` says whether it is running
A component with no host port is reached somewhere else rather than nowhere: see
Component ports. The one exception is an OCS
instance with no host port and a recorded --base-url - the public origin it
builds its links from - which is an address a browser can be pointed at, so
that is what chap-devops open ocs opens, with a line saying it is the proxy and not a
port on this machine. See
Behind a reverse proxy.
A component that is enabled but not running is a warning rather than a refusal, and the page is opened anyway:
opening chap-core's API documentation at http://localhost:8700/docs
warning: no chap container is running, so the page will not load yet; run `chap-devops up` to start this deployment
The container is what decides that, exactly as it does for chap-devops status and
chap-devops doctor. Three things follow from asking docker rather than the port:
- A component whose container is up gets one request to its address, with a
three-second limit. The closing line says
answered at that addressordid not answer yet.chap-devops statusis the full check. - A component with no container gets the warning above. It is a warning and
not a refusal because the browser reloads: the tab is already at the right address
when
chap-devops uphas finished, and DHIS2 in particular takes minutes to answer after its container starts. - A machine where docker cannot be asked at all - no CLI, no daemon - gets
docker could not be asked whether dhis2 is running, so this is the address and not a promise, and the page is still opened. Refusing on a probe that could not be made would refuse everywhere docker is missing, which is not an answer about the component.
On a server with no browser
The URL is handed to open on macOS, start on Windows and xdg-open
elsewhere, spawned and not waited for. A machine with none of them - a server
over ssh, which is where chap-devops mostly runs - is told the address rather than
that something failed, and the exit code stays 0:
the DHIS2 user interface is at http://localhost:18080
With -v, a hint names the opener that is missing:
hint: there is no `xdg-open` on this machine to open it with, so the address above is the whole of it
--no-browser asks for that answer on a machine that does have an opener: the
address and the notes, and no browser window. A script or an AI agent working
for someone at another screen wants this, because a window appearing on the
machine the command runs on helps nobody:
$ chap-devops open ocs --no-browser
the OCS web interface is at http://localhost:8790
chap-devops open NAME --json carries url, page, opened, no_browser, running
and the notes, and messages with every closing line and its level. That is
the shape to read from a script, and it never opens a
browser: no_browser is always true under --json. chap-devops open --json carries
the listing under components.
From the browser
o on the browser’s components page does the same thing for the row under the
cursor, and Open web interface in the palette does it by name. It opens what
this deployment publishes now, so a component enabled in this session, or one
whose port was changed and not yet saved, is said in the footer rather than
opened: nothing is running behind an address chap-devops sync has not written yet.
The browser reads no .env, so CHAP_ROOT_PATH is chap-devops open’s to honour and
not the page’s. See The components page.
OCS
The ocs component runs ghcr.io/dhis2/open-climate-service. OCS publishes no
release tags yet, so the pin follows the moving main tag; OCS_IMAGE_TAG in
.env pins a build of your own, and chap-devops update reports which of the two is
in force. The image is multi-arch, so no platform: pin is needed and an arm64
host runs it natively - unlike chap-core and the model images, which are amd64
only. It ships its own HEALTHCHECK, so the compose file declares none.
OCS serves a web interface as well as an API, so it is published on a host port
by default (8790). chap-core reaches it inside the deployment at
http://ocs:9000, on the compose default network, which is what makes OCS
usable as a data source for Chap: OCS’s own example configuration carries a
commented-out chirps-to-chap workflow trigger for exactly that, and the file
scaffolded here leaves it out, so adding it is yours.
There is no depends_on between OCS and chap-core. They are independent
services and either one is useful without the other.
ocs/climate-service.yaml
Each OCS instance is configured for one country or region. chap-devops scaffolds
that file when the component is first enabled. From that moment it is yours:
chap-devops changes only its read_only and plugins_dir keys, and leaves every
other line as you wrote it. The first line of the file says the same. It is
mounted read-only at
/app/climate-service.yaml, which is where CLIMATE_SERVICE_CONFIG points.
Without any flags it holds example values for Laos, the country of the DHIS2
demo database, and a note saying so. chap-devops doctor warns while that note is there, so an instance is
never deployed as the wrong country by accident; edit the file and delete the
note.
The three flags fill it in directly, on init and on components enable alike:
chap-devops init mychap --with ocs \
--ocs-name Malawi --ocs-country MWI --ocs-bbox 32.6,-17.2,35.9,-9.3
id: malawi-climate-service
name: Malawi Climate Service
extent:
name: Malawi
bbox: [32.6, -17.2, 35.9, -9.3] # xmin, ymin, xmax, ymax
country_code: MWI
data_dir: /app/data
A western or southern extent starts with a minus sign, and --ocs-bbox takes
it without the =:
chap-devops components enable ocs --ocs-bbox -13.5,6.9,-10.1,10.0
OCS’s own documentation describes every other field, including the scheduler and the workflow triggers: see the instance guide.
Schedules
OCS can sync a dataset again on a schedule. The file only turns the scheduler on:
scheduler:
enabled: true
Make the schedules in the Schedule tab of a dataset in the OCS web UI, or with
POST /schedules/sync. OCS keeps them in schedules.json in its data volume,
so chap-devops backup keeps them too. A read-only instance refuses changes to them.
An older OCS read a dataset_sync: list under scheduler:. The current OCS
refuses that list, and does not start; see
scheduler.dataset_sync is no longer supported.
Compute limits
Three variables in .env limit what OCS does at the same time. chap-devops passes
each one only when .env sets it; else OCS uses its own default:
| Variable | Default | What it limits |
|---|---|---|
CLIMATE_SERVICE_MAX_CONCURRENT_JOBS | 2 | Ingestion, sync, export and openEO jobs that run at the same time. The others wait in the queue. |
DASK_NUM_WORKERS | half the cores | The threads of every dask computation. |
TOKIO_WORKER_THREADS | one per core | The threads that read and write the data store. |
On a machine with 8 GB of memory or less, set
CLIMATE_SERVICE_MAX_CONCURRENT_JOBS=1. Then run chap-devops restart ocs.
Data source credentials
Two of the dataset families OCS can ingest need an account. WorldPop and CHIRPS3 need nothing at all, so a deployment that only wants those needs none of this.
| Source | Variables | Datasets |
|---|---|---|
| Copernicus Climate Data Store | ECMWF_DATASTORES_URL, ECMWF_DATASTORES_KEY | ERA5-Land monthly, and the recent tail of the daily ones |
| Earth Data Hub | EDH_API_KEY | ERA5-Land daily, and the 1991-2020 day-of-year normals |
| Copernicus Data Space Ecosystem | CDSE_S3_ACCESS_KEY, CDSE_S3_SECRET_KEY | the CLMS GPP dataset plugin |
| WorldPop, CHIRPS3 | none | population, precipitation |
The two ERA5-Land rows are not alternatives. The monthly datasets come from the Climate Data Store alone and the day-of-year normals from the Earth Data Hub alone, but every daily dataset reads both, choosing per period: the Earth Data Hub store for the history, and the Climate Data Store for the roughly four weeks the hub has not published yet. So a daily ingestion that reaches into the present needs both accounts, while one that stops short of the hub’s coverage needs only the hub.
chap-devops writes the five as commented placeholders into .env when the ocs
component is enabled, and appends the section to a .env that does not have it
yet - it never rewrites the file, exactly as with the image pins:
# OCS data sources (optional): ERA5-Land needs one or both of ECMWF_DATASTORES_*
# and EDH_API_KEY, per dataset; WorldPop and CHIRPS3 need none.
# ECMWF_DATASTORES_URL=https://cds.climate.copernicus.eu/api
# ECMWF_DATASTORES_KEY=
# EDH_API_KEY=
# CDSE_S3_ACCESS_KEY=
# CDSE_S3_SECRET_KEY=
Uncomment the ones you have, fill them in, and run chap-devops up: a container
reads .env when compose creates it, so a running OCS does not pick up a
credential that was added after it started.
compose.ocs.yml passes all five unconditionally, as ${VAR:-}. That is safe
because each of them is read as os.getenv(...) or <a credentials file>, so a
variable that arrives empty is one nothing has: a deployment that sets none of
them behaves exactly as one whose compose file never mentioned them. Which code
does the reading, and which file it falls back to, differs per source:
| Variables | Read by | Falls back to |
|---|---|---|
ECMWF_DATASTORES_URL, ECMWF_DATASTORES_KEY | the ecmwf-datastores-client library OCS calls | ~/.ecmwfdatastoresrc |
EDH_API_KEY | OCS itself | ~/.netrc, for api.earthdatahub.destine.eu |
CDSE_S3_ACCESS_KEY, CDSE_S3_SECRET_KEY | the CLMS GPP dataset plugin, which is yours rather than part of OCS | the [cdse] profile in ~/.aws/credentials |
Nothing in OCS reads the CDSE_S3_* pair - the plugin that does is one you
bring. That plugin reads two more variables of its own, CDSE_S3_PROFILE and
CDSE_S3_ENDPOINT; chap-devops writes neither into .env, and compose.ocs.yml
names only the five above, so neither reaches the container. A plugin that needs
a profile or an endpoint other than its own default has to carry it itself.
Without either the variables or the file, an ERA5-Land ingestion that reaches the
Climate Data Store fails with
No such file or directory: '/home/ocs/.ecmwfdatastoresrc' - that is the
container looking for the file form of the same credential, in the home directory
of the image’s ocs user.
chap-devops auth show reports which of them .env sets, never their values:
OCS data sources
ECMWF_DATASTORES_URL set
ECMWF_DATASTORES_KEY set
EDH_API_KEY unset
CDSE_S3_ACCESS_KEY unset
CDSE_S3_SECRET_KEY unset
No value is shown, whatever --reveal asks for: these are accounts with Copernicus and
Earth Data Hub rather than this deployment’s own secret, so there is nothing to
paste into a client here - only the question of whether a dataset will ingest.
chap-devops doctor says ERA5-Land: credentials unset (WorldPop and CHIRPS3 work without them) on its components line, as information. It is not a warning: a
deployment with no credentials is a working deployment.
Behind a reverse proxy
A public instance normally sits behind a proxy that terminates TLS, and then two things change. The host port should not be published at all, so the proxy is the only way in, and OCS has to be told its public origin - otherwise it builds its STAC and openEO links from the request it received, which names the internal address:
chap-devops components enable ocs --port none --base-url https://ocs.example.org
chap-devops init mychap --with ocs --ocs-base-url https://ocs.example.org # at creation
The overlay then exposes the container port and publishes nothing:
services:
ocs:
expose:
- "9000"
environment:
CLIMATE_SERVICE_BASE_URL: ${CLIMATE_SERVICE_BASE_URL:-https://ocs.example.org}
It is rendered as a ${VAR:-default}, like the image pin, so the recorded value
is the default and a CLIMATE_SERVICE_BASE_URL line in .env still moves it
without a sync. The URL has to be absolute, with a scheme: OCS refuses a
schemeless value, logs a warning and goes back to composing links from the
request, which is the setting quietly not working.
chap-devops status then shows where the instance actually is:
ocs up internal (proxy: https://ocs.example.org)
--port 9010 publishes it again, and the address is then the address: it is
where this machine reaches it, whatever the proxy is called. --base-url ""
clears the setting.
The command names the change on its own line: set the ocs base URL to URL,
or removed the ocs base URL. A run that gives the value that is already
recorded changes nothing, and says so: ocs was already enabled on http://localhost:8790; nothing changed.
Point the proxy at the container on the compose network. Give it an
allowlist of the open routes rather than a denylist, so a route added in a
later OCS release is not permitted by default, and add request timeouts,
body-size limits and per-IP rate limiting: read-only protects integrity, not
availability, and POST /result is still an unbounded compute endpoint.
Read-only instances
read_only: true in ocs/climate-service.yaml makes OCS refuse every write
over HTTP: ingestion and sync under /manage - POST /manage/ingest and
POST /manage/sync, which are what the data source and dataset pages post their
forms to rather than a console of their own - plus stored process graphs, batch
jobs and operator export delivery. /manage, /jobs and /exports are closed
as whole trees, GET included, so nothing added under one of them in a later
release is open by default. The catalogue, the data endpoints, the map viewer and
synchronous POST /result stay open, and so do the pages themselves, which
simply leave their forms out. This is how a public instance runs.
chap-devops components enable ocs --read-only
chap-devops restart ocs
chap-devops components enable ocs --read-write # and back again
Both flags edit the one read_only key in ocs/climate-service.yaml and leave
every other byte of it alone - your comments, your dataset list, your scheduler
- adding the key at the end when the file does not have it. The value is also
recorded in
components.yaml, andchap-devops statusprintsread-onlynext to the OCS line:
ocs up http://localhost:8790 read-only
OCS reads the file and not the record, and it reads it once at startup, so the
instance has to be recreated. The instance config is a bind mount, which compose
does not compare, so chap-devops restart checks the file itself: written after the
ocs container was created, it recreates that one service and leaves chap-core
and the models alone. When ocs is already on, chap-devops components enable says
so in its closing lines:
ocs is now read-only
run `chap-devops restart ocs` to apply
A second run with the same flag says ocs is already read-only and changes
nothing. Until the restart, chap-devops status warns while
ocs/climate-service.yaml is newer than the ocs container:
warning: `ocs/climate-service.yaml` changed after the ocs container started, so ocs may still use the old settings; run `chap-devops restart ocs` to apply it
Check it after every deploy. read_only is an ordinary config key, so a build
of open-climate-service that predates read-only mode ignores it silently and
serves a fully writable instance:
curl -s http://localhost:8790/info | grep read_only # must be true
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://localhost:8790/ingestions -d '{}' # 403
Read-only applies to HTTP, so ingestion becomes an operator task. Ingest through a throwaway container, which reads the credentials from your shell and never gives them to the running service:
set -a; . ./.env; set +a
chap-devops docker run -- run --rm --no-deps \
-e ECMWF_DATASTORES_URL -e ECMWF_DATASTORES_KEY \
ocs python -c "
from open_climate_service.ingestions.processes import execute_ingestion
execute_ingestion(dataset_id='era5land_temperature_monthly', start='2020-01', end='2024-12')
"
Ingest a whole range in one call. Extending an existing dataset with a second, adjacent ingestion writes the new periods and then fails the coverage check, leaving the registry pointing at the old range.
On a host that runs read-only, keep the credentials out of .env. Read-only
makes them unusable there, so their only effect is to sit in the process
environment of a service that cannot ingest; the throwaway container gets them
from wherever you run it from instead.
Dataset plugins
OCS loads extra datasets from a plugin directory. Put them in ocs/plugins/:
mychap/
ocs/
climate-service.yaml
plugins/
datasets/
clms_gpp.py
clms_gpp.yaml
The directory existing is the whole declaration. chap-devops sync then renders the
mount
volumes:
- ./ocs/climate-service.yaml:/app/climate-service.yaml:ro
- ./ocs/plugins:/app/plugins:ro
and adds plugins_dir: /app/plugins to ocs/climate-service.yaml if the file
does not already name one - a mount OCS is not pointed at is a mount it never
looks in. A plugins_dir you set yourself is never moved. chap-devops doctor
reports the file count on its components line, as information.
The mount is conditional because compose refuses to start a service whose bind
source does not exist, so a project without plugins must not carry the line.
Nothing creates the directory for you, and chap-devops never writes into it: the
plugins are yours, and a plugin with dependencies the image does not have needs
an image of its own.
Adding a plugin to a running deployment
Run chap-devops up, not chap-devops restart. chap-devops up is the only wrapper that
re-renders the compose files before it calls Docker; chap-devops restart,
chap-devops logs, chap-devops docker ps and the rest run against the files exactly as
they are on disk. So on a deployment where ocs was enabled before
ocs/plugins/ existed, compose.ocs.yml carries no plugin mount, and a
chap-devops restart finds a container that already matches that file: nothing
happens at all - no error, no mount, no plugin.
mkdir -p ocs/plugins/datasets
cp clms_gpp.py clms_gpp.yaml ocs/plugins/datasets/
chap-devops up # syncs first, so the mount is rendered and then ocs is recreated
chap-devops sync followed by chap-devops restart is the same thing in two steps: once
the mount is in the file, the running container no longer matches it and compose
recreates it.
This is the one place where chap-devops restart ocs is the wrong reach. That
belongs to ocs/climate-service.yaml, and for the
opposite reason: that file is a bind mount, so an edit to it is already visible
inside the container and there is nothing to re-render - the only problem is
that OCS read the old text at startup, which recreating that one service fixes. A directory that did not exist at render time is the
other case entirely: the compose file itself is out of date, and no amount of
recreating mounts a directory it does not mention.
chap-devops doctor is the safety net, because its compose files line is
chap-devops sync --check:
warn compose files out of date with .chap-devops/ (2 to write, 5 unchanged, 0 to remove)
run `chap-devops sync`, or `chap-devops up`, which syncs first
Two files to write, because the mount in compose.ocs.yml and the plugins_dir
key in ocs/climate-service.yaml are rendered by the same sync.
The object store
OCS will soon require an S3-compatible object store. It does not read any S3_*
variable yet, so chap-devops does not force one on a deployment: enabling ocs
prints a one-line note that the store is coming and that
chap-devops components enable s3 adds it.
When s3 is on, the OCS service gets four forward-looking variables -
S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY and S3_BUCKET - with a comment
in the file saying that OCS does not read them yet. The exact contract is not
published, so those names may still change.
The store itself is RustFS. chap-devops generates S3_ACCESS_KEY and
S3_SECRET_KEY (32 hex characters each) into .env once, and never rewrites
them: the data volume is created with them, and changing them later locks the
store’s own contents away. The compose file substitutes them into the image’s
own RUSTFS_ACCESS_KEY and RUSTFS_SECRET_KEY, so the credentials live in
.env and nowhere else.
A one-shot s3-init service creates the ocs bucket once the store is healthy.
It runs the same RustFS image and signs the request with curl --aws-sigv4, so
nothing extra is pulled: the image is Alpine-based and ships both curl and wget,
which is what the s3 health check uses on GET /health as well. Creating a
bucket that already exists answers 200, so every later chap-devops up runs the
one-shot again and changes nothing.
The store publishes no host port: OCS reaches it at http://s3:9000 on the
compose default network. chap-devops components enable s3 --port 9002 publishes one
for an S3 client of your own. A published port is for a client and not for a
browser: RustFS serves no web interface, so chap-devops open s3 says that rather than
opening an XML error document. See
Reaching a component from a browser.
DHIS2
The dhis2 component is a demo or development DHIS2 and a PostgreSQL of its own,
published on 8780 by default, for a deployment that wants one beside Chap. Chap
is often deployed with DHIS2 and not always, so this is one way to get one rather
than something a deployment needs.
It is the largest component here - four services, three volumes, a mandatory
instance config and a seeded database - and the only one with settings the browser
and the --port flag do not cover. It has a chapter of its own:
- DHIS2, which covers why it brings its own PostgreSQL,
dhis2/dhis.confand what DHIS2 will not start without, the seed dump and the one chance a deployment gets to apply it, the first start taking minutes, the 4 to 5 GB the analytics populate phase wants, and why moving the image tag starts withchap-devops backup.
One thing to read before deploying it:
connecting the Modeling App to Chap
is a step of its own. The app reaches chap-core through a DHIS2 Route with
code: "chap", chap-devops up does not create it, and a seeded instance carries a
chap route from the demo dump that points at somebody else’s server - so it
looks configured and is not. chap-devops dhis2 connect is the command that settles
all of it.
Standalone OCS
--only names the whole component set, and chap-core is in it only when you
name it:
chap-devops init climate --only ocs,s3 # OCS and its object store, no Chap
chap-devops init dhis --only dhis2 # a DHIS2 on its own
--only ocs,s3 is --with ocs,s3 --without chap-core in one flag, and it
cannot be combined with either of them. --only none is no component at all:
a directory to add components to later, or, with --models, model services on
their own. The same set spelled out:
chap-devops init climate --with ocs,s3 --without chap-core
Neither compose.yml nor compose.chap-devops.yml is rendered, and the -f list is
just the component files and the (empty) marketplace umbrella. chap-devops status
leaves out the chap-core line and counts components rather than models in its
closing line; a deployment with nothing running is told that nothing in it is
running, never that Chap is not, because there is no Chap here to be running.
chap-devops doctor judges the deployment by its components alone.
init asks GitHub nothing about chap-core for such a deployment: no release is
looked up and no compose.ghcr.yml is downloaded. The commands that only
chap-core can answer (chap-devops jobs, chap-devops api without --url,
chap-devops models test, and chap-devops update --chap-tag or --pin-chap-core) refuse
with one line naming chap-devops components enable chap-core. chap-devops update on its
own still pulls the components’ images. chap-devops backup create has no database to
dump and says so on its database line. Turning chap-core on later renders
compose.yml from the copy built into chap-devops and says so;
chap-devops update --pin-chap-core then moves it to the newest release.
Model services without chap-core
A model service does not need chap-core to run. With chap-core in the deployment it registers there and is reached through it; without chap-core it runs on its own and is reached on a host port of its own:
chap-devops init multistep --only none --models chapkit_simple_multistep_model
cd multistep
chap-devops up
curl http://localhost:5001/health
open http://localhost:5001/docs
What changes for a model when the deployment has no chap-core:
- Its overlay registers nowhere.
SERVICEKIT_ORCHESTRATOR_URLand the registration key are left out, and servicekit skips registration when the URL is unset. The overlay also stops waiting forchapindepends_on. See What an overlay contains. - It gets a host port by default, the lowest free one from 5001 up, because
nothing else in the deployment can reach it.
--port Npicks one,--port noneasks for none (the service is then only reachable from other containers on the compose network). chap-devops statusasks the model itself. Each row is judged by its container and then byGET /healthon its host port:up,running, not answeringornot running.chap-devops doctorcounts the same rows.- The chap-core commands refuse:
chap-devops models testandchap-devops jobsrun models through chap-core, so they namechap-devops components enable chap-core.
It works in both directions on an existing deployment. chap-devops components disable chap-core under enabled models keeps them, publishes a host port for each one
that had none, and says where each one now answers. chap-devops components enable chap-core puts the orchestrator URL back, and the models register on the next
chap-devops up; the host ports they were given stay until you
chap-devops models unexpose them.
init enables no model unless --models names one, with or without
chap-core. Name the ones you want with --models ID,....
A chap-core elsewhere
A deployment can use a chap-core it does not run: one started from a chap-core checkout on this machine, another compose project, or a server. Its model services then register there, and the chap-core commands talk to it.
chap-devops init models --chap-core-url http://localhost:8000 --models chapkit_simple_multistep_model
chap-devops components enable chap-core --url http://localhost:8000 # on an existing one
chap-devops components enable chap-core --url http://localhost:8000 --models-host host.docker.internal
.chap-devops/components.yaml records it as its own block, and chap-devops components list shows chap-core as external, at that URL:
chap-core:
enabled: false
chap-core-external:
url: http://localhost:8000
models_host: localhost
-
The overlays register there.
SERVICEKIT_ORCHESTRATOR_URLis the URL with a loopback host (localhost,127.0.0.1) turned intohost.docker.internal, because inside a container the loopback is the container. Each overlay maps that name to the host gateway, which Docker Desktop does anyway and Linux needs. -
chap-core calls the models back at
models_hostand their host port, throughSERVICEKIT_HOSTandSERVICEKIT_PORT. Where the app listens in its container depends on the image, and chap-devops reads that off the image’s command when it enables the model:- An image that starts uvicorn with
--port 8000(every marketplace model but one) listens on 8000, so the host port maps to 8000. servicekit 3, which these images have, checks 8000 before it registers. - An image whose command names no port reads
PORT(the Simple Multistep model). Its servicekit 2 checks onlySERVICEKIT_PORT, so chap-devops setsPORTto the host port and maps that port on both sides.
Every model gets a host port, as in any deployment without chap-core of its own.
localhostis right for a chap-core process on this machine; a chap-core in a container needs--models-host host.docker.internal, and one on another machine the name it reaches this one by.initandcomponents enablepickhost.docker.internalthemselves when a running container publishes the URL’s port, and say so.chap-devops statuschecks the way back through chap-core’s proxy and shows a model it cannot reach asregistered, unreachable. - An image that starts uvicorn with
-
chap-devops status,jobs,apiandmodels testuse that URL in place ofhttp://localhost:<API port>.chap-devops statusshows only the version that chap-core reports. If chap-core does not answer, the error line says that this deployment does not run it, so start it there, or set another URL withchap-devops components enable chap-core --url URL.
It is one or the other: components enable chap-core --url is refused while
this deployment runs its own chap-core (disable that first, which stops it),
components enable chap-core without --url forgets the external one and runs
chap-devops’s own again, and components disable chap-core forgets it too.
What the other commands say
chap-devops upchecks the components’ host ports in its preflight, alongside the API port, and names the command that moves each one.chap-devops statusprints one line per enabled component under the chap-core line, each judged by its container first, exactly aschap-devops doctordoes: a component with no container readsnot running, and one that has a container is then asked whatever it answers - OCS its/healthendpoint, and only while that container is up. That gate is what keeps a stopped OCS from being reportedupbecause another process answered on its host port: OCS defaults to 8790, so two deployments on one machine collide there, which is the clash theuppreflight andchap-devops doctoralready warn about. An OCS instance with no host port cannot be asked from out here at all, so its container is the whole answer, and the line saysinternal (proxy: <base_url>)andread-onlywhere those apply. An OCS instance that answered also reports what it holds,3 datasetsand212.0 MB data. The rows come from.chap-devops/components.yamlrather than from docker, so they are printed with nothing running too, each readingnot runningbeside the address it will answer on.--jsoncarries the rows undercomponents, withread_only,health_url,datasetsanddata_bytes. Adhis2row is judged the same way and then asked one thing,/api/ping: a DHIS2 container can be up and the instance entirely broken, with Tomcat serving pages while every/api/*request answers 404, so a row whose ping goes unanswered readsstartingrather thanup. It carries no version, because no component row does and DHIS2 gives its own only to a logged-in session. Seechap-devops status.chap-devops doctoradds acomponentsline - which components are on, whether the OCS config is present and still the example, whether any data source credential is set, how many plugin files there are and, while the instance is up, what it holds - plus a port line per published component and an image line per component image. Those last facts ride on the warning about an unedited config as much as on anokline: a deployment that has just enabledocsstill has the example config, and that is the run in which a missing ERA5-Land key is most worth reading. Fordhis2the same line reports whetherdhis2/dhis.confis there - afailwhen it is not, because DHIS2 throws on startup without it - and which seed this deployment is set to, including the case where the setting isdefaultand chap-devops knows no dump for the pinned minor line, which is the one to read before a firstchap-devops upbrings up an empty DHIS2. A deployment withdhis2enabled also gets amemoryline among the machine checks, measured as what docker says it can give a container rather than as the host’s memory. Thedeployment filesline names the component directories it does not count, since these facts are what covers them.chap-devops auth showadds anOCS data sourcesblock, set or unset, never the values. The two DHIS2 secrets are not in it: they are this deployment’s own, in.env, andDHIS2_ENCRYPTION_PASSWORDis one the database was created with rather than something to paste anywhere.chap-devops updatereports each component’s image.ocsands3follow moving tags, so there is no pin to move, only a pull:docker compose pulltakes whatever the tag points at today. An activeOCS_IMAGE_TAGorS3_IMAGE_TAGline in.envis your own pin, and is reported as such.dhis2is pinned to a minor line, which means the same tag is a newer patch release tomorrow: a pull moves it without anything on the screen changing, and a newer DHIS2 migratesdhis2_dbirreversibly on the nextchap-devops up. A run that would re-pull it while that volume is already on the machine warns and nameschap-devops backup createfirst; see Changing the DHIS2 version. When the pull brings an image the machine did not have, the closing line names the component andchap-devops restartis what puts it in service.chap-devops backuparchives each component volume that holds state:ocs_data,s3_dataanddhis2_home. The DHIS2 database is apg_dumpwithout the analytics tables, not a copy ofdhis2_db; see Backing it up.dhis2_dumpis left out on purpose - it is a download cache the one-shot refills - so no archive carries the seed dump. See Backup and restore.chap-devops uihas a components page beside the models one:Tabmoves between them, the rows are the oneschap-devops components listprints, andspace,pandPdo there what they do for a model.oopens the row’s web interface, the same pagechap-devops openopens, and says in the footer when there is none. Onessaves both pages. The settings it deliberately does not edit are named in each component’sioverlay rather than hidden: OCS’s--base-urland--read-only, because they write toocs/climate-service.yaml, which is yours; and DHIS2’s seed, whichchap-devops components enable dhis2 --seed SPECchanges.von thedhis2row picks the DHIS2 version. See The components page.
DHIS2
The dhis2 component runs a DHIS2 instance and a PostgreSQL of its own beside
Chap, for a deployment that wants one.
chap-devops init mychap --with dhis2
chap-devops components enable dhis2 # or afterwards
chap-devops dhis2 connect # once DHIS2 answers, so the Modeling App can
# reach Chap
What this component is for
A demo or a development DHIS2: something to log into while trying Chap out,
developing against it, or showing it. It is not a way to run DHIS2 in
production, and that is upstream’s own judgement of the images rather than a
limitation of chap-devops - docker/DOCKERHUB.md in dhis2/dhis2-core says “We
cannot recommend the images for use in production”. A production DHIS2 is
deployed by other means, and a Chap deployment can be pointed at one without
this component: see Connecting the Modeling App to Chap
for what the connection is made of, all of which is requests to DHIS2 that you
can make against an instance chap-devops did not deploy.
Chap is often deployed with DHIS2 and not always. A deployment that already
has a DHIS2, or does not want one, leaves this component off and loses nothing:
chap-core, the models and OCS do not depend on it, and there is no depends_on
between DHIS2 and anything else here. This component is one way to get a DHIS2,
for the deployments that want one alongside.
Four services and three volumes
chap-devops sync renders compose.dhis2.yml:
| Service | What it is |
|---|---|
dhis2 | DHIS2 itself, from dhis2/core, published on 8780 by default. Multi-arch since 2.39.1-rc, so nothing pins a platform for it. |
dhis2-db | Its PostgreSQL, from postgis/postgis:16-3.5. The image is amd64-only, so the service pins linux/amd64. exposed on 5432 and never published. |
dhis2-dump | A one-shot that prepares the seed dump and exits. Not rendered at all for a deployment that starts empty. |
dhis2-prep | A one-shot that runs on every start, after the database is healthy and before DHIS2 boots. |
| Volume | What it holds |
|---|---|
dhis2_home | /opt/dhis2: the installed apps and the file store, about 429 MB on a real instance. Without it both go with every recreate. |
dhis2_db | The database’s data directory. |
dhis2_dump | The prepared dump - a download cache, not data. The one-shot fetches the dump again when the volume is empty. |
dhis2-prep does two things nothing else does, neither of them visible from
outside:
ANALYZE. A freshly restored dump carries no planner statistics, and the analytics aggregations then get terrible query plans - measured here, a first analytics run of about 20 minutes instead of about 20 seconds. Autovacuum gets there eventually, but not before that first run.- Reset any job a hard stop left flagged
RUNNING. DHIS2 will not start a new analytics run while one isRUNNING, so a single stale row blocks analytics for good.
It runs under ON_ERROR_STOP=0, so a database DHIS2 has not migrated yet - one
with no jobconfiguration table - is a no-op rather than a failure.
Its own database, and why it is not chap-core’s
Three reasons, and the first is enough on its own.
- DHIS2 needs PostGIS. Its own startup check throws without the
postgis,pg_trgmandbtree_ginextensions, so the image is a PostGIS one rather than plain PostgreSQL:postgis/postgis:16-3.5, from the PostGIS project. - chap-core deploys
postgres:17, which has none of the three. - A component could not reach it anyway. Upstream’s
compose.ymlputs the database on a second network, off the compose default one, which is exactly what keeps a model service away from it. A component’s services join the default network like a model’s do.
The two databases are therefore separate containers with separate volumes and
separate passwords, and DHIS2_DB_PASSWORD in .env has nothing to do with the
Chap one.
The health check on dhis2-db probes over TCP (pg_isready -h 127.0.0.1) on
purpose: while the entrypoint runs its init scripts the server listens on the
unix socket alone, so a socket probe would report a half-restored database as
ready and DHIS2 would start against half a database.
dhis2/dhis.conf
DHIS2 loads this file at startup and will not start without it. There is no
environment-only mode: a missing file is a startup failure, not a fallback to
defaults. So chap-devops scaffolds it when the component is first enabled, mounts it
read-only at /opt/dhis2/dhis.conf, and never rewrites it - it is yours from
that moment on, exactly like
ocs/climate-service.yaml. chap-devops sync re-creates it only if it has gone missing, which is worth doing precisely
because nothing starts without it.
What DHIS2 does with ${...} in that file is substitute the environment
variable of that name, so the values live in the compose file and the file
itself carries placeholders. The dhis2 service passes five:
| Variable | Default in the compose file |
|---|---|
DHIS2_DB_HOST | dhis2-db |
DHIS2_DB_NAME | dhis |
DHIS2_DB_USER | dhis |
DHIS2_DB_PASSWORD | dhis, overridden by the generated value in .env |
DHIS2_ENCRYPTION_PASSWORD | empty, overridden by the generated value in .env |
The last is passed even when nothing sets it, so the placeholder in the file
becomes the empty string rather than standing there as a literal ${...}.
Applying an edit
chap-devops restart dhis2
DHIS2 reads this file once, at startup, and chap-devops mounts it as a bind
mount, which compose does not compare: docker compose up -d alone finds
nothing to do after an edit. chap-devops restart checks the file itself - when it
was written after the dhis2 container was created, that container is
recreated, and the line says so:
recreating dhis2 to apply its edited config file
If the file’s modification time does not show the edit (a copy that kept an
older one), chap-devops restart --all dhis2 recreates the container regardless.
DHIS2 migrates before it serves a request again, so it is not back the moment
compose returns; the next chap-devops dhis2 command waits for it and says what it
found. This is the same trap, for the same reason, as
ocs/climate-service.yaml.
encryption.password
24 characters or more. DHIS2 encrypts stored credentials with it, and it judges the length itself:
- shorter than 24, and it reports
ENCRYPTION_PASSWORD_TOO_SHORTand stops; - empty, and it reports
MISSING_ENCRYPTION_PASSWORDand encrypts with a password compiled into DHIS2’s own source - which every other DHIS2 in the world also has.
chap-devops generates 32 hex characters into .env as DHIS2_ENCRYPTION_PASSWORD,
comfortably past the floor. Like the database password it is written once and
never rewritten: the volume was created with it, and changing it later leaves the
credentials already in the database undecryptable.
route.remote_servers_allowed
This is the one value chap-devops fills in rather than leaving as a placeholder:
route.remote_servers_allowed = http://*,https://*
DHIS2 version 42 and later default it to https://* and then refuse an
http:// target. The target the Modeling App needs is usually exactly that: the
app does not reach chap-core directly, it reaches it through a DHIS2 Route, and
chap-core is http://chap:8000 inside the deployment,
http://host.docker.internal:8000 when it runs on this machine
(--chap-core-url), and plain http on most lab servers. So the default value
would refuse the one route this component exists to make possible.
The scaffold allows every http and https target, so any chap-core works without touching the file. A DHIS2 chap-devops deploys is a development or evaluation instance; DHIS2 logs a warning about the wildcard on every start, and on a DHIS2 that is not a test instance the line belongs narrowed to the origins in use. One origin per entry, comma separated, and none of them may carry a path - DHIS2 throws on startup if one does.
Behind a TLS-terminating proxy
Two lines are in the file, commented:
# server.https = on
# server.base.url = https://dhis2.example.org
The first marks the session cookie Secure, the second is the origin DHIS2
builds absolute links from. Uncomment both behind a proxy that terminates
TLS, and leave both commented while DHIS2 is reached over plain http - a Secure
cookie on an http origin makes the login loop.
Pair that with --dhis2-port none (or chap-devops components enable dhis2 --port none), so the proxy is the only way in. chap-devops records no second copy of
server.base.url: that file is yours, so chap-devops status and chap-devops components list say internal for such an instance rather than naming a proxy.
Restoring an older dump
Two more commented lines cover the case where the seed dump comes from an older DHIS2 than the image runs, which can stop on Flyway’s migration order:
# flyway.repair_before_migration = on
# flyway.migrate_out_of_order = on
The seed
seed: in .chap-devops/components.yaml decides what the database is restored from
the first time it is created. --dhis2-seed takes the same four answers at
init:
| Value | What happens |
|---|---|
default | The demo dump published for the pinned minor line, if chap-devops knows of one. |
none | Nothing. The database starts empty and DHIS2 migrates a new schema into it. |
| a URL | That dump, downloaded by the one-shot. An http:// or https:// prefix is the whole of the rule. |
| a path | That file, bind-mounted into the one-shot and copied rather than downloaded. |
chap-devops init mychap --with dhis2 # the version-matched demo
chap-devops init mychap --with dhis2 --dhis2-seed none # empty
chap-devops init mychap --with dhis2 --dhis2-seed dumps/laos.sql.gz
chap-devops init mychap --with dhis2 --dhis2-seed https://example.org/mine.sql.gz
After init, chap-devops components enable dhis2 --seed SPEC changes it. SPEC
takes the same values as --dhis2-seed. The seed applies only when dhis2_db
is created.
A path is relative to the deployment directory, or absolute. It becomes a bind
mount, and compose refuses to start a service whose bind source does not exist,
so chap-devops sync warns when the file is not there, before chap-devops up fails. A
relative path keeps the deployment directory self-contained: copy the dump into
it.
chap-devops init reads the version of the dump only from the deployment
directory, because that is the file the restore uses. If init makes the
directory, the file is not there yet, so init does not read its version and
warns:
laos.sql.gz is not in /srv/laos, so chap-devops did not read its DHIS2 version, and DHIS2 runs 2.42
...
warning: the dhis2 seed names laos.sql.gz, which is not in /srv/laos; copy the dump there before `chap-devops up`, or run `chap-devops components enable dhis2 --seed none`
To let init read the version, make the directory and copy the dump into it
first:
mkdir laos
cp laos.sql.gz laos/
chap-devops init laos --only dhis2 --dhis2-seed laos.sql.gz
cd laos && chap-devops up
A URL seed under --offline is refused outright: the two ask for opposite
things.
It applies once, to a database being created
The restore is the stock PostgreSQL entrypoint’s own. dhis2-dump writes the
prepared dump into dhis2_dump, and dhis2-db mounts that volume at
/docker-entrypoint-initdb.d/, which the entrypoint reads only while it
initialises a data directory it has just created. So:
- the first
chap-devops uprestores the dump; - every later
chap-devops updoes not, whateverseed:says; - changing the seed on a deployment that has already started means removing
dhis2_dbfirst -chap-devops components disable dhis2 --purgeis the honest way to ask for that, and it takes all three volumes.
Docker copies the image’s own PostGIS init script into that volume as the container is created, so the extensions are still set up, ahead of the dump.
A restore that stops
If the restore stops with an error, the postgres entrypoint stops too. Docker
then starts dhis2-db again, and postgres sees a data directory, so it skips
the init. Without a guard, the result is an empty or incomplete database that
answers like a good one.
So the restore leaves a mark as its last init script, zzz-chap-devops-seeded.sql:
the comment chap-devops: seed restored on the database. On a seeded deployment, the
health check of dhis2-db asks for that mark. A restore that stopped has no
mark, so dhis2-db stays unhealthy, DHIS2 does not start, and chap-devops up
prints the lines of the log that say why. Here the dump had a statement that
PostgreSQL could not run:
dependency failed to start: container mychap-1ab2c3-dhis2-db-1 is unhealthy
why dhis2-db is unhealthy:
2026-10-08 04:44:18.888 UTC [71] ERROR: function this_is_not_a_function() does not exist at character 8
ERROR: function this_is_not_a_function() does not exist
the database has no mark `chap-devops: seed restored`, so its seed restore did not finish
the restore of the DHIS2 seed stopped before the end, so `dhis2_db` is incomplete; fix the cause above, then run `chap-devops components disable dhis2 --purge`, `chap-devops components enable dhis2` and `chap-devops up`
error: docker compose exited with status 1
This takes about 15 minutes. The health check of dhis2-db has a start
period of 600 seconds and then 30 checks 10 seconds apart, and chap-devops up
prints nothing new until compose gives up. While it waits, chap-devops status
says dhis2 starting, because dhis2-db runs and DHIS2 waits for it. To see the error sooner, run chap-devops logs dhis2-db
in a second shell. The error is there seconds after the restore stops.
chap-devops backup restore sets the mark again after it loads the pg_dump of the
database, so a database that it puts back is healthy too. See
Backing it up.
The version of the dump
DHIS2 runs a database of its own minor version or older, and chap-devops supports
DHIS2 2.41 and newer, because the Modeling App needs it. The version of a dump
is in its Flyway table: each migration that DHIS2 ran is a row of
flyway_schema_history, and the newest one says the version. 2.42.54 is
migration 54 of DHIS2 2.42. The last number is not the patch release.
- For a dump on disk,
chap-devops initreads the version. It looks for the path only in the deployment directory. If the dump is not there,initsays<path> is not in <dir>, so chap-devops did not read its DHIS2 version, and DHIS2 runs <tag>. A dump of 3.9 GB takes about 20 seconds, because the table comes afterdatavalue.chap-devops components enable dhis2 --seed PATHdoes not read it.- Without
--dhis2-tag, DHIS2 runs the version of the dump. - A
--dhis2-tagolder than the dump is refused. - A dump older than 2.41 is refused.
- Without
- A
--dhis2-tagor--tagolder than 2.41 is refused, also without a dump:--tag 2.40 is older than DHIS2 2.41, the oldest version chap-devops supports; use `--tag 2.41` or newer. - For a URL, the dump step reads the version after the download, before the
rewrite, and stops with the same messages.
chap-devops upthen prints the error line of the dump step underwhy dhis2-dump failed:, with the command that fixes it.
$ chap-devops init laos --only dhis2 --dhis2-seed laos.sql.gz
reading the DHIS2 version of laos.sql.gz from its Flyway table; a large dump takes about a minute
laos.sql.gz is DHIS2 2.42 (Flyway migration 2.42.54), so DHIS2 runs 2.42
A URL dump that is newer than the tag stops in the dump step, and chap-devops up
says why:
service "dhis2-dump" didn't complete successfully: exit 1
why dhis2-dump failed:
error: DHIS2 2.41 is older than the dump (DHIS2 2.42), and DHIS2 does not run a newer database; run chap-devops components enable dhis2 --tag 2.42, then chap-devops up
the dump is DHIS2 2.42, and DHIS2 does not run a newer database; run `chap-devops components enable dhis2 --tag 2.42`, then `chap-devops up`
error: docker compose exited with status 1
chap-devops logs dhis2-dump shows the whole log of the step. To set the tag, run
chap-devops components enable dhis2 --tag 2.42, then chap-devops up.
Values that analytics cannot read
A real DHIS2 database has data values that look like numbers but do not fit in
the number type of analytics (double precision, at most about 1.8 × 10^308).
An example is a value of 2,081 digits. DHIS2 accepted it when someone typed it
in or imported it. Analytics casts every value of a numeric data element, so
one such value stops every analytics run with out of range for type double precision.
So every restore of a seed runs zz-chap-devops-clean.sql after the dump. The
script does two things:
- It lists each such value in the log of
dhis2-db: the data element, the period, the organisation unit, the first digits and the length. - It empties the value (
value = NULL). It does this for deleted values too, because the outlier query of analytics reads them as well.
$ chap-devops logs dhis2-db | grep -A4 "cannot read as a number"
chap-devops: data values that analytics cannot read as a number, emptied:
dataelement | name | period | orgunit | value_starts | length
-------------+-------------+------------+-------------+--------------+--------
BW7mJYKnkks | SBCC - VF1 | 2023-05-01 | Kh35DvuCtnl | 999999999999 | 2081
This changes the data of the restored copy, not the dump. Only values that analytics cannot read change. Correct the values at their source too.
One password for every user
A dump of a real DHIS2 has the passwords of its real users. To log in as any of them, give every user one password at the restore:
chap-devops init laos --only dhis2 --dhis2-tag 2.42 --dhis2-seed laos.sql.gz --dhis2-seed-password
cp laos.sql.gz laos/
Without a value, the password is district. The option writes
seed_password: in .chap-devops/components.yaml:
dhis2:
seed: laos.sql.gz
seed_password: district
The restore then does four things to every row of userinfo:
- It sets the password. PostgreSQL makes the bcrypt hash with
pgcrypto, one hash for all the users. A hash for each user takes minutes on a large dump. - It turns on the account (
disabled = false). - It turns off two-factor login (
twofactortype,secret). A password alone does not log in a user who has it. - It removes the expiry date of the account (
accountexpiry).
The last two run only when the column is in the dump, because the columns change between DHIS2 versions. Use this for a test or a development copy of a database, not for a server that real users log in to.
The dhis2-dump one-shot writes the SQL to zz-chap-devops-passwords.sql in the
dhis2_dump volume. The postgres entrypoint runs the files of that volume in
name order, so the file runs after dump.sh, which restores the dump. When the dump did not have
the pgcrypto extension, the file removes it again.
The rules of the seed apply:
- It applies once, when
dhis2_dbis created. To apply it to a database that exists, runchap-devops components disable dhis2 --purgefirst. - It needs a seed.
--dhis2-seed nonehas no users, so the two options together are refused. - To change or remove it later, edit
seed_password:and runchap-devops sync.DHIS2_SEED_PASSWORDin.envalso sets it.
The password is plain text in .chap-devops/components.yaml and in
compose.dhis2.yml. Use it for a known password such as district, not for a
secret. chap-devops dhis2 uses it for every user, so chap-devops dhis2 analytics --user em logs in as em with no other setting. A password in .env or in the
environment still comes first.
The default dump comes from a table, not from the tag
The published filename does not follow from the version:
| Minor line | Dump |
|---|---|
2.42 | https://databases.dhis2.org/climate/laos/2.42/laos.sql.gz |
The 2.41 line used to have a dump as well; it is no longer published, so a 2.41
deployment starts empty unless seed: names one. DHIS2’s Sierra Leone demo is
published for 2.41, 2.42 and 2.43
(https://databases.dhis2.org/sierra-leone/<version>/dhis2-db-sierra-leone.sql.gz),
without the climate data the Modeling App uses.
Published paths have differed in patch version and basename from one line to
the next, so chap-devops keeps a table keyed by the minor line (2.42, 2.42.1 and 2.42.1.1
are all 2.42) rather than building a URL from the tag, because a constructed
one would download a 404 on the first start and leave an empty DHIS2 with no
explanation.
A minor line the table does not list starts empty and says so, on every sync:
warning: chap-devops knows no DHIS2 demo dump for 2.41, so a new `dhis2_db` starts empty; name one with `chap-devops components enable dhis2 --seed URL` (a URL or a path)
What the one-shot does to the dump
The one-shot dhis2-dump downloads or copies the dump and keeps it as
seed.dump.gz in the dhis2_dump volume. The download goes to raw.part
first, and one mv makes it the seed, so a download that stopped is never
kept. The name does not end in .sql.gz, so the PostgreSQL entrypoint does not
load it by itself.
The restore is one pass. The one-shot writes dump.sh, and the entrypoint runs
it in name order, before the zz- scripts:
gunzip -c seed.dump.gz | sed -E -f chap-devops-rewrite.sed | psql -v ON_ERROR_STOP=1
The dump is read once, with the gzip and sed of the PostgreSQL image. On a
dump of 3.9 GB, the old way took 24 to 40 minutes: three reads with the busybox
tools of the one-shot, then a fourth read for the restore. The entrypoint runs
dump.sh with pipefail, so a truncated dump stops the restore, and the
mark reports it.
chap-devops-rewrite.sed changes the dump so that it loads into a new database:
- It retrofits
--if-exists. Published dumps arepg_dump --cleanwithout--if-exists, so they open onDROPandALTERstatements that assume the schema is already there, and the restore runs underON_ERROR_STOP=1, where the first missing object stops it. The rewrite is idempotent and anchored to the start of each line, soCOPYdata rows stay as they are.DROP EXTENSIONandDROP SCHEMAgo entirely: the PostGIS image owns those objects. - It removes what this PostgreSQL does not know. A dump of
pg_dump17.6 or 16.10 and later opens with\restrictand closes with\unrestrict, and a dump of PostgreSQL 17 setstransaction_timeout. - It removes the owners and the grants, as
pg_dump --no-owner --no-privilegesdoes. They name the roles of the source server, which do not exist here, so every object belongs to the database user.
An operator’s own dump goes through the same steps: the mounted file is copied to the place where a download goes, and everything after that is the same.
The first start
Minutes, not seconds. DHIS2 migrates its whole schema before it serves a
request, and a seeded deployment restores the dump before that. Every other
service in a Chap deployment answers in seconds, so an operator who does not know
this reads the first chap-devops status as a broken deployment. With -v, both
chap-devops init --with dhis2 and chap-devops components enable dhis2 say so, and
chap-devops logs dhis2
is where the migration shows.
The honest budget, measured:
| Shape | Roughly |
|---|---|
| Native arm64, empty database | 40 seconds |
| Under emulation (an amd64 image on arm64, or the other way round) | 8 to 15 minutes |
| Seeded | the above, plus the restore: about 10 seconds for the climate demo (7 MB), much longer for a national dump |
The health check allows for that: /api/ping - the one route that answers
unauthenticated - with a start_period of 240 s and 60 retries, and 600 s on
dhis2-db, because on a seeded deployment the database’s first start is the
restore.
Memory
DHIS2 wants roughly 4 to 5 GB for the analytics populate phase. Below that
the JVM is SIGKILLed part-way through the populate, and the failure looks like
anything except memory: a container that exits with no Java exception in the log,
an analytics run that never finishes, a DHIS2 that was healthy a minute ago and
is now gone. On Docker Desktop the limit to raise is the VM’s, not the container’s.
The heap is passed as JAVA_TOOL_OPTIONS, not JAVA_OPTS: it is the JVM’s own
variable and works for both image variants, while the distroless one ignores
JAVA_OPTS and then sizes the heap at about a quarter of the machine’s memory.
The default is in the compose file and commented into .env:
# DHIS2_JAVA_TOOL_OPTIONS=-Xms2g -Xmx4g -XX:+UseG1GC
Uncomment and edit that line to change it; the variable replaces the options rather than adding to them, so write the whole set.
Changing the DHIS2 version
DHIS2 migrates its schema forward only, and that makes the image tag the one setting here worth being careful with:
- a newer image migrates the database on the way up, irreversibly;
- an older image against a migrated database gives an instance that answers its own health check while every API request 404s. Up, and wrong - the worst shape a deployment can be in.
So: run chap-devops backup create first. Then move the tag deliberately, either in
.chap-devops/components.yaml followed by chap-devops sync:
dhis2:
enabled: true
port: 8780
image_tag: '2.41'
image: dhis2/core
seed: default
at creation with chap-devops init --with dhis2 --dhis2-tag 2.41, later with
chap-devops components enable dhis2 --tag 2.41 (and --image for another
repository), or in the browser:
chap-devops ui, Tab to the components page, v on the dhis2
row. It offers the minor lines chap-devops has a demo database for (and the version in
force, if that is another), says the forward-only rule as soon as you pick one
that differs, and warns again on save when dhis2_db is already there. Or with
an active DHIS2_IMAGE_TAG line in .env, which Compose reads last and
which therefore wins without a sync. The rendered service is
dhis2/core:${DHIS2_IMAGE_TAG:-2.42}, so the recorded tag is only the default.
An unreleased DHIS2 comes from another image repository: DHIS2 publishes its
development builds as dhis2/core-dev, with the next version under master.
chap-devops init --with dhis2 --dhis2-image dhis2/core-dev --dhis2-tag master runs
it, and image: in the dhis2 block of .chap-devops/components.yaml is the setting
afterwards. There is no demo database for a development build, so it starts
empty.
The tag is two parts rather than a full version on purpose, so a patch release
arrives with a docker pull rather than an edit.
chap-devops warns when a run moves the tag while dhis2_db is already there. It
compares the tag in the rendered compose.dhis2.yml - what is deployed -
against the one being asked for, not two records against each other:
warning: the DHIS2 image moves from 2.42 to 2.41 and `dhis2_db` is already there: DHIS2 migrates a schema forward only, so run `chap-devops backup create` first - an older image on a migrated database answers healthy while every API request 404s
A move to a newer version gets its own warning, because the migration cannot be undone:
warning: the DHIS2 image moves from 2.41 to 2.42 and `dhis2_db` is already there: the next `chap-devops up` migrates the database to 2.42, and DHIS2 migrates a schema forward only, so run `chap-devops backup create` first - the migration cannot be undone
Moving the seed does not need this care and moving the tag does. So the browser
edits the tag only through the v prompt, which states the forward-only rule.
It does not edit the seed: its i overlay sends you to chap-devops components enable dhis2 --seed SPEC.
Connecting the Modeling App to Chap
A DHIS2 and a Chap started side by side cannot talk, and the reason is worth
understanding before the commands make sense: chap-core never calls DHIS2 and
DHIS2 never calls chap-core. The only thing that talks to both is the Modeling
App running in a browser, and it reaches chap-core through DHIS2’s Route API -
a reverse proxy configured as a Route row inside DHIS2, with code: "chap",
that DHIS2 then serves under /api/routes/chap/run/.
Three things follow from that, and they are the three the commands do:
| Without it | What the app does |
|---|---|
the chap route | redirects to /get-started; it cannot see Chap at all |
the analytics_* tables | has no data to send, and nothing on screen says why |
| the apps themselves | there is no Chap user interface in DHIS2 |
chap-devops dhis2 show # what this instance has, changing nothing
chap-devops dhis2 connect # the route, the apps, then analytics
| Command | What it does |
|---|---|
chap-devops dhis2 show | Asks and reports: where the chap route points and whether chap-core answers through it, what DHIS2’s analytics timestamp is worth, which of the two apps are installed, and each piece that is missing. A route that is right in every field but that nothing answers through counts as missing. Writes nothing. |
chap-devops dhis2 route | Creates the chap route, or repoints one that is there, then proxies a request through it to prove the whole path. |
chap-devops dhis2 analytics | Generates the analytics tables and waits for them; --no-wait starts the run and leaves it going. |
chap-devops dhis2 apps | Installs the Modeling App and the Climate App from the App Hub, at the newest version this DHIS2 can run. |
chap-devops dhis2 connect | All three: the route, then the apps, then analytics. It also creates the configured models in chap-core, which the Modeling App lists. |
chap-devops dhis2 use | Points all of the above at a DHIS2 that runs elsewhere. See A DHIS2 that runs elsewhere. |
Every one of them is idempotent and meant to be re-run: a second chap-devops dhis2 connect repoints nothing, reinstalls nothing and says so. It runs analytics
again, which takes about 20 seconds on the climate demo. A step that found
nothing to do still reports it.
chap-devops up does none of this, on purpose
chap-devops up is a thin wrapper around docker compose up, and three things make
this the wrong work to hang off it:
- it needs DHIS2 credentials, which are not chap-devops’s to invent;
- it reaches the network - the App Hub, twice - and
chap-devops upnever does; - DHIS2’s API is not ready when
upreturns. Tomcat serves pages while every/api/*request 404s, which is the shapechap-devops status’sdhis2row already knows about, and analytics then takes tens of seconds on demo data and much longer on real data. Anupthat waited for that would be anupthat takes an hour.
So chap-devops components enable dhis2 prints the line that names the command
instead, and chap-devops init --with dhis2 prints it with -v:
the Modeling App reaches chap-core through a DHIS2 route, and this deployment has none yet; once DHIS2 answers, `chap-devops dhis2 connect` adds it, generates analytics and installs the apps
up and status keep asking until a connect is recorded
That note is printed at the one moment DHIS2 does not exist yet. What follows it
is chap-devops up, then several minutes of restoring a dump and migrating a schema,
and by the time anything can be connected the note is far up the scrollback. A
deployment can therefore sit for good with every row up, chap-devops doctor
reporting no problem, and the Modeling App unable to reach Chap at all.
So the two commands that report on a running deployment say it again, and go on
saying it until a connect has been recorded. chap-devops up closes with it, under
the line it always ends on:
already running: chap, chapkit-simple-multistep-model, dhis2, dhis2-db, postgres, redis, worker
chap-devops has not connected this DHIS2 to Chap; run `chap-devops dhis2 connect` once DHIS2 answers
and chap-devops status puts it under its verdict, with the model hints:
chap-core up http://localhost:8700 2.4.0 auth: off
dhis2 up http://localhost:8780
MODEL STATE REACH LAST PING
chapkit-simple-multistep-model registered, not configured via chap-core 10s ago
1 model registered
chapkit-simple-multistep-model: chap-core has no configured model for it, so nothing can run it; run `chap-devops models configs sync`
chap-devops has not connected this DHIS2 to Chap; run `chap-devops dhis2 connect`
Both lines come off .chap-devops/components.yaml and nothing else. Neither asks
DHIS2 anything, which is why each says what chap-devops has recorded rather than
what DHIS2 is - and naming the command is safe either way, because every chap-devops dhis2 verb is idempotent and a second run changes nothing.
chap-devops status says it only while the dhis2 row reads up, since a DHIS2 that
is not answering cannot be connected to anything and the advice could not be
taken. chap-devops up has asked nothing at all and DHIS2 is minutes from its first
request, so it says once DHIS2 answers. Neither says it on a deployment
without chap-core, where chap-devops dhis2 connect refuses: the
route would point at a service that is not there. There, chap-devops dhis2 show
says target none (this deployment has no chap-core) and names chap-devops dhis2 apps and chap-devops dhis2 analytics in place of connect.
connected_at, and what it is not
The line stops once chap-devops dhis2 connect has got through, and that is recorded
in the dhis2 block of .chap-devops/components.yaml:
dhis2:
enabled: true
port: 8780
image_tag: '2.42'
image: dhis2/core
seed: default
connected_at: 2026-09-27T12:09:53Z
It is a note that the command ran, and it is not evidence. Nothing decides
anything by it except whether those two lines are printed. chap-devops doctor
also shows the time, as last `chap-devops dhis2 connect`: TIME. The route can be
deleted, repointed at another server or disabled in DHIS2’s own Route
administration a minute later, and this timestamp will not move, because nothing
reads DHIS2 to check it.
chap-devops dhis2 show is the command that
asks DHIS2. It reads the route out of the instance, proxies a request through
it to prove chap-core answers, lists which of the two apps are installed and
says what the analytics timestamp is worth - and it writes nothing, so it is
safe to run on anything. If the question is can the Modeling App reach Chap,
that is the command; connected_at cannot answer it and does not try.
connect says as much where it writes the record:
hint: recorded the connect in `.chap-devops/components.yaml`, so `chap-devops up` and `chap-devops status` stop asking for it
hint: the record says that this ran, not that the route is still right; `chap-devops dhis2 show` asks DHIS2
What sets it
A run of chap-devops dhis2 connect that ended with a verified route and both apps
in place. On an external DHIS2, where connect sets only the route, a
verified route is enough: the apps are the step of its admin, and chap-devops dhis2 show names them. Two of the three steps, and deliberately so: the hint exists because
a DHIS2 beside a Chap cannot be used at all without them - the app redirects to
/get-started with no route, and there is no Chap user interface in DHIS2 with
no apps. Analytics is not part of it. The Modeling App reaches chap-core and
works without the analytics_* tables; empty tables are a deployment with no
data rather than one that cannot talk to itself, chap-devops dhis2 analytics --no-wait is a supported shape in which no run has finished, and chap-devops dhis2 show reports the analytics question separately and with the evidence it is
worth.
The route is judged by the proxied request, not by the row: connect asks
chap-core for its health through /api/routes/chap/run/, the way the app does.
When chap-core has an API token (see Authentication), the route
carries it: connect writes it into the route’s auth as an Authorization: Bearer header, which DHIS2 adds to every request it proxies and never lists
back. /health needs no token, so with one on the check also asks for
/v2/services, which does; a route that reaches /health and gets a 401 past
it is rewritten, and one DHIS2 lists with no header auth at all is rewritten
before anything is asked. The Modeling App itself never needs the token.
The single-step verbs record nothing. chap-devops dhis2 route alone leaves a
deployment with a route and no apps, which is still one nobody can use Chap
from.
chap-devops dhis2 connect --offline is the third answer, and it is neither of the
other two. It skips the app install and says so, so the run has looked at
nothing that could tell it whether the apps are in DHIS2 - it judges nothing,
and leaves the record exactly as it found it, in either direction. A
deployment nothing has connected still records no connect. A deployment that
was connected keeps the timestamp it has: the apps are still installed, this
run simply did not ask, and a flag that turned off an unrelated step is not
evidence that anything broke. Clearing on it would make chap-devops up say chap-devops
has not connected a DHIS2 that chap-devops connected.
What clears it
| When | What happens |
|---|---|
chap-devops components disable dhis2, with or without --purge | Forgotten with the component. With -v, the disable says so. The record is about a DHIS2 instance this deployment no longer has. |
chap-devops down --volumes, when dhis2_db was actually removed | Forgotten with the database, and the line says why. |
A chap-devops dhis2 connect that found something wrong | Cleared. With -v, the report says cleared the earlier `chap-devops dhis2 connect` . A route nothing answered through clears it. An app that is missing or failed clears it only for the dhis2 component, not for an external DHIS2. The hint comes back, which is the answer that errs the safe way. |
A chap-devops dhis2 connect that could not look | Nothing. --offline skips the apps, so the run has nothing to say about them and the timestamp stays where it was. |
The chap-devops down --volumes row is the one worth understanding. dhis2_db going
means the next chap-devops up restores the seed dump into a database
being created and that dump ships a chap route of its own, pointed at an
external server. A record that survived would suppress the one line asking the
operator to repoint it, on a deployment whose Modeling App is quietly talking to
somebody else’s Chap. Disabling the component is the same hazard by another
route, which is why it clears the record whether or not --purge was given: a
dhis2_db kept on disk may be re-created from the dump later anyway.
hint: the record of `chap-devops dhis2 connect` is forgotten with the component; a DHIS2 enabled here again is asked to connect afresh
What does not clear it
Nothing that happens inside DHIS2. Deleting the route, repointing it, disabling
it, dropping the F_CHAP_MODELING_APP authority, uninstalling either app, or
restoring a different database into the same volume by hand all leave the
timestamp exactly where it is, and the two hint lines stay quiet. So does a
restored backup, which brings .chap-devops/ and the volumes back
together and is therefore consistent, but says nothing about what has happened
to that DHIS2 since.
None of that is a gap to be closed by reading more state: a record can only ever
say what happened at one moment, and the live answer needs a request. chap-devops dhis2 show is that request.
The credentials, and where they come from
chap-devops holds no DHIS2 credentials, and every request above needs one: a
username and password (HTTP Basic), or a DHIS2 personal access token. The
rule is that a password belongs to the user it was set with, so the places
chap-devops reads are pairs, and the first that has a value wins:
| Order | Source | Notes |
|---|---|---|
| 1 | DHIS2_API_TOKEN in .env | A personal access token. Preferred to the password beside it: it can be scoped and revoked. |
| 2 | DHIS2_ADMIN_PASSWORD in .env, for DHIS2_ADMIN_USERNAME | The deployment’s own answer, and where its other secrets already live. The user is admin when the file names none. |
| 3 | CHAP_DEVOPS_DHIS2_TOKEN in the environment | A token kept off disk: export it for one shell. |
| 4 | CHAP_DEVOPS_DHIS2_PASSWORD in the environment, for CHAP_DEVOPS_DHIS2_USERNAME | A password kept off disk. Without CHAP_DEVOPS_DHIS2_USERNAME it is the password of the user .env names. |
| 5 | seed_password: in .chap-devops/components.yaml | For any user, on a DHIS2 chap-devops restored from a seed with that option. The restore gave every user that password. |
| 6 | admin / district | The DHIS2 default, and only on a DHIS2 chap-devops deployed. |
The default is not a guess. A seeded demo dump ships that user, and a
Flyway-bootstrapped empty database gets it from DefaultAdminUserPopulator, which
has both words compiled in. It is never sent to an
external DHIS2: chap-devops did not create that
instance, so district is nobody’s password there, and trying it would only be a
failed login in its audit log. An external DHIS2 with none of the four set is an
error before any request is made. So is a .env that names a user and sets no
password for them: the default belongs to admin, not to whoever the file names.
.env carries the variable names as commented placeholders. On a DHIS2 chap-devops
deployed they hold exactly the values already used, so uncommenting one changes
nothing until it is edited:
# DHIS2 login `chap-devops dhis2` uses. Only chap-devops reads these - no container is given
# them - and commented out means the DHIS2 default below. A personal access token,
# when set, is used instead of the username and password.
# DHIS2_API_TOKEN=
# DHIS2_ADMIN_USERNAME=admin
# DHIS2_ADMIN_PASSWORD=district
For an external DHIS2 the password line is empty, since there is no default to
write down. chap-devops sync adds the section when a DHIS2 is first used.
Only chap-devops reads these variables. None of them reaches a container, so none
appears in a generated compose file.
--user NAME asks for one user by name for one run. That always means a
password, even when a token is on offer, and only a password that belongs to
NAME is sent:
DHIS2_ADMIN_PASSWORD, when.envnamesNAME;CHAP_DEVOPS_DHIS2_PASSWORD, unlessCHAP_DEVOPS_DHIS2_USERNAMEgives it to someone else;seed_password:in.chap-devops/components.yaml, for any user, on a DHIS2 chap-devops restored from a seed with that option;- the default, for
adminon a DHIS2 chap-devops deployed.
Anything else is refused before a request is made, rather than sending one user’s password in another’s name. So a one-off run as someone else is:
CHAP_DEVOPS_DHIS2_PASSWORD=theirs chap-devops dhis2 show --user alice
There is deliberately no --password or --token flag, because a secret on
a command line ends up in the shell history and in ps. Nothing ever prints the
secret. A report names the user and says what the credential is and where it was
found (password from `.env` , API token from CHAP_DEVOPS_DHIS2_TOKEN, the DHIS2 default password). For a token, the user is whoever DHIS2 says owns it
(GET /api/me). -vv traces the header as Authorization: Basic <admin and its password> or Authorization: ApiToken <the token>.
A token is created in DHIS2 under Profile > Personal access tokens. Leave its
allowed HTTP methods unrestricted, or allow at least GET, POST and PUT:
route writes with POST and PUT, and analytics and apps start work with
POST. The token’s user needs the same authority a password’s would, and a
superuser has it.
A credential DHIS2 does not accept gets an error that says so, rather than “request failed”:
error: DHIS2 at http://localhost:8780 did not accept the password for `admin` (the DHIS2 default password); set `DHIS2_ADMIN_USERNAME` and `DHIS2_ADMIN_PASSWORD` in `.env`, or export `CHAP_DEVOPS_DHIS2_PASSWORD`
error: DHIS2 at https://dhis2.example.org did not accept the API token (API token from `.env`): it is expired, revoked, or not allowed from this address; set a current one as `DHIS2_API_TOKEN` in `.env`, or export `CHAP_DEVOPS_DHIS2_TOKEN`
A DHIS2 that runs elsewhere
Most real deployments put Chap beside a DHIS2 that already runs on a server of
its own. chap-devops dhis2 use records one, and from then on every chap-devops dhis2
verb talks to it instead of the dhis2 component:
chap-devops dhis2 use https://dhis2.example.org --chap-url https://chap.example.org
It takes two URLs because the two directions are different:
| URL | Whose view | What it is for |
|---|---|---|
| the DHIS2 URL | this machine’s | every request chap-devops makes: the origin, plus the context path of a DHIS2 served under one (https://example.org/dhis) |
--chap-url | DHIS2’s | where the chap route points. chap-core as that DHIS2 server reaches it, including any CHAP_ROOT_PATH |
The route is the only reason for --chap-url, and chap-devops cannot work it out. The
compose alias http://chap:8000 resolves only inside this deployment, and
localhost on the DHIS2 server is that server. So the route becomes
<chap-url>/** (https://chap.example.org/**), and a --chap-url that names
localhost or 127.x is recorded with a note that it will not work. When the
DHIS2 URL is on this machine too, the note says that a DHIS2 in Docker reaches
chap-core at http://host.docker.internal:PORT instead. A
DHIS2 URL that is plain http:// on another machine - not this one, not a
private address - is recorded with a note too: every request carries the
password or the token, and over http:// they cross the network unencrypted. A
DHIS2 that redirects - http:// to https:// behind a proxy, say - is
followed, and its credentials go along only on the same host, never to
another one.
The record lives in .chap-devops/components.yaml, next to the components:
dhis2-external:
url: https://dhis2.example.org
chap_url: https://chap.example.org
connected_at: null
use records the URLs and then asks the DHIS2 two things: whether /api/ping
answers, and whether it accepts the credential chap-devops dhis2 would send. It
reports both, and names the next step: the credential to set, or chap-devops dhis2 connect. A URL that did not answer is still recorded, since the DHIS2 may just
be down for maintenance. The report says so.
dhis2 https://dhis2.example.org (answers /api/ping)
chap-url https://chap.example.org
route https://chap.example.org/**
login API token from `.env` (accepted)
connected never recorded
recorded the external DHIS2 at https://dhis2.example.org in `.chap-devops/components.yaml`
run `chap-devops dhis2 connect` to point its route at this Chap
| Form | What it does |
|---|---|
chap-devops dhis2 use URL --chap-url URL | Record one, or replace the one recorded. |
chap-devops dhis2 use URL / chap-devops dhis2 use --chap-url URL | Move one of the two URLs of the one already recorded. With --chap-url alone, the headline says changed the chap-url of the external DHIS2 at URL to CHAP_URL in `.chap-devops/components.yaml`. |
chap-devops dhis2 use | Say which DHIS2 chap-devops dhis2 talks to, and ask it. Writes nothing. |
chap-devops dhis2 use --clear | Forget it; chap-devops dhis2 talks to the dhis2 component again. |
What changes against an external DHIS2:
connectsets the route and nothing else. Installing apps and generating analytics tables change a server chap-devops does not run, and an analytics run on a national instance takes hours of its CPU.connectsays which of the two apps are missing and ends with askipped:line for each step it left; its admin runschap-devops dhis2 appsandchap-devops dhis2 analyticswhen they agree.connected_atis recorded once the route is proved. If an app is missing,connectthen ends withthe Modeling App can reach Chap after the admin of this DHIS2 installs the Modeling App and the Climate App; `chap-devops dhis2 show` says when they are there.chap-devops dhis2 showends withrun `chap-devops dhis2 apps` for the apps, then `chap-devops dhis2 analytics` for the analytics tables; the apps and analytics change this DHIS2, so ask its admin first.- Credentials have no default. See the credentials. A token is the natural choice for a server you do not own the admin password of.
- Nothing asks docker. There is no container. The wait for
/api/pingis the whole check, and a DHIS2 that does not answer points you back at the recorded URL rather than atchap-devops logs dhis2. - The analytics timestamp is trusted. No seed dump of chap-devops’s was restored
into it, so
lastAnalyticsTableSuccessis that instance’s own. - The allowlist is that server’s. A refused route names
route.remote_servers_allowedin thedhis.confon the DHIS2 server, which is its operator’s file, not one in this directory. chap-devops open dhis2opens it, andchap-devops upnameschap-devops dhis2 connectuntil a connect is recorded against it, just as for the component.connected_atis its own record. Moving either URL forgets it, because a connect was a fact about one DHIS2 and one route target.
It is one DHIS2 or the other, never both. use refuses while the dhis2
component is on, and chap-devops components enable dhis2 refuses while an external
DHIS2 is recorded. Each names the command that clears the way.
The route is repointed, not created
A seeded instance already has a chap route, and it points at a stranger’s
Chap. The climate demo dumps ship one - right code, right authority, not
disabled - aimed at an external server, so it looks fully configured. A
“create if absent” implementation would skip it and the deployment would quietly
send its data somewhere else.
chap-devops dhis2 route therefore compares and rewrites. It leaves the route alone
only when all three are already true, and names whichever is not:
- the URL is this deployment’s chap-core;
- it is not disabled;
- it carries the
F_CHAP_MODELING_APPauthority.
$ chap-devops -v dhis2 route
DHIS2 2.42.6 at http://localhost:8780, as `admin`
hint: the credential is the DHIS2 default password
repointed the `chap` route at http://chap:8000/**; chap-core answered through it
hint: the route was rewritten because it pointed at http://158.39.75.126/stable/**
hint: chap-core said: healthy
hint: run `chap-devops dhis2 apps` next, or `chap-devops dhis2 show` to see what is still missing
The target is http://chap:8000/** - the compose service alias, never
localhost, because the request is made by DHIS2 from inside the deployment, and
localhost there is DHIS2’s own container. The /** suffix is what makes DHIS2
proxy the paths under the route; without it exactly one path is proxied and
everything the app asks for after /health answers 404. A deployment that sets
CHAP_ROOT_PATH gets it in the middle (http://chap:8000/master/**), because
that prefix moves every chap-core route with it.
Then the route is proved rather than assumed: GET /api/routes/chap/run/health goes through DHIS2 to chap-core and back, which is
the same address the Modeling App uses. A row in DHIS2’s database says nothing
about whether DHIS2 can resolve the hostname, is allowed to reach it, or gets an
answer. If nothing answers, the route is still correct and the command still
succeeds - a chap-core that is down is chap-devops up’s problem - so the line is a
warning and --json carries "verified": false:
warning: the `chap` route is in place but nothing answered through it: HTTP 502 Bad Gateway; run `chap-devops status` to see whether chap-core is up
The chap-devops status clause is there only when chap-core does not answer on this
machine either. When chap-core answers, the line names the more probable
cause: the allowlist of DHIS2, or a chap-url that DHIS2 cannot reach. chap-devops dhis2 show adds the same clause after missing: nothing answered through the `chap` route: .... See The route is there but nothing answers through
it.
route.remote_servers_allowed in
dhis2/dhis.conf already permits every http and
https target, so the allowlist is not normally in the way. On an instance whose file was narrowed
or replaced it is, and DHIS2’s refusal is turned into the line that says so:
error: DHIS2 refused the route: version 42 and later only allow the origins `route.remote_servers_allowed` lists, and http://chap:8000 has to be one of them; check that line in `dhis2/dhis.conf` and run `chap-devops restart dhis2`, which recreates it to read the file again
DHIS2’s own answer to that write is 409 Conflict with the message Route URL is not permitted, which names neither the setting nor the file - and its
errorCode, E1004, is DHIS2’s general-purpose conflict code rather than this
one’s, so the message above is built from the message alone. If
chap-devops restart dhis2 does not recreate the container, use
chap-devops restart --all dhis2; see Applying an edit.
Analytics, and the parameter that populates nothing
The Modeling and Climate apps read their figures out of DHIS2’s analytics_*
tables, which exist only once analytics has been generated.
chap-devops dhis2 analytics
is POST /api/resourceTables/analytics?skipTrackedEntities=true, followed by
polling GET /api/system/tasks/ANALYTICS_TABLE/{id} until DHIS2 says the run
completed.
No lastYears, ever. It reads as an obvious optimisation and it is a trap:
measured on the climate demo, &lastYears=8 produced zero rows in every
analytics table while reporting success in 16.9 seconds, where the same run
without it wrote 146,129 rows into analytics_2024 in 15.7. A silent success
that populates nothing leaves the app with no data and nothing explaining why, so
there is no flag for it here. An instance so large that the years have to be
limited is a curl against DHIS2 rather than a footgun in this command.
DHIS2 runs one analytics job at a time, and a second POST queues behind the
first with an empty notifier - so a wait on it would report nothing for as long
as the first one takes. chap-devops dhis2 analytics therefore adopts a run that is
already going instead of asking for another:
the analytics run that was already going finished in 4 minutes (job jFxL1tE0pAy)
DHIS2 lists a run in its notifier about five seconds after it accepts it. A
second chap-devops dhis2 analytics in those seconds also reads
/api/jobConfigurations. There it finds the analytics job that DHIS2 accepted
and has not started yet, and it adopts that job. It does not start another.
A job left RUNNING by a hard stop blocks every future run for good;
dhis2-prep resets those on every start,
which is why that is not a state you have to get out of by hand.
The run is not silent: a step is printed to stderr every half minute, and -vv
prints every one DHIS2 announces. --timeout SECONDS is how long to wait, an
hour by default, and running out of it is not a cancellation - DHIS2 carries on,
and the same command watches the same job again.
What show can say about analytics, and what it cannot
DHIS2 reports a lastAnalyticsTableSuccess in /api/system/info, and on a
seeded deployment that timestamp is not about this deployment.
It is a settings row, so it is restored with everything else: measured on a
2.42.6 with the Laos climate demo, a freshly seeded instance reported a last
success of 2026-06-16T07:51:00.093 while analytics_2024 did not exist at all -
the table was absent, not empty. It is a fact about the database the dump was
taken from.
chap-devops reaches DHIS2 over HTTP and cannot look at the tables, so show reports
what it checked and labels the timestamp with what it is worth:
| Row | What chap-devops checked |
|---|---|
analytics never run | DHIS2 records no successful run at all. DHIS2 2.42 with an empty database reports 1970-01-01T00:00:00.000 instead, and chap-devops reads that time as no run too. |
analytics 2026-09-27 12:10 (a run finished on this deployment) | An analytics run has finished on this DHIS2 since it started, which GET /api/system/tasks/ANALYTICS_TABLE says. That notifier lives in the running process, so nothing a dump carries can put an entry in it. |
analytics 2026-06-16 09:51 (unconfirmed on a seeded database) | DHIS2 records a success, this deployment was seeded, and no run has finished here since DHIS2 started. The timestamp proves nothing either way. |
analytics 2026-09-27 12:10 | DHIS2 records a success and this deployment has no seed, so its database was migrated from empty and the record was made against it. |
DHIS2 records the time with no zone, on the clock of the server, and a
DHIS2 container runs in UTC. chap-devops reads the offset of the server from
serverDate in GET /api/system/info, and shows the time on the clock of
this machine. --json gives the same time in RFC 3339, with the offset of
this machine. If DHIS2 sends no serverDate, the time stays as DHIS2 wrote it,
with (DHIS2 server time) after it.
The third of those is a question and not a verdict, and it is put as one:
missing: analytics may never have run on this deployment: a seeded database can carry the dump's timestamp, and no run has finished since DHIS2 started; run `chap-devops dhis2 analytics` to settle it
Restarting DHIS2 empties the notifier, so a seeded deployment whose run was made
before the last restart is reported unconfirmed again. That is the cautious
answer rather than a wrong one: running chap-devops dhis2 analytics a second time is
idempotent and cheap, and reading show as “analytics is done” when the tables
are absent is the failure this whole command exists to catch.
A run that finished is not the same as tables with rows in them. Nothing
here can see a row count, so nothing here claims one - which is the same
distinction the lastYears trap above turns on.
The apps come from the App Hub, server-side
chap-devops dhis2 apps
resolves each app’s newest version that this DHIS2 can run from
https://apps.dhis2.org/api/v1/apps/{id}, then POST /api/appHub/{versionId} -
and DHIS2 downloads the app itself. Nothing is fetched here and no multipart
upload is built.
| App | What it is for |
|---|---|
| Modeling App | the Chap user interface inside DHIS2 |
| DHIS2 Climate App | imports climate data into DHIS2 through Google Earth Engine, which is where the Modeling App’s covariates come from - chap-core does not fetch them |
A version whose minDhisVersion is newer than the instance is skipped: DHIS2
installs it happily and the app then fails in the browser. An app already
installed at that version is left alone, and one at another version is moved to
it.
A blank bound is no bound. The App Hub sends a bound it has not set as an
empty string rather than as an absent field - every one of the Modeling App’s
published versions carries "maxDhisVersion": "" - and chap-devops treats missing,
empty, whitespace and unreadable the same on both sides. A bound it cannot
compare against is one it cannot honour, and refusing on it would refuse
everything.
Three names for the same app, and they all differ: the App Hub publishes the
Modeling App as Modeling, an instance lists it under Modeling with the key
dhis2-chapmodeling-app, and chap-devops calls it the Modeling App everywhere it
prints. Matching takes the punctuation and the case out and accepts one name
containing the other, so all three spellings find each other.
chap-devops dhis2 show reports those two apps and no others. A 2.42.6 ships 29
bundled apps of its own, identical on every DHIS2, and they are not what this
command answers for:
apps Modeling App 7.3.0, DHIS2 Climate App not installed
missing: the Climate App is not installed
Installed apps live in /opt/dhis2, which is the dhis2_home volume, so they
survive a recreate and a backup carries them.
This is the one step that cannot work --offline, and it needs the network twice:
here for the version id, and from DHIS2 for the app. Under --offline it is
refused with what it would have needed, and chap-devops dhis2 connect --offline does
the other two steps and reports the skip rather than failing:
skipped: installing an app needs the DHIS2 App Hub, twice: chap-devops resolves the version there and DHIS2 downloads the app itself; run the command without `--offline`, or install both apps from DHIS2's own App Management page
The waits are long, and they are refusals first
Before a single request, three things are refused with what is true instead: a
deployment without the component, one whose DHIS2 publishes no host port and so
cannot be reached from this machine at all, and one whose dhis2 container docker
says is not running. A docker that cannot be asked is not a refusal - that would
refuse on every machine docker is absent from - so the container check degrades
to a trace line and DHIS2 is asked directly.
Then /api/ping is polled until it answers, for twenty minutes by default
(--wait SECONDS), because that is what a first start can take. An external
DHIS2 recorded with chap-devops dhis2 use is not migrating behind this
deployment’s back, so it gets one minute by default: a mistyped URL or one
behind single sign-on fails in a minute rather than in twenty. A DHIS2 that is
already answering says nothing at all; one that is not says so once:
DHIS2 at http://localhost:8780 is not answering /api/ping yet; waiting up to 20 minutes, and `chap-devops logs dhis2` is where the migration shows
Right after chap-devops restart dhis2, DHIS2 can hold the /api/ping request
until it is ready, which took about 15 seconds on the climate demo. The command
then waits without this line.
/api/ping is the only route DHIS2 answers without credentials, so it is the
honest probe for “the API layer is up”; the authenticated /api/system/info right
after it is what settles whether the password is right, and its answer is the
version every report opens with.
The files
mychap/
.chap-devops/
components.yaml intent: the dhis2 block - enabled, port, image_tag, seed
compose.dhis2.yml artifact: the four services and the three volumes
dhis2/
dhis.conf the DHIS2 instance configuration - yours, written once
.env the two generated secrets, three commented pins and the login
The .env block is appended once, when the component is first enabled, and never
rewritten:
# DHIS2 (component). Generated once; the database volume was created with them.
DHIS2_DB_PASSWORD=d82bc75bc49b4c2670235dd1e0ff1fb8
DHIS2_ENCRYPTION_PASSWORD=217ab7126f92244466434ebc5a4c7c16
# DHIS2_IMAGE_TAG=2.42
# DHIS2_DB_DUMP_URL=https://databases.dhis2.org/climate/laos/2.42/laos.sql.gz
# DHIS2_JAVA_TOOL_OPTIONS=-Xms2g -Xmx4g -XX:+UseG1GC
# DHIS2 login `chap-devops dhis2` uses. Only chap-devops reads these - no container is given
# them - and commented out means the DHIS2 default below. A personal access token,
# when set, is used instead of the username and password.
# DHIS2_API_TOKEN=
# DHIS2_ADMIN_USERNAME=admin
# DHIS2_ADMIN_PASSWORD=district
The commented lines each carry the value the command that reads them already
falls back to, so uncommenting one changes nothing until it is edited. The login
block is a section of its own so that
chap-devops sync appends it to a .env that does not have it yet; the two variables are the whole answer to where the
credentials come from, and a name nobody can find is a name nobody sets. For a seed
that is a file rather than a URL, DHIS2_DB_DUMP_URL carries the path inside
the container (/opt/seed.sql.gz), because the host path would be one nothing in
there can read.
chap-devops components disable dhis2 keeps all three volumes and names each one, and
leaves dhis2/ alone:
$ chap-devops -v components disable dhis2
disabled dhis2
stopped and removed 4 containers (dhis2, dhis2-db, dhis2-dump, dhis2-prep); host port 8780 is free again
kept volume mychap-1ab2c3_dhis2_home; remove it with `chap-devops components disable dhis2 --purge` or `docker volume rm mychap-1ab2c3_dhis2_home`
kept volume mychap-1ab2c3_dhis2_db; remove it with `chap-devops components disable dhis2 --purge` or `docker volume rm mychap-1ab2c3_dhis2_db`
kept volume mychap-1ab2c3_dhis2_dump; remove it with `chap-devops components disable dhis2 --purge` or `docker volume rm mychap-1ab2c3_dhis2_dump`
hint: the dhis2/ directory is left alone; it is yours
hint: the record of `chap-devops dhis2 connect` is forgotten with the component; a DHIS2 enabled here again is asked to connect afresh
hint: removed compose.dhis2.yml
hint: `chap-devops status` shows what runs now
Backing it up
chap-devops backup create archives dhis2_home and the DHIS2 database, one member
each, and chap-devops backup restore puts both back.
The database is a pg_dump, not a copy of the dhis2_db volume, with the
exclusions of dhis2-server-tools:
- It leaves out the tables
analytics_*,aggregated_*,completeness_*and_*. DHIS2 makes them again with the nextchap-devops dhis2 analytics. - It leaves out the rows of
audit, and keeps the table. - It does not pause the database:
pg_dumpreads one consistent snapshot while DHIS2 runs.
The restore makes the database new, loads the dump with pg_restore -j 4, and
sets the mark of a complete restore. Measured on a national dump of 22 GB, with
a volume of 62 GB after analytics:
| Time | Size | |
|---|---|---|
chap-devops backup create | 4 min 32 s | 4.2 GB |
chap-devops backup restore | 7 min 36 s | 19 GB database, no analytics tables |
Run chap-devops dhis2 analytics after a restore: until then, DHIS2 has no
analytics tables, so its dashboards and the Modeling App have no data.
dhis2_dump is deliberately left out: it
is a download cache the one-shot refills on its own, so archiving it would add
the whole dump to every backup of the deployment for nothing. See
Backup and restore.
dhis2/ is in the archive’s files/, on the same terms
ocs/climate-service.yaml is: it is scaffolded once and is yours from then on, so an
edit made to dhis.conf exists nowhere else, and chap-devops sync can only write a
fresh one. The whole directory goes in rather than the one file, because whatever
you keep beside it is yours on the same grounds, and anything nested under
dhis2/ is archived with it.
The directory is collected whether or not dhis2 is enabled when the backup is
taken. chap-devops components disable dhis2 leaves dhis2/ alone and says it is
yours, so an archive written while the component is off still has your
dhis.conf in it, and a restore puts it back where it was.
Ports
One published port
A deployment of chap-core and models publishes one host port: chap-core’s
API, 8700 by default and chap-devops init --api-port N otherwise. Nothing else in it
needs one. The opt-in components are the exception, and each
one publishes a well-known port of its own: see
Component ports.
chap-core reaches each model over the compose default network at
http://<service_id>:8000, and that internal URL is what a model registers
with. PostgreSQL and Valkey sit on a second network that model services never
join.
So a model service only gets expose: ["8000"], which declares the container
port without asking the host for anything.
That one port is open to whatever can reach it, unless the deployment has an API token: see Authentication.
Reaching a model
Two ways:
# through chap-core's read-only proxy, no host port needed
curl http://localhost:8700/v2/services/chapkit-simple-multistep-model/run/api/v1/info
open http://localhost:8700/v2/services/chapkit-simple-multistep-model/run/docs
# or give that model a port of its own
chap-devops models expose chapkit-simple-multistep-model # lowest free port, 5001 up
chap-devops models expose chapkit-simple-multistep-model --port 5010
chap-devops models unexpose chapkit-simple-multistep-model # and take it away again
chap-devops up # apply either change
In a deployment without chap-core there is no proxy, so a model gets a host port when it is enabled, from the same range; see Model services without chap-core.
expose and unexpose only rewrite the overlay’s ports:; they never
re-resolve the version, so they are safe on a deployment running a build you do
not want moved. chap-devops models enable ID --port N|auto does the same thing
while enabling.
chap-devops models list shows either the port or via chap-core, chap-devops status
says the same in its REACH column, and
chap-devops models info ID prints the proxy URL for a model with no port of its
own. In chap-devops ui, p opens a prompt on the row under the cursor that takes
a port number, auto, or nothing at all, and P takes the port away.
p works the same way on the browser’s components page, with a narrower
prompt: a number, or an empty line or none, and not auto. auto means
the lowest free port in the model range, and a component publishes a well-known
port of its own outside it, so there is nothing for auto to pick from. p on
chap-core is refused rather than prompted - its host port is the API port,
which CHAP_API_PORT in .env sets - and the footer names that line instead. See
The components page.
Model ports come from the range 5001 to 5999, with 5001 the default lowest
(init --port-base moves it). A port has to be free twice over: unclaimed by
.chap-devops/models.yaml, by every compose*.yml in the directory and by
chap-core’s API port as .env sets it, and with nothing on the machine
listening on it. So two models never collide, and
neither does a model and something else you are running. --port N claims one
explicitly, and the command fails if it is taken.
Which address a model port is published on
Compose publishes a host port on every address the machine has, so a model
with a port is reachable from the network unless a firewall says otherwise.
Models have no login, so on a laptop or a shared server that is often more
than you meant. --bind names the address instead:
chap-devops models enable chapkit_simple_multistep_model --port auto --bind 127.0.0.1
chap-devops models expose chapkit_simple_multistep_model --bind 127.0.0.1 # an enabled one
chap-devops models add ghcr.io/org/model:latest --port auto --bind 127.0.0.1
The overlay then publishes "127.0.0.1:5001:8000", which only this machine’s
own processes can reach: docker port <container> shows 127.0.0.1:5001. A
model’s own --bind is recorded as bind: in .chap-devops/models.yaml and wins over
the deployment’s model_bind: in .chap-devops/project.yaml. The deployment that
chap-devops run keeps sets model_bind: to 127.0.0.1. With neither, the port is
published on every address as before. --bind 0.0.0.0 asks for every
address explicitly, which is how one model leaves a loopback default. An IPv6
address is written bare (--bind ::1) and rendered in brackets.
chap-devops run groups publish every model on 127.0.0.1.
The URLs chap-devops prints say localhost, which is right for every address and
for loopback. For a model bound to one LAN address, use that address instead.
A model registered with a chap-core elsewhere must stay reachable from it:
chap-core calls it back on its host port, and a loopback-only port does not
answer a container. chap-devops sync warns when a model is set up that way and
names the expose --bind 0.0.0.0 that undoes it.
Component ports
A component publishes a well-known port of its own rather than one from the model range:
| Component | Default host port | Why that one |
|---|---|---|
ocs | 8790 | OCS serves a web interface as well as an API, so it is published. |
s3 | none | OCS reaches the object store at http://s3:9000 inside the deployment; nothing out here needs it. |
dhis2 | 8780 | Not the container’s 8080, which a DHIS2 already running on this machine likely holds. DHIS2 is a web application people log into. |
A component port that one of this deployment’s own models already publishes
is warned about when it is set, whether or not that model is up, with the next
free port as the way out: chap-devops up could not start both.
Each is set at creation time or afterwards, and none is how any of them is
kept off the host entirely:
chap-devops init mychap --with ocs,s3 --ocs-port 9010 --s3-port 9002
chap-devops init mychap --with ocs --ocs-port none # reachable only inside
chap-devops init mychap --with dhis2 --dhis2-port 18080
chap-devops components enable ocs --port 9010 # or afterwards
chap-devops components enable dhis2 --port none # behind a reverse proxy
--ocs-port, --s3-port and --dhis2-port each need their component: asking
for a port for something this deployment is not getting is refused rather than
silently ignored, because a port that quietly did nothing would leave you
waiting for a service on an address no file mentions.
chap-devops open NAME opens the component at that port in a browser - and says so
rather than opening anything when the component publishes none. See
Reaching a component from a browser.
8000, 8080 and 9000 are what most development servers default to, so chap-devops’s
own services start one block away from them: chap-core on 8700, DHIS2 on 8780
and OCS on 8790, while inside the deployment they keep their container ports
(http://chap:8000, http://ocs:9000). Two deployments on the same defaults
still collide with each other. Neither the init probe nor chap-devops components enable refuses a taken port; both warn, name a free one, and put the way out
on the same line:
warning: port 8780 is also used by demo (/home/me/demo), which is not running; run only one of them at a time, or run `chap-devops components enable dhis2 --port 8781`
chap-devops components enable probes the port it is given the same way init does,
and warns rather than refuses:
enabled ocs on http://localhost:8790
warning: port 8790 is already in use on this machine (needed by ocs); free it, or run `chap-devops components enable ocs --port 8791`
run `chap-devops up` to apply
A warning, because the listener is often something you are about to stop, and
because nothing is started here: chap-devops up is where a taken port becomes a
refusal. A port one of this deployment’s own running services publishes now
is not a conflict at all. As at init, a port no one is listening on but
another deployment on this machine claims gets the same line with that
deployment named.
How the API port is set
The API’s port is not an edit of compose.yml. That file is upstream’s, byte
for byte. chap-devops sync renders a small compose.chap-devops.yml next to it and puts
it in the -f list:
services:
chap:
ports: !override
- "${CHAP_API_PORT:-8700}:8000"
!override (Compose 2.24.4+, which is why that is the supported minimum)
replaces chap’s own mapping rather than adding to it, which is why the API ends
up on exactly one port; a file listed in include: could not do that at all,
which is why compose.chap-devops.yml is a separate -f entry rather than part of
the umbrella.
The port comes from api_port in .chap-devops/project.yaml, and CHAP_API_PORT in
.env overrides it without touching .chap-devops/.
Because Compose reads .env last, that line wins - and chap-devops follows it, so
the CLI and Compose always talk about the same port. It is the port
chap-devops status probes (unless --url names another), the port chap-devops up’s
preflight reserves, and the port chap-devops doctor checks - and the checklist names
the file it came from when the two disagree:
ok api port 18000 is free (from .env, over the 8700 recorded in .chap-devops/project.yaml)
chap-devops status --json carries the same answer as two fields, api_port and
api_port_source (env or project). A CHAP_API_PORT= line that is
commented out, empty or not a port is not an override, and the recorded value
stands.
init never rewrites a .env it finds, since the database password in it
outlives the rest, so
chap-devops init --api-port 8710 --force
over an existing deployment moves .chap-devops/project.yaml and compose.chap-devops.yml
and then warns that the older CHAP_API_PORT= line is still what Chap
will use. Edit that line, or pass --fresh-env (which rotates the database
password too; see the .env contract).
That is why every message about a busy API port - from init, up’s
preflight and doctor - names that .env line and not init --force: the
line is there from the first init on, so it is the one edit that moves the
port.
CHAP_API_PORT is written as an active line even at 8700, so the one published
port is discoverable by reading .env.
The preflight
chap-devops up checks that every host port this deployment is about to publish is free
before it calls Docker, and refuses with one line per conflict:
error: 2 host ports this deployment needs are already in use; nothing was started
port 8700 is already in use on this machine (needed by chap); free it, or set CHAP_API_PORT=8701 in `.env`
port 5001 is already in use on this machine (needed by chapkit-simple-multistep-model); free it, or run `chap-devops models unexpose chapkit-simple-multistep-model` (the model stays reachable through chap-core) / `chap-devops models expose chapkit-simple-multistep-model --port auto`
or run `chap-devops up --no-preflight` to hand the conflict to Docker
Docker finds the same conflict eventually, several seconds in and named after a container rather than a port.
- A port that a running container of this deployment publishes now is
skipped, so
chap-devops upon a running deployment stays a no-op. If the recreate moves a running service to another port, the check looks at the new port. chap-devops up --no-preflighthands the question back to Docker.- A port that another running chap-devops deployment publishes is named with that
deployment and the command that stops it, and at a terminal
upoffers to stop it and start this one instead;chap-devops up --replacedoes that without asking. A stopped deployment is not named, and--replacedoes not stop it. See A host port is already in use. chap-devops initprobes the API port too, but only warns: the process holding it is often a previous deployment you are about to replace. It warns as well when nothing is listening and another deployment already uses that port - one in a directory beside the new one, or one Docker has started at least once from anywhere - naming it and the first port above that neither a listener nor another deployment holds. Both stay warnings, and 8700 stays the default:chap-devops init hello1 && chap-devops init hello2writes two deployments on one port that take turns, and--api-port NorCHAP_API_PORTin.envmoves either one. The same goes for a component’s port, such as the oneocspublishes. A deployment written somewhere else and never started is in neither search, so that collision still waits forchap-devops up.
The probe is a bind, not a connect, so it needs no privileges and leaves
nothing behind: a listening socket has no TIME_WAIT. It takes four binds
rather than one, because std sets SO_REUSEADDR on Unix and on BSD (macOS
included) that lets a wildcard bind succeed next to a loopback-only listener
and the other way round. Only the same address reliably collides, so every
address Chap could be published on is tried in turn.
Internal-only models and the proxy URL
via chap-core - in chap-devops status, in chap-devops models list and in the
browser’s PORT column - means the model publishes no host port of its own.
The way in is chap-core:
http://localhost:8700/v2/services/<service_id>/run/
chap-devops status prints that line once, under the table, rather than repeating a
URL per row. chap-devops models info ID prints the model’s own proxy URL. The
model’s chapkit endpoints hang off it, so /run/api/v1/info and /run/docs
both work.
Authentication
A deployment is unprotected by default: anything that can reach chap-core’s
port can use its API. Turning authentication on is one flag at init time, or
one command afterwards.
chap-devops init mychap --models chapkit_simple_multistep_model --api-token # at creation
chap-devops auth enable # on a deployment that already exists
What the token protects
CHAP_API_TOKEN gates the whole API. Clients send it as an HTTP header:
curl -H "Authorization: Bearer $TOKEN" http://localhost:8700/v2/services
Three paths stay open, because they are not credentials-bearing calls: /health
and /health/ready, which the container healthcheck calls with no headers at
all, and /system/info, which is how a client discovers that a token is
required in the first place. Everything else needs the token, /v2/services
and /docs included.
The token is the one thing a client has to carry. For DHIS2 that is the chap
route rather than the Modeling App: the app calls DHIS2, DHIS2 proxies the
call to chap-core, and chap-devops dhis2 connect writes the token into the route’s
auth as an Authorization: Bearer header, which DHIS2 stores and never lists
back. Anything else calling the API needs it from chap-devops auth show --reveal.
How the models authenticate
SERVICEKIT_REGISTRATION_KEY is a second, independent secret. It is not an API
token: chap-core accepts it only under /v2/services, which is where a chapkit
service registers itself and sends its keepalive pings. A model presents it as
X-Service-Key, because servicekit can send no other header.
chap-devops writes both secrets together, and this is why: once the API is
protected, an unauthenticated registration is rejected like any other request,
so a model service that sends nothing never appears in chap-devops status. The
registration key is what it sends instead.
Nothing has to be copied for that to work. Every model overlay carries
environment:
SERVICEKIT_ORCHESTRATOR_URL: http://chap:8000/v2/services/$$register
SERVICEKIT_REGISTRATION_KEY: ${SERVICEKIT_REGISTRATION_KEY:-}
and Compose substitutes the value from the .env beside the compose files, the
same file chap-core reads its own copy from. The two ends cannot disagree.
chap-devops sync writes that line when .chap-devops/project.yaml says the deployment has
a key, and comments it out again when it has none.
chap-core needs the same value, and upstream’s compose.ghcr.yml passes only
CHAP_API_TOKEN into the chap service - so the key never reached the
container and the API answered every registration with
{"detail": "Missing or invalid API token"}
compose.chap-devops.yml is where chap-devops puts that right:
services:
chap:
environment:
SERVICEKIT_REGISTRATION_KEY: ${SERVICEKIT_REGISTRATION_KEY:-}
Compose merges environment maps across -f files additively, so this adds
the one variable and leaves upstream’s file untouched. It is rendered whether
or not the deployment has a key: with none in .env the variable arrives
empty, which chap-core reads as no key at all.
Where the secrets live
In .env, and nowhere else:
CHAP_API_TOKEN=d1f0...
SERVICEKIT_REGISTRATION_KEY=9a3c...
.chap-devops/project.yaml records two booleans and no values:
auth:
api_token: true
registration_key: true
That split is deliberate. .chap-devops/ is intent, it is small, and it is the kind
of thing that gets committed, archived and pasted into a bug report; .env is
the file Compose reads and the one to keep out of version control.
A generated secret is 64 lowercase hex characters, the same thing
openssl rand -hex 32 produces. An explicit --api-token VALUE (or chap-devops auth enable --token VALUE) is used verbatim; below 32 characters chap-devops
warns, and so does chap-core at startup, because its API has no rate limiting
and a short token is guessable by anyone who can reach the port.
Verbatim includes $: a token with characters Compose would read as anything
but themselves is written in single quotes (CHAP_API_TOKEN='$abc def'), which
Compose reads literally. Written bare, $abc would be expanded, to an empty
string when no such variable is set, and an empty CHAP_API_TOKEN is
authentication off. A quote, a backslash or a control character cannot be
written that way, so a token containing one is refused.
chap-devops reads .env by Compose’s rules as well: CHAP_API_PORT=8701 # mine is
port 8701 to both, because a # after a space starts a comment. A #
without a space before it (val#ue) is part of the value.
Turning it on and off
| Command | What it does |
|---|---|
chap-devops auth show [--reveal] | Whether each secret is set, and the token only with --reveal; plus whether each OCS data source is set, never its value. |
chap-devops auth enable [--token VALUE] | Write both secrets, record them, re-render the overlays. |
chap-devops auth disable | Comment both lines out, keeping their values, and re-render. |
chap-devops auth rotate | Replace both secrets with new ones. |
chap-devops auth token | Print the token alone on stdout, for a script; exit 1 when authentication is off. |
chap-devops auth show reads both ends: .chap-devops/project.yaml for what the
deployment intends and .env for what is actually set, and says so when the two
disagree - a token recorded as in use but commented out in .env, say. The
token itself is printed only when --reveal asks for it.
On a deployment with the ocs component it also lists the OCS data source
credentials, which live in the same .env:
OCS data sources
ECMWF_DATASTORES_URL set
ECMWF_DATASTORES_KEY set
EDH_API_KEY unset
CDSE_S3_ACCESS_KEY unset
CDSE_S3_SECRET_KEY unset
Those five are read and never written: they are accounts with Copernicus and
Earth Data Hub rather than this deployment’s own secrets, so enable, disable
and rotate do not touch them, and their values stay hidden even under --reveal -
there is nothing to paste into a client, only the question of whether a dataset
will ingest. --json carries them under ocs_data_sources. See
Components.
enable, disable and rotate only ever write those two .env lines. The database password, the
image pins, the comments and the blank lines come out byte for byte as they
went in, which is what makes them safe on a file an operator has edited.
They read and write the file by Compose’s rules, which is what makes the answer
they report the answer Compose will act on: the last active assignment of a
variable is the live one, export KEY= and quoted values are read as Compose
reads them, and a write rewrites the variable’s first active assignment and
removes every later duplicate of it. So a .env that somehow carries two
CHAP_API_TOKEN= lines comes out of chap-devops auth rotate with one, holding the
new token - rather than with the old, lower line still deciding what chap-core
enforces. See the .env contract.
chap-devops auth enable
API authentication is on
run `chap-devops up` to restart chap-core and the models with authentication
With -v, the hints say where the secrets went and which files changed:
$ chap-devops -v auth enable
API authentication is on
hint: wrote the API token to `.env`; `chap-devops auth show --reveal` prints it
hint: wrote the registration key to `.env`; every model overlay now sends it
hint: wrote compose.auto-arima-chapkit.yml
hint: wrote compose.chapkit-simple-multistep-model.yml
run `chap-devops up` to restart chap-core and the models with authentication
hint: the DHIS2 `chap` route carries it once `chap-devops dhis2 connect` has run; any other client needs it from `chap-devops auth show --reveal`
chap-devops up is not optional. chap-core and the model containers read .env when
Compose creates them, so nothing changes for a container that is already
running until it is recreated.
chap-devops auth disable comments the lines out rather than deleting them:
# CHAP_API_TOKEN=d1f0...
so the token your clients are configured with is still recoverable. chap-devops auth enable afterwards picks those values back up rather than generating new ones,
which makes an accidental disable a round trip.
Rotation
chap-devops auth rotate
chap-devops up
Rotating is two steps, and the second one matters: until chap-devops up recreates
the containers, chap-core is still enforcing the old token, and after it every
client that has not been updated gets a 401. Update them in the same sitting:
chap-devops dhis2 connect rewrites the DHIS2 route with the new token, and anything
else calling the API needs it by hand.
The registration key rotates with the token, and the models pick it up from the
same chap-devops up, so there is nothing to do for them.
Reading it back
chap-devops status
chap-core up http://localhost:8700 v2.3.1 auth: on
For a script rather than a screen, chap-devops auth token prints the token and
nothing else:
TOKEN=$(chap-devops auth token)
curl -H "Authorization: Bearer $TOKEN" http://localhost:8700/v2/services
On a deployment with authentication off it prints nothing on stdout, says so on
stderr and exits 1, so a caller that captured an empty string stops rather than
sending an empty header. chap-devops api needs none of this - it reads .env
itself - and chap-devops auth show --reveal is the spelling for reading the value
rather than capturing it. See Jobs and the API.
chap-devops status reads the token out of .env and sends it on every request, so
it keeps working on a protected deployment, and the last cell of the chap-core
line says which mode the deployment is in. A 401 is reported as a token problem
rather than as “not chap-core”: see
Troubleshooting.
--fresh-env regenerates it
init --fresh-env renders a whole new .env, which means a new database
password and, if --api-token is passed with it, a new token and registration
key. Without --api-token the new file has both secrets commented out, so the
deployment comes back unprotected. init on a directory that already has a
.env keeps that file, --force included, so --api-token there does nothing
but warn and point at chap-devops auth enable. See the
.env contract.
Updating
This chapter is about updating a deployment: the model versions and the
chap-core release it runs. Updating the chap-devops binary itself is
chap-devops self update, which is in Install and has nothing to do
with anything here.
Three commands, one job each
| Command | What it does |
|---|---|
chap-devops up | Starts what is on disk. Never changes a version. |
chap-devops update | Fetches newer versions: moves the pins, pulls the images. Never touches a container. --chap-tag moves chap-core somewhere else entirely. |
chap-devops restart | Applies what was fetched to the services that are running. |
Without a deployment, chap-devops update has no pin to move. It refreshes the
marketplace registry of this machine instead, as chap-devops registry update does.
chap-devops self update updates chap-devops itself, and
chap-devops -C ~/.local/share/chap-devops/run/<group> update updates a chap-devops run group. --dry-run reports
the registry that chap-devops has now, and fetches nothing, so it also works with
--offline. --chap-tag, --pin-chap-core and --list-tags need a
deployment, so they are refused there.
The first line says what changed against the copy that chap-devops had
before: the new and removed models, and the models whose entry changed, with
the stable version before and after. -v adds the age of that copy.
$ chap-devops update
updated the marketplace registry: 7 models; changed: chapkit_simple_multistep_model 0.1.3 to 0.1.4
$ chap-devops update
the marketplace registry did not change: 7 models
$ chap-devops update --dry-run
the marketplace registry has 7 models (cache (0 seconds old)); --dry-run fetched nothing
An update that restarted things by itself would decide for you when your
deployment goes down, and an update that started a stopped deployment would be
a deployment nobody asked for. So update ends by telling you which of the
other two to run, and you run it when it suits you:
chap-devops update # ... restart needed: chapkit-simple-multistep-model
chap-devops restart # recreated chapkit-simple-multistep-model
What moves a pin
Model pins move only with chap-devops models enable ID (with --channel or
--version), chap-devops models add SOURCE and chap-devops update. Nothing else, up,
restart, expose and unexpose included, changes the version a model runs.
| Command | Moves a pin? |
|---|---|
chap-devops up, chap-devops up --pull | No. Starts what is on disk. |
chap-devops restart | No. Recreates containers from the files as they are. |
chap-devops sync | No. Renders the compose files from .chap-devops/. |
chap-devops docker pull | No. Downloads the pinned images. |
chap-devops models expose, chap-devops models unexpose | No. Only the ports: of one overlay. |
chap-devops models enable ID --channel C | Yes, for that one model. |
chap-devops models enable ID --version X | Yes, to exactly X. |
chap-devops models add SOURCE | Yes, for the model it adds: to the newest published build of the branch, or to the tag or digest it was given. |
chap-devops update | Yes: every channel-following model, every model added from a repository URL, and chap-core. |
chap-devops update
chap-devops update [--dry-run] [--pin-chap-core] [--chap-tag TAG] [--list-tags] [--yes]
chap-devops update fetches the registry from the network: no cache, no fallback. It
fails under --offline, --dry-run included, because a plan made from a stale
catalogue is not a plan. It exits with status 2:
error: `chap-devops update` refreshes the marketplace registry, which needs the network; drop --offline, or run `chap-devops update --list-tags` to see the chap-core tags offline
A run is four steps, printed in that order:
-
The plan. One row per enabled model, one for chap-core when the deployment has it, and one per optional component, saying what each is pinned to and what it would move to. The names are padded to one width, so that the versions line up. For every model that follows a channel the channel is re-resolved; a pin that moved is recorded in
.chap-devops/models.yamland its# <ID>_IMAGE_TAG=comment in.envis updated. Models enabled with--versionare listed as pinned and skipped:auto_arima_chapkit v1.0.2 (sha-e9047d4) unchanged chapkit_simple_multistep_model v0.1.1 (sha-6c1d9ee) -> v0.1.2 (sha-10b2bc7) chap-core v2.4.0 unchanged, the newest releaseA model enabled with
--version 0.1.1readsv0.1.1 (sha-6c1d9ee) pinned, skippedinstead.A model added with
chap-devops models addis resolved against its own repository rather than the catalogue, so its row reads in image tags:chapkit_dengue_model sha-1eb8cf1 -> sha-b1d6c31One added from a repository URL follows that repository’s default branch: the row moves when a newer commit has a published
sha-build, and the new pin is written to.chap-devops/models-manual.yamlas well, so a latermodels enablebrings back what is running now. One added from an image reference ispinned, skipped. A repository or registry that will not answer is a warning and a row readingunchanged (could not check): the pin stays where it is, and the rest of the update goes ahead. -
The sync, which renders the compose files from
.chap-devops/. -
The pull:
docker compose pull. In a terminal, docker shows its own progress. Without a terminal (a pipe, a log file, CI), chap-devops tells docker to print no progress, unless you give-vv, so the run is silent until the pull ends. A pull of new chap-core images can take several minutes. -
The closing lines, below.
The closing lines
They answer the only question a finished update leaves: is there anything to
do now? There are five answers, and every run ends on exactly one of them.
With -v, the run also says which registry it used, as a hint. The warnings
of the run, such as a lookup that did not answer, come after these lines on
stderr.
Nothing moved, no new image arrived, and nothing running is out of date:
already up to date
Running services are behind what the files now say, which is the case the whole command is shaped around:
updated 1 model pin; restart needed: chapkit-simple-multistep-model
run `chap-devops restart` to apply
Something moved and there is nothing running for it to be ahead of. With
-v, a hint says that chap-devops up starts the new versions:
updated chap-core v2.4.0 -> latest
hint: Chap is not running; `chap-devops up` starts the new versions
Something moved, Chap is up, and none of it was affected. With -v, a hint
says nothing running needs a restart:
updated 1 model pin
Something moved, and docker could not say what is running:
updated 1 model pin
run `chap-devops restart` to apply it to what is running
The second answer can also start with another clause. Nothing was pinned to
move, but a moving tag brought a newer image - ocs:main after upstream has
published, say - and the running container is still on the old one:
pulled a new image for ocs; restart needed: ocs
run `chap-devops restart` to apply
chap-devops restart then recreates that service alone, and a second chap-devops update
says already up to date.
If you run chap-devops update again before chap-devops restart, nothing moves, but the
services are still behind. The line then starts with already up to date:
already up to date; restart needed: chap, worker
run `chap-devops restart` to apply
The first clause names what actually moved: the model pins, chap-core’s tag,
and the services the pull brought a genuinely different image for. A moving
tag that pulled the same image this machine already had is not a change and is
not named, which is why a second chap-devops update on a latest deployment says
already up to date rather than claiming to have done something. A switch to
a tag with other images names both clauses:
updated chap-core latest -> master and pulled new images for chap, worker; restart needed: chap, worker
run `chap-devops restart` to apply
If this machine already has the images at the new tag, the pull brings
nothing new, and the line has no pulled clause:
updated chap-core latest -> master; restart needed: chap, worker
run `chap-devops restart` to apply
What “needs a restart” means
For every running container, update asks docker two questions:
- Is the image it is running still the image its reference points at, now that
the pull has finished? A moving tag such as OCS’s
mainanswers no as soon as upstream publishes. - Is the configuration hash Compose stamped on it
(
com.docker.compose.config-hash) still the hash Compose computes for the files as they are now? A pin that moved, a port that changed, a secret that was rotated: all of them answer no.
Either answer being no means that container is running something this project no longer describes, and it is named in the closing line. A question docker would not answer is never counted as a difference, so an unreachable daemon cannot turn into “restart everything”.
chap-devops restart
chap-devops restart [SERVICE..] [--all]
chap-devops restart is docker compose up -d, which recreates only the containers
that no longer match the files and leaves the rest running. It changes no file,
no pin and nothing in .chap-devops/, and it never syncs: it applies what is already
on disk.
Compose cannot see an edit to a mounted config file such as dhis2/dhis.conf
or ocs/climate-service.yaml. If such a file changed after its container was
made, chap-devops restart first recreates that service and says
recreating <service> to apply its edited config file. --all recreates
everything anyway, so it skips this step.
chap-devops restart # the whole deployment; compose decides
chap-devops restart chap worker # only these, without their dependencies
chap-devops restart --all # recreate everything, changed or not
chap-devops restart --all my-model # recreate this one, changed or not
Docker’s own lines (Container ... Running, Recreated, Healthy) come
first. The command ends with what it did:
recreated chap, worker
or, when it turned out there was nothing to apply, nothing needed a restart: every container matches its files. With -v, the hints are there too: the
services it did not recreate, and the command to run next:
$ chap-devops -v restart
...
recreated chapkit-simple-multistep-model
hint: unchanged: auto-arima-chapkit, chap, postgres, redis, worker
hint: run `chap-devops status` to check that everything answers
When nothing needed a restart, the hint is
hint: `chap-devops restart --all` recreates every one anyway.
A service name the project does not have is refused with the list of the names it does have. A deployment with nothing running at all is not this command’s job, and it says which command it is, then exits non-zero:
Chap is not running; start it with `chap-devops up`
--all is for the case where nothing has changed and a restart is still what
you want. A model whose container is up but which never registered with
chap-core is the example, and chap-devops status prints it as a hint:
chap-devops restart --all chapkit-rwanda-malaria-bym-model
--dry-run
chap-devops update --dry-run prints the plan and stops: nothing is pulled and
nothing is written. It still needs the network, and it says what it would do:
would update 1 model pin
With -v, a hint says hint: `chap-devops update` does it. When nothing would
move, the line is already up to date. A dry run fetches no
compose.ghcr.yml either, so a warning about a missing one comes only from
the real run.
It says nothing about restarting. The images were not pulled and the files were not written, so there is nothing yet for a running container to be out of step with.
chap-core’s own pin
chap-core’s pin moves in the same run.
chap-devops init resolves the default --chap-tag latest to the release it points
at today (v2.4.0, say) and writes that into .env and .chap-devops/project.yaml,
so a deployment is reproducible rather than following a tag that changes under
it.
chap-devops update then looks up the newest release, and when there is a newer one
it downloads the compose.ghcr.yml that release publishes, moves
chap_image_tag, and rewrites the single active CHAP_IMAGE_TAG= line in
.env. Only that line, and only when it still says what the project recorded.
A value you pinned yourself, or the commented placeholder, is left alone with a
warning saying what Chap will actually run.
Moving tags
--chap-tag latest|master|dev keeps a moving tag instead. There is nothing to
move, so update only re-pulls it. Compose never re-pulls a tag it already
has, so the image is refreshed by chap-devops docker pull, chap-devops update or
chap-devops up --pull. The plan row says so:
chap-core latest moving tag, re-pulled; pin it with `chap-devops update --pin-chap-core`
chap-devops update --pin-chap-core
converts such a tag into the newest release pin, the way init does by
default. From latest, it pins the release that latest points at, so there
is no warning and no question. From dev or master, it is a move backwards
(see Moving backwards), so it shows the warning and asks.
In a script, give --yes: chap-devops update --pin-chap-core --yes.
If the newest release cannot be looked up, for example because the GitHub rate limit is used up, the command pins nothing and fails:
error: --pin-chap-core pinned nothing, because the newest chap-core release could not be looked up; try again later, or run `chap-devops update --chap-tag <TAG>` with a release from `chap-devops update --list-tags`
A tag that is neither a release nor a moving one, a sha- build for instance,
is never moved. When --chap-tag moves the pin to such a tag, a warning says
so:
warning: sha-fa880a1 is neither a chap-core release nor a moving tag, so it is recorded as an exact pin and `chap-devops update` will never move it
A dry run says so it would be recorded as an exact pin. The warning comes
only when the pin moves. If ghcr refuses the tag, chap-devops prints only the ghcr
error.
Switching chap-core’s tag
chap-devops update moves a release pin forward and re-pulls a moving one. Going
somewhere else - onto master, back to a release, or straight to a release
that came out this morning - is --chap-tag:
chap-devops update --chap-tag master # follow the master branch from now on
chap-devops update --chap-tag v2.4.0 # pin that release
chap-devops update --chap-tag latest # follow whatever the newest release is
Before the pin moves, chap-devops asks ghcr whether it has both images, the
chap-core and the chap-worker image, at that tag. The dry run asks too. At
the time of writing, ghcr has a chap-core:dev image and no
chap-worker:dev image, so --chap-tag dev is refused:
error: ghcr.io has no image chap-worker:dev, so the pull would fail; nothing was changed; pick another tag, for example a release from `chap-devops update --list-tags`
Use chap-devops update --list-tags to see the tags you can move to.
It is the same run as any other update: the tag is checked, the
compose.ghcr.yml that goes with it is fetched and cached under .chap-devops/,
chap_image_tag and chap_compose_source are recorded, the single active
CHAP_IMAGE_TAG= line in .env is rewritten, the compose files are
re-rendered, the images are pulled, and the closing lines say what moved and
what needs restarting:
auto_arima_chapkit v1.0.2 (sha-e9047d4) unchanged
chapkit_simple_multistep_model v0.1.2 (sha-10b2bc7) unchanged
chap-core latest -> master (compose.ghcr.yml too)
updated chap-core latest -> master and pulled new images for chap, worker; restart needed: chap, worker
run `chap-devops restart` to apply
When the plan is printed, the new compose.ghcr.yml is not fetched yet. The
chap-core row ends in (compose.ghcr.yml too) when this run fetches
compose.ghcr.yml at a ref that the deployment does not have yet. A move from
latest to master shows it. A move from v2.4.0 to latest, where latest
is v2.4.0, does not.
--chap-tag cannot be combined with --pin-chap-core: both decide where
chap-core’s pin goes, and one command does not get to do it twice. clap
refuses the pair with
error: the argument '--chap-tag <TAG>' cannot be used with '--pin-chap-core'.
The three moves
| Move | Command | What happens |
|---|---|---|
| A release to the newest release | chap-devops update | The ordinary run. Nothing to name: update looks the newest release up itself. |
A release to master and back | chap-devops update --chap-tag master, then chap-devops update --chap-tag v2.4.0 --yes | Onto master without a question; back to the release with the warning below, and an answer. |
| To a release the day it is out | chap-devops update --chap-tag v2.4.1 | The same as waiting for chap-devops update to find it, except that it happens now. A release that does not exist is refused. |
A release that does not exist is refused before anything is written:
error: chap-core has no release v9.9.9; run `chap-devops update --list-tags` to see the tags this deployment can move to
Asking for the tag that is already pinned changes nothing and says so:
...
chap-core master already the pin, nothing to switch
already up to date
Which compose file comes with which tag
compose.ghcr.yml is fetched at a ref of the repository, so a release tag and
a branch bring their own, and latest - which is an image tag rather than a
ref - brings the one the newest release publishes. A ref that has no
compose.ghcr.yml is a warning: the image pin moves and compose.yml keeps
the layout it has.
If the newest release cannot be looked up, --chap-tag latest still moves the
pin, because you named the tag. Only the compose file of the newest release is
missing:
warning: could not resolve the newest chap-core release (...); the pin moves to `latest`, and compose.yml keeps the layout it has
A dry run says the pin would move to `latest` . Without --chap-tag, the
warning ends in the chap-core pin stays at `v2.4.0` and the pin does not
move.
Moving backwards
Going back to an older chap-core means running an older schema against a
database a newer one has already migrated. chap-devops does not know chap-core’s
migrations and will not pretend to, so it names the risk and asks:
warning: moving chap-core from master to v2.4.0 can run an older schema against a database migrated by the newer one; run `chap-devops backup create` first
move chap-core from master to v2.4.0? [y/N]
An answer other than y stops the run with
error: cancelled; nothing was fetched, written or pulled.
Two moves count as backwards: a release to an older release, and a moving tag
to any release, since dev and master are ahead of every release and
latest is the newest of them. Going forward, and moving between moving tags,
needs no answer. chap-devops update --pin-chap-core from latest needs no answer
either: it pins the release that latest points at.
--yes is how a script says it meant it. A run with nothing to ask - no
terminal on stdin, or --json - is refused rather than assumed:
error: moving chap-core backwards needs an answer and this is not a terminal; run `chap-devops update --chap-tag v2.3.1 --yes` to confirm it
With --json, the error field says --json has nobody to ask, and the
hint field holds the same --yes command.
The same refusal comes from chap-devops update --pin-chap-core from dev or
master; it ends with run `chap-devops update --pin-chap-core --yes` to confirm it. A --dry-run prints the warning and stops there, because it writes
nothing to confirm.
What a moving tag shows afterwards
master is the same name today and tomorrow, so the tag alone does not say
what is running. Both chap-devops status and chap-devops doctor say which build it is:
chap-core up http://localhost:8700 2.5.0.dev0 master: running ad5aec7dc76f, revision a690e8804f25 auth: off
ok chap-core pin master (moving tag, running ad5aec7dc76f); `chap-devops update` re-pulls it, `chap-devops update --pin-chap-core` pins a release
The digest is the one the image was pulled at (docker inspect), shortened to
the twelve characters a person compares; the revision is what chap-core
reports for itself at /system/info, which a build without a GIT_REVISION
does not carry. Either is left off the line when it could not be had rather
than printed as a hole.
--list-tags
chap-devops update --list-tags
lists where this deployment can move chap-core to, newest first: the three moving tags, then the newest releases GitHub publishes.
That list, and the release lookup behind every chap-devops update, come from
GitHub’s REST API, which allows 60 requests an hour to an address that sends
no credential; set GITHUB_TOKEN (or GH_TOKEN) and chap-devops sends it, for
5000 an hour instead. chap-devops never stores it and never prints it. See
a used-up rate limit.
TAG KIND PUBLISHED NOTE
dev moving - -
master moving 2026-10-07 pinned (moving, running ad5aec7dc76f)
latest moving 2026-10-05 -
v2.4.0 release 2026-10-05 newest
v2.3.1 release 2026-09-21 -
v2.3.0 release 2026-09-11 -
...
chap-core is pinned to master
With -v, a hint names chap-devops update --chap-tag <TAG>, which moves the pin.
PUBLISHED is the day the release was published, or the day the branch behind
a moving tag was last committed to; latest carries the date of the release it
publishes. A cell that could not be had cheaply is -. The dev row has -
because chap-core has no dev branch at the time of writing.
NOTE marks the tag this deployment records (pinned, with the running build
for a moving tag whose container is up) and the newest release (newest).
It writes nothing, pulls nothing, asks for no confirmation and ignores
--dry-run. Under --offline it prints the moving tags and this deployment’s
own pin, with a warning that the releases could not be listed:
warning: --offline: the chap-core releases were not listed, so this is the moving tags and this deployment's own pin
Without the network
chap-devops init --offline, or an init whose fetch fails, keeps the tag exactly as
given and renders compose.yml from the copy of compose.ghcr.yml compiled
into the binary, warning on stderr both times. With --chap-tag latest that
means the deployment does follow the moving tag rather than a resolved release.
An already cached .chap-devops/compose.chap-core.<tag>.yml is reused rather than
re-downloaded, so a second init --force at the same tag works offline.
Pulling without updating
chap-devops docker pull # download the pinned images; no files change
chap-devops up --pull # docker compose up --pull always
chap-devops up --pull is how a moving chap-core tag gets refreshed on the way up.
Neither of these asks the marketplace anything, so neither moves a pin.
Backup and restore
A deployment is four things: the files in the project directory, the chap-core
database in PostgreSQL, one data volume per model service, and the state of
each enabled component: the ocs and s3 volumes, the dhis2_home volume and
a pg_dump of the DHIS2 database.
chap-devops backup create puts all four into one tar.gz, and
chap-devops backup restore puts them back.
The archive
A plain gzipped tar with a flat layout, so tar -tzf reads it and an admin can
restore it by hand:
chap-devops-backup-<project>-<YYYYMMDD-HHMMSS>.tar.gz
manifest.yaml what this archive is: the chap-devops version that wrote
it, the UTC timestamp, the project directory name,
the chap-core image tag (no key for a deployment
without chap-core), the PostgreSQL server
version, every enabled model with its service id,
version, image tag, host port (null when it
publishes none), data dir, uid:gid and volume, and
what was included (or why it was not)
files/ .env, .chap-devops/**, ocs/**, dhis2/** and every
compose*.yml at the project root
db/chap_core.dump pg_dump in the custom format (-Fc)
models/<service_id>.tar one model's data directory, as tar saw it
components/<name>.tar one component volume, likewise: ocs, s3 and
dhis2-home
components/dhis2-db.dump the DHIS2 database: pg_dump -Fc without
analytics_*, aggregated_*, completeness_*, _* and
the rows of audit
Every member but the manifest is optional, and the manifest records which ones are there: what was included, what was skipped and why, and how long each service was held still while its volume was read.
ocs/climate-service.yaml is in files/ because it is the operator’s own
file rather than a rendered artifact: chap-devops sync only ever creates a missing
one, so nothing can rebuild the edits made to it. dhis2/dhis.conf is the same
kind of file and is in files/ on the same grounds - DHIS2 does not start
without it, and all chap-devops sync can do is scaffold a fresh one.
What is collected is the whole of each such directory rather than those two
files, because what an operator puts beside them - ocs/plugins/ is the case in
point - is theirs for the same reason; anything nested under dhis2/ is in the
archive too. A directory is collected whether or not its component is enabled at
the time, because chap-devops components disable leaves it alone and says it is
yours, so a backup taken while a component is off still protects its files. See
DHIS2.
The archive is built and read with the system tar binary rather than a Rust
tar crate, so what an operator sees with tar -tzf is exactly what
chap-devops backup restore sees.
The logs, runs, renv, uv and pytensor volumes are deliberately left
out: they are caches that rebuild themselves, and they are far larger than
everything above put together.
Taking a backup
chap-devops backup create [--out PATH] [--no-db] [--no-models] [--no-components]
The archive is named chap-devops-backup-<project>-<YYYYMMDD-HHMMSS>.tar.gz (UTC)
and written to the current directory, unless --out names a file or a
directory to put it in. A path that ends in / is a directory. chap-devops makes the
directories that do not exist yet. If an archive with that name is already
there (two backups in the same second), chap-devops adds -2, -3, … before
.tar.gz. A file that --out names is overwritten.
PostgreSQL has to be running for the database part, because the dump goes
through docker compose exec. When it is not, the command writes nothing and
stops:
error: the postgres container is not running, so the database cannot be dumped; start it with `chap-devops up`, or pass --no-db
Model data is read through the overlay’s one-shot <service_id>-init
container, which mounts the same named volume at the same path as the model
itself, so it works whether the model is running, stopped, or removed by
chap-devops down. Every model overlay has one, root included. A model that has never
started has no volume yet; it is skipped with a warning and recorded as such
in the manifest. A read that fails - the busybox image cannot be pulled, tar
errors out - is different: the archive is still written, for what it does
hold, and the volume is recorded as failed in the manifest, but backup create exits non-zero and names what it could not read, so a cron job or a
script does not take a partial backup for a whole one.
Component data is read through a throwaway busybox container instead: no
component one-shot - s3-init, dhis2-dump, dhis2-prep - mounts a volume
that ends up in the archive. The DHIS2 database is the exception: it is a
pg_dump run in dhis2-db, so dhis2-db must be running. If it is not, the
backup marks it as failed and exits non-zero; start it with chap-devops up, or pass
--no-components. --no-components leaves all of it out.
dhis2 contributes two members rather than one, dhis2-home and dhis2-db,
because it keeps /opt/dhis2 - the installed apps and the file store - apart
from its database. Its third volume, dhis2_dump, is deliberately left out: it
is a download cache the dhis2-dump one-shot refills on its own, so archiving it
would add the whole seed dump to every backup of the deployment for nothing.
chap-devops components disable dhis2 --purge still takes it, and chap-devops doctor does
not call it a leftover; the archive is the one place it costs something.
chap-devops backup create prints one line: where the archive is, and its size.
With -v, a hint for each part gives its size. For chap-core, one model and
DHIS2 on the Laos demo database, the backup took 6 seconds:
$ chap-devops -v backup create
wrote /srv/chapx/chap-devops-backup-chapx-20261012-113057.tar.gz (34.2 MB gzipped, 70.2 MB of data)
hint: files: .env, .chap-devops/components.yaml, .chap-devops/compose.chap-core.v2.4.0.yml, .chap-devops/models.yaml, .chap-devops/project.yaml, dhis2/dhis.conf, compose.chapkit-simple-multistep-model.yml, compose.dhis2.yml, compose.marketplace.yml, compose.chap-devops.yml, compose.yml
hint: database: chap_core as chap (1.1 MB), PostgreSQL 17.11 (Debian 17.11-1.pgdg13+2)
hint: model chapkit-simple-multistep-model: /app/data (11.9 MB, paused for 0.6 s)
hint: component dhis2: /opt/dhis2 (48.8 MB, paused for 1.3 s)
hint: component dhis2: the database as a pg_dump without analytics_*, aggregated_*, completeness_*, _* and the data of audit (8.3 MB)
hint: `chap-devops backup restore /srv/chapx/chap-devops-backup-chapx-20261012-113057.tar.gz` restores it
The archive path is printed resolved, without .. parts: --out ../archives
prints /srv/archives/....
A part left out by --no-db, --no-models or --no-components is a hint. A
part left out for another reason, such as a model that never started, is a
warning:
warning: chapkit-simple-multistep-model: no ck_chapkit_simple_multistep_model_data volume yet, so there is no data to back up; the service has never started
A service that is running is paused for the seconds its volume takes to read,
as the paused for 1.3 s above shows.
docker compose pause freezes the processes in the container without touching
its storage, so nothing writes into a half-read tar - a chapkit model keeps a
live SQLite database in its data directory, and a tar of a file that is being
written to is a tar of a torn database. The service is always let go again,
including when the read fails. A Ctrl-C while the backup holds a service lets
it go at the next step, and says so. The line names every service that the
backup paused until then. For the deployment above, a Ctrl-C while dhis2 was
paused gave:
error: stopped by Ctrl-C; no archive was written; chapkit-simple-multistep-model, dhis2 were paused for the backup and run again
The command exits with code 130 and removes its staging directory.
Only a second Ctrl-C, or a kill, leaves the service paused. A second Ctrl-C
stops chap-devops at once, with no message. chap-devops status then shows the service as
paused, with a warning:
warning: dhis2 is paused, so it does not answer; run `chap-devops up` to resume it
The next chap-devops up resumes it and says so:
resuming dhis2, left paused (an interrupted `chap-devops backup create`?)
Where pause is unsupported the service is
stopped and started instead (stopped for 6.0 s), and where neither works the
read goes ahead with a warning. The database needs none of this: pg_dump
reads one transactional snapshot, however busy chap-core is while it runs.
A large volume means a long pause. A paused service does not answer.
Before chap-devops pauses a running model or component service for a volume of 1 GiB
or more, it warns. The warning for a model ends with or use `--no-models` to omit the data of every model.
The DHIS2 database is not paused at all: it is a pg_dump without the
analytics tables, see DHIS2.
warning: ocs is paused while chap-devops copies 3.1 GB of `mychap_ocs_data`, and it does not answer until the copy is done; this can take minutes, so run the backup when nobody uses it, or use `--no-components` to omit the volumes of every component
Two things to know about the pause. Docker cannot run a health check on a
frozen container, so a paused service is reported unhealthy until its next
probe succeeds - up to one health check interval (30 seconds for the services
here) after the backup, chap-devops status and docker compose ps may still say
so about a service that is fine. And a chap-devops backup create that is killed
outright cannot unpause what it paused; chap-devops docker run -- unpause SERVICE
lets it go.
Everything is staged under .chap-devops/tmp/ (the same filesystem as the project,
so a multi-gigabyte model volume never lands in a small /tmp) and packed in
one tar -czf to .<name>.tmp beside the destination, which is renamed into
place only once tar has finished. The staging directory is removed afterwards,
also when the backup fails. A failure halfway leaves no half-written archive,
and any archive already at that path - last night’s, say - exactly as it was.
Ctrl-C removes the staging directory too. A staging directory that a killed
run left behind is removed by the next chap-devops backup create: each one holds a
lock in .chap-devops/tmp/<kind>-<pid>.lock, and a staging directory whose lock no
run holds is a leftover.
chap-devops reads and writes a component volume through a busybox container
with the name chap-devops-volume-<pid>-<n>. If Ctrl-C stops the run, chap-devops removes
that container with docker rm -f. If the run was killed and the container
stays, it holds the volume, so chap-devops down --volumes cannot remove that volume
and warns volume is in use. To find the container, run
docker ps -a --filter name=chap-devops-volume. Remove it with docker rm -f NAME.
Restoring
chap-devops backup restore ARCHIVE [--yes] [--files-only] [--db-only] [--no-models]
[--no-components] [--adopt-identity] [--no-start]
The restore goes into a deployment that exists. In a directory without
.chap-devops/project.yaml, it stops with run `chap-devops init` first. To restore on
a new machine or in a new directory, make the deployment first, then restore
into it:
chap-devops init chapy --models none
cd chapy
chap-devops backup restore /srv/backups/chap-devops-backup-chapx-20261012-113057.tar.gz
The models and components that init sets do not matter: the restore replaces
.chap-devops/ with the one from the archive.
It first prints what the archive holds, what it will overwrite and what it will
stop, and asks. --yes answers in advance; without a terminal to ask at and
without --yes it refuses rather than guessing. Nothing is touched before the
plan has been printed and confirmed.
Then, in order:
docker compose stop chap worker <models> <components>, only the services that are actually running, so nothing is woken up just to be stopped,- the files go back over the project directory, and
chap-devops syncre-renders the compose files from the.chap-devops/that just arrived. A.chap-devops/models.yaml,.chap-devops/models-manual.yamlor.chap-devops/components.yamlthe archive does not carry is removed here rather than kept: the deployment the backup came from had none, and keeping this one’s would mix the two. A path in the archive’s manifest that leads outside the project directory, or a compose file name in the restored.chap-devops/that does, stops the restore before anything is written. A.envthat differs from the one in the archive is kept as.env.before-restore. The database credentials in it stay this deployment’s:POSTGRES_USER,POSTGRES_PASSWORD,POSTGRES_DB,CHAP_DATABASE_URLandDHIS2_DB_PASSWORD. A role and its password live in the database volume, the restore keeps this deployment’s volume, andpg_restorerestores rows, not roles, so the archive’s password would not open it. The DHIS2 database is apg_dumptoo, and it loads into this deployment’s role.DHIS2_ENCRYPTION_PASSWORDcomes from the archive when the restore loads the DHIS2 database, because DHIS2 encrypted some of those rows with it. When the restore leaves the DHIS2 database out, it stays this deployment’s. Under--adopt-identitythe volumes are the archive’s, and so are the credentials. Every step below works from the deployment as it now is, so acompose.ocs.ymlthat arrived with the archive is in the-flist the rest of the restore uses, - postgres is started on its own (
docker compose up -d postgres) if it is not up; oncedocker compose psreports it healthy,psql -tAc 'select 1'has to answer before anything is dropped, and then the dump goes in throughpg_restore --clean --if-exists --no-owner, - each model’s data directory is emptied, hidden files included, and refilled through its init container, then chowned back to the model’s numeric uid:gid (busybox resolves no account names, which is why the numbers matter),
- each component data volume is emptied and refilled the same way, through a
busyboxcontainer, since no component one-shot mounts a volume the archive holds. The DHIS2 database is the exception: chap-devops startsdhis2-dbalone, makes the database new (dropdb,createdb), loads the dump withpg_restore -j 4, and sets the mark of a complete restore, - the port check of
chap-devops up, thendocker compose up -d, unless--no-start.
If the archive holds the DHIS2 database, run chap-devops dhis2 analytics after the
restore, when the dhis2 line of chap-devops status says up. The dump has no
analytics tables, so DHIS2 has no data for its dashboards and the Modeling App
until then. The restore ends with a line that says so. On the Laos demo
database, DHIS2 said up less than 10 seconds after the restore, and
chap-devops dhis2 analytics took 30 seconds.
The order is the whole design: nothing writes to the database or a data volume while its storage is being swapped underneath it.
When it is done, the restore prints what it restored in one line, and the next step. For the chap-core, model and DHIS2 archive above, the restore took 19 seconds:
restored 11 files, the chap_core database, the data of chapkit-simple-multistep-model, dhis2, dhis2-db
the deployment is starting
the DHIS2 dump has no analytics tables; when DHIS2 answers, run `chap-devops dhis2 analytics` to make them again
The services it stopped, the files, the .env copy, the kept credentials and
the identity are hints, shown with -v. For a restore into a new deployment
that had not started yet:
restored 11 files, the chap_core database, the data of chapkit-simple-multistep-model, dhis2, dhis2-db
hint: files: .env, .chap-devops/components.yaml, ...
hint: the previous .env is in .env.before-restore
hint: kept this deployment's POSTGRES_PASSWORD, DHIS2_DB_PASSWORD in .env: its database volumes stayed, and they open only with these values
hint: chapv-60913c is kept; the archive's own (chapx-31ab52) is not adopted, so this deployment keeps its containers and volumes
the deployment is starting
hint: `chap-devops status` shows when it answers
the DHIS2 dump has no analytics tables; when DHIS2 answers, run `chap-devops dhis2 analytics` to make them again
The kept line names only the values that differ from the archive’s.
In a deployment that had not started yet, the restore creates a missing
component volume itself, with the labels that Docker Compose gives its own
volumes (docker volume create --label com.docker.compose.project=... --label com.docker.compose.volume=...). So a later docker compose up gives no
warning about it, and chap-devops down --volumes removes it.
Under --no-start or --files-only, run `chap-devops up` to apply replaces
the deployment is starting. The line about the DHIS2 analytics tables still
comes after it.
When the database restore fails
pg_restore exits 1 both when it finished and only complained about objects
--clean --if-exists could not avoid complaining about, and when it restored
nothing at all - a refused connection, a role that does not exist and a server
that is not listening are all exit 1 as well. Exit 1 therefore counts as
success only when
-
pg_restoreprinted its closingerrors ignored on restore: N, which it reaches only after finishing, and -
every
pg_restore: error:line it printed is one ofalready exists,does not existormust be owner of, the three that--clean --if-exists --no-ownercannot help producing, and none of them came from loading rows. ACOPYinto a table thatdoes not existreads like a harmless drop, but its rows were never imported:pg_restore: error: could not execute query: ERROR: relation "public.jobs" does not exist Command was: COPY public.jobs (id, name) FROM stdin;
Anything else, and any exit above 1, fails the restore: the last lines of
pg_restore’s stderr are printed, the exit code is non-zero, nothing claims
the database was restored, and neither the model volumes nor
docker compose up -d are reached. The select 1 in step 3 exists so that the
common case - the wrong credentials, a postgres that is not up yet - fails
before anything has been dropped, with the error PostgreSQL actually gave.
Whose deployment it becomes
A restore never takes the archive’s compose project name. That name is what every container and named volume of a deployment is prefixed with, so adopting it would point this deployment at the volumes of the one the backup came from and abandon its own. Restoring production’s archive into a staging copy is a normal thing to do, and the plan says which name is kept:
identity chapy-8df379 is kept; the archive's own (chapx-31ab52) is not adopted, so this deployment keeps its containers and volumes
Everything else in .chap-devops/project.yaml does come from the archive, the API
port included - the .env beside it sets that too, and the two have to agree.
The DHIS2 port in .chap-devops/components.yaml also comes from the archive.
The labels of the plan are padded to one width: files, database, models,
components and identity. Above them, the from line names the deployment
that the archive came from and its chap-core tag, for example
from deployment chapx (chap-core v2.4.0). For a deployment without
chap-core, it says from deployment chapx (no chap-core).
A second deployment on the same machine
A staging copy on the machine of the deployment it copies gets that
deployment’s ports from the archive (by default 8700, 8780 for DHIS2 and 8790
for OCS). Before the restore starts the deployment, it does the port check of
chap-devops up. If the other deployment is up, the restore puts the data back and
then stops. For the chap-core, model and DHIS2 archive above:
error: restored 11 files, the chap_core database, the data of chapkit-simple-multistep-model, dhis2, dhis2-db, and did not start the deployment: 2 host ports this deployment needs are already in use
port 8700 (needed by chap) is in use, and chapx (/srv/chapx) publishes it too; stop it with `chap-devops -C /srv/chapx down`, or move this one: set CHAP_API_PORT=8701 in `.env`
port 8780 (needed by dhis2) is in use, and chapx (/srv/chapx) publishes it too; stop it with `chap-devops -C /srv/chapx down`, or move this one: run `chap-devops components enable dhis2 --port 8781`
or run `chap-devops up --no-preflight` to hand the conflict to Docker
or run `chap-devops up --replace` to stop chapx first
if you move the port, run `chap-devops up` to start it
The data is restored at that point, and the command exits with code 1. The
ports that you set at chap-devops init do not prevent this, because the restore
writes the ports of the archive. To prevent it, restore without the start and
move the ports before chap-devops up:
cd chapy
chap-devops backup restore ../chapx/chap-devops-backup-chapx-20261012-113057.tar.gz --no-start
- Set
CHAP_API_PORT=8701in.env. - If the archive enables DHIS2, run
chap-devops components enable dhis2 --port 8781. If it enables OCS, runchap-devops components enable ocs --port 8791. - Run
chap-devops up. - If the archive holds the DHIS2 database, run
chap-devops dhis2 analyticswhen DHIS2 answers.
If you skip the steps, chap-devops up names the ports in use and the commands that
move them. A restore that stopped at the port check does the same: do steps 1
to 4. A --files-only restore also writes the ports of the archive, so do the
same steps after it.
The port check also sees a port that the restore moves a running service to.
If the dhis2 of this deployment runs on 8781 and the archive sets 8780, the
check looks at 8780, because chap-devops up recreates dhis2 on that port.
Taking over the identity
--adopt-identity is the takeover, for the one case that wants it: this
deployment is the one in the archive, moved to another directory or another
machine, and should answer to its name again. The plan says so:
identity compose project chapx-31ab52, taken over from the archive (--adopt-identity)
shared /srv/chapx also records the compose project name chapx-31ab52, so the two directories use the same containers and volumes; use only one of them, or remove /srv/chapx
On the same machine, the old directory then has the same compose project name.
The shared line is in the plan only when the old directory is still there,
and the closing lines of the restore repeat it as a warning. chap-devops doctor in
either directory warns on its compose project line: <name>, and <dir> records the same name, so these directories use the same containers and volumes, with the fix use only one of these directories, or remove <dir>.
chap-devops down --volumes in the old directory removes the volumes the new one
uses. Remove the old directory, and do not run chap-devops in it again.
The restore records the takeover as adopted_identity: true in
.chap-devops/project.yaml. So chap-devops doctor does not warn that the database volume
predates the deployment: the volume is the one from the old directory.
Scopes
--files-onlydoes step 2 and nothing else, and needs no Docker at all, which is handy for rebuilding a project directory on a new machine before starting anything. Its plan saysfiles only (--files-only): no service is stopped or started.--db-onlyrestores the chap-core database alone: it stopschapandworker(step 1), does step 3, and then starts the deployment (step 6) unless--no-start. A running model loses chap-core for that time, and registers again within about a minute.--no-modelsleaves the model data volumes as they are.--no-componentsleaves the component data volumes as they are.--no-startskips the last step.
--files-only cannot be combined with --db-only, --no-models,
--no-components or --no-start, --db-only cannot be combined with
--no-models or --no-components, and --adopt-identity cannot be combined
with --db-only.
JSON output
With --json, both commands print one JSON object. Its messages hold the
lines of the human output, each with its level, the hints included.
chap-devops backup creategivespath,size_bytesand themanifestof the archive.chap-devops backup restoregivesfiles,database,database_warnings,models,components,stopped,started,env_backup,kept_credentials,removed_stateand theplanit followed, with the manifest of the archive in it.
Under --json, the restore prints its plan and its question to stderr, so
stdout holds only the JSON object. Without a terminal, pass --yes, or the
restore stops with pass --yes. A command that fails gives "ok": false and
the message in error, as every chap-devops command does.
The same thing without chap-devops
Every step is an ordinary Docker command. They all need the project’s -f list,
which chap-devops docker run -- ... supplies (or write out
docker compose -f compose.yml -f compose.chap-devops.yml -f compose.marketplace.yml ...
yourself). $PGU and $PGDB are POSTGRES_USER and POSTGRES_DB from .env,
defaulting to chap and chap_core.
| step | command |
|---|---|
| list an archive | tar -tzf <archive> |
| read its manifest | tar -xzf <archive> -O manifest.yaml |
| unpack the files | tar -xzf <archive> -C <project> --strip-components=1 files |
| dump the database | docker compose exec -T postgres pg_dump -U $PGU -Fc $PGDB > chap_core.dump |
| load the database | docker compose exec -T postgres pg_restore -U $PGU -d $PGDB --clean --if-exists --no-owner < chap_core.dump |
| read a model’s data | docker compose run --rm --no-deps -T <service>-init tar cf - -C <data_dir> . > <service>.tar |
| write it back | docker compose run --rm --no-deps -T <service>-init sh -c 'rm -rf <data_dir>/* <data_dir>/.[!.]* <data_dir>/..?* 2>/dev/null; tar xf - -C <data_dir>' < <service>.tar |
| hand it to the model | docker compose run --rm --no-deps -T <service>-init chown -R 1000:1000 <data_dir> (0:0 for a root model) |
| check the connection | docker compose exec -T postgres psql -U $PGU -d $PGDB -tAc 'select 1' |
| hold a service still | docker compose pause <service>, and unpause afterwards |
| read a component volume | docker run --rm -v <project>_ocs_data:/v busybox:1.38 tar -C /v -cf - . > ocs.tar |
| write it back | docker run --rm -i -v <project>_ocs_data:/v busybox:1.38 sh -c 'rm -rf /v/* /v/.[!.]* /v/..?* 2>/dev/null; tar -C /v -xf -' < ocs.tar |
| dump the DHIS2 database | docker compose exec -T dhis2-db sh -c 'pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc -Z 1 -T "analytics_*" -T "aggregated_*" -T "completeness_*" -T "_*" --exclude-table-data=audit' > dhis2-db.dump |
| load it | docker compose cp dhis2-db.dump dhis2-db:/tmp/d.dump, then in dhis2-db: dropdb --force, createdb and pg_restore -j 4 --no-owner --no-privileges /tmp/d.dump |
| mark it complete | in dhis2-db: psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "COMMENT ON DATABASE \"$POSTGRES_DB\" IS 'chap-devops: seed restored'" |
On a seeded deployment, the health check of dhis2-db asks for the mark. The
new database from createdb does not have it. Without the last step, the
health check fails, Docker marks dhis2-db unhealthy after 30 checks (about 5
minutes), and dhis2 waits for a healthy dhis2-db before it starts. See
DHIS2.
Stop chap, worker, the model services, ocs, s3 and dhis2 before you
load a database or a data volume. Keep dhis2-db running for its own dump and
restore. Start the services again afterwards. <data_dir> and
the uid:gid are in the manifest, one entry per model; <project> is the
compose_project in .chap-devops/project.yaml, which is also the prefix
docker volume ls shows.
Status and output
What chap-devops status reports
One line for chap-core, one line per enabled component, one row per model, and
one line saying what it adds up to. Under that line, each row that needs a fix
gets a line that says what to do. With -v, the hints are there too:
chap-core up http://localhost:8700 v2.3.1 auth: on
ocs up http://localhost:8790 3 datasets 212.0 MB data
s3 up internal
MODEL STATE REACH LAST PING
chapkit-simple-multistep-model registered port 5001 12s ago
chapkit-rwanda-malaria-bym-model running, not registered via chap-core -
auto-arima-chapkit not running via chap-core -
some-other-service unmanaged http://c0ffee:8000 3s ago
2 of 3 models are not registered.
chapkit-rwanda-malaria-bym-model: restart it with `chap-devops restart --all chapkit-rwanda-malaria-bym-model`
auto-arima-chapkit: start Chap with `chap-devops up`, then `chap-devops logs auto-arima-chapkit`
hint: models without a host port are reachable through chap-core at http://localhost:8700/v2/services/<id>/run/
In a deployment without chap-core nothing registers, so the STATE column says
what the model answered itself: up when GET /health on its host port
answered, running, not answering when its container is up and /health did
not answer (still starting, or failed: read chap-devops logs <id>), and
not running. The closing line counts those instead of registrations, and the
exit code is non-zero while any model is not up. In --json, the list
missing is empty, because no model is expected to register.
There is one line per row that needs a fix. When no row needs one, a hint
(shown with -v) names the one check status cannot do itself. A model whose
container started under two minutes ago is not stuck yet, so its line says to
run chap-devops status again in a minute, not to restart it. See
When everything registered below.
With a chap-core that this deployment does not run, a model that does not
register gets one more line: if chap-devops logs <id> shows App never became ready, enable the model again with a network. A hint says what the two log
lines that matter mean.
A model can get a warning when chap-core cannot run it. A model reports no git
revision when its registration has git_revision: null, or has no
git_revision key on chap-core 2.4 or later. chapkit 2.1 leaves out the key.
For a model of this deployment, the warning is:
ID: it reports no git revision, so chap-core stores no model template for it and `chap-devops models configs sync` cannot configure it; build its image again with `--build-arg GIT_REVISION=$(git rev-parse HEAD)`, then run `chap-devops restart`
For that row, status does not also print the run `chap-devops models configs sync` line. A model registered from outside the deployment gets one of
these:
ID: it reports no git revision, so chap-core stores no model template for it; set `GIT_REVISION` where it runs, then start it againID: chap-core stores its model template VERSION from another git revision and refuses to run it; set a new version in the model, then start it againID: chap-core has configured models of it of other versions only, not of VERSION; run `chap-devops models configs sync ID`
The last warning comes when the model registered with a new version. chap-core
keeps the configured models of the old version, but it does not run them. So
chap-devops models test --backtest has nothing to run until sync creates
configured models of the new version.
The version is chap-core’s own when it publishes one, and otherwise the tag
.chap-devops/project.yaml pins, marked (pinned) so nobody reads it as the running
build. A moving tag such as latest is marked (from the pin) instead, because
a moving tag pins nothing.
With a chap-core elsewhere, the line shows only the version that chap-core
reports, and nothing when it does not answer. There is no pin and no
moving-tag cell. If it does not answer, the error line ends with this deployment does not run it, so start it there, or set another URL with `chap-devops components enable chap-core --url URL`.
A deployment pinned to a moving tag (latest, master, dev) gets one more
cell, because the tag alone does not say which build is behind it: the digest
the running image was pulled at, and the commit chap-core reports at
/system/info. Either is left off when it could not be had. See
Switching chap-core’s tag.
chap-core up http://localhost:8190 2.4.0.dev0 master: running cc09e3654ff2, revision 7bf2a98739f4 auth: off
auth: on means status had a token - CHAP_API_TOKEN from .env, or from
the environment when .env sets none - and sent it as Authorization: Bearer
on every request; auth: off means the API is open to
anyone who can reach the port. See Authentication.
A chap-core that answers 401 to that token is up, token rejected rather than
down: it answered, and its log says it started fine. The registry is behind
the same token, so the model table is left out rather than filled with
not registered rows it could not check, and the error line under it is about
the token. status exits non-zero, and --json carries "state": "rejected".
See 401 from the Modeling App.
Each enabled component other than chap-core gets a line of
its own, directly under it and in the order the compose files are rendered. OCS
is asked over HTTP at http://localhost:<port>/health; the object store
publishes no host port, so the only thing that can be said about it from out
here is whether its container is up.
A dhis2 line is judged by its container first, exactly as the OCS one is, and
only then asked anything: one request to /api/ping, the single route DHIS2
answers without credentials, and the same route its container’s healthcheck is
built out of - judged by the status code, not the body. That request is there for
a failure OCS does not have. A Spring context that failed to come up - an
unreadable dhis2/dhis.conf, a PostgreSQL extension the image cannot find, a
Flyway checksum that does not match the database - leaves Tomcat running and
serving pages while every /api/* request answers 404, so a container that is up
is no evidence at all that the instance works. A 200 from /api/ping is, and a
container that is up while the ping goes unanswered reads starting rather than
up. An instance with no host port cannot be asked from out here and is judged
by its container alone, like the object store.
The line carries no version, and deliberately: no component line does. DHIS2
tells only a logged-in session which version it is running - /api/system/info
answers nothing else, and chap-devops holds credentials for no instance - and the
image_tag in .chap-devops/components.yaml is not that version either. It is intent:
the tag the next chap-devops up would start, while the container standing there was
created, and migrated dhis2_db, at whatever tag was in force when it was
created. Printing the recorded one beside a live container would name a version
that container is not. The recorded tag is chap-devops doctor’s image dhis2 line,
and moving it is an edit to .chap-devops/components.yaml and a chap-devops sync; see
Changing the DHIS2 version.
A dhis2 line that reads up gets one more line under the verdict while
nothing has recorded a chap-devops dhis2 connect for this deployment:
no models enabled; run `chap-devops models enable ID` to add one
chap-devops has not connected this DHIS2 to Chap; run `chap-devops dhis2 connect`
Being up is not being connected: the Modeling App reaches chap-core through a
DHIS2 route that nothing creates on its own, and a deployment can otherwise sit
with both rows up and the app unable to see Chap at all. The line is read off
.chap-devops/components.yaml and asks DHIS2 nothing, which is why it says what
chap-devops has recorded; it waits for the row to read up, because a DHIS2 that is
not answering cannot be connected to anything. It is not printed on a deployment
without chap-core, where chap-devops dhis2 connect refuses. Once a connect is
recorded the line stops - which is a suppressed hint and not a verdict on the
route, and connected_at, and what it is
not says what that record is worth
and which command actually asks DHIS2.
See DHIS2 for the component, and a DHIS2 that answers but 404s every API request for what that failure looks like from the outside.
| State | Meaning |
|---|---|
up | Answering its health endpoint, or - for a component with no endpoint to ask - running. |
starting | Its container is up but it is not answering yet. |
unhealthy | Its container is up, it does not answer, and the health check of the container failed. It is broken, not starting: chap-devops logs <service> says why. |
not running | No container, so nothing to answer. chap-devops up starts it. |
paused | Its container is paused, as an interrupted chap-devops backup create can leave it, so it does not answer. chap-devops up resumes it. The closing line says 1 of 2 components is paused; run `chap-devops up` to resume it. |
An OCS line carries two more facts when they can be had, which are the two an operator would otherwise open its landing page for:
| Cell | Where it comes from |
|---|---|
3 datasets | GET /datasets?f=json, asked only when /health has just answered, with a three-second timeout of its own. |
212.0 MB data | du -sk /app/data inside the running container (docker compose exec -T ocs), bounded at ten seconds. |
Both are silent when they could not be had - an instance that is down, an older
OCS without the JSON dataset list, a docker that could not be asked - so the
line is the one it has always been rather than one with holes in it. A size is
only read from a container that is running; what the data volume holds while
the container is down is chap-devops doctor’s to report, where the
question is how much data a chap-devops down --volumes would destroy. Under
--json the two are components[].datasets and components[].data_bytes,
both null where the line says nothing.
A deployment with chap-core disabled has no chap-core line at all, and
status does not exit non-zero over an API that is not meant to be there. Its
closing line counts components rather than models - both components are up,
or 1 of 2 components is not running; start it with `chap-devops up` - because it
has no models to count and the models line would name chap-devops models enable,
the one command such a deployment refuses. When all components are up, a hint
names chap-devops open for each one that has a page. See
Standalone OCS.
Every model the project enables gets a row, registered or not, with its state
taken from the registry and docker compose ps together.
| State | Meaning |
|---|---|
registered | The container is up, chap-core knows about it, and it has a configured model. chap-devops models configs lists the configured models. |
registered, not configured | chap-core knows it, but has no configured model of the version it registered with, so nothing can run it and the Modeling App does not list it. Run chap-devops models configs sync. This state does not make status exit non-zero. |
running, not registered | The container is up but chap-core has never heard from it. |
not running | The project enables it, but no container exists or it is stopped. |
paused | Its container is paused, as an interrupted chap-devops backup create can leave it, so it does not answer. chap-devops up resumes it. |
unmanaged | chap-core has a service registered that this project does not enable. |
registered, unreachable | chap-core knows it, but cannot reach the model’s /health through its proxy. Usually the model registered with an address chap-core cannot use, such as localhost from inside a container. |
via chap-core in REACH means the model publishes no host port of its own;
the way in is a hint under the table (shown with -v), not repeated per row. A
model that does publish one reads port 5001. --json is unchanged: it
carries internal and the full http://localhost:5001 as it always has. See
Ports.
A model whose container is up but which never registered is the interesting
case. chapkit stops trying five attempts into its startup, so one that came up
before chap-core was healthy stays invisible until it is restarted, which is
what its hint says. The hint asks for chap-devops restart --all <id> rather than a
plain chap-devops restart: nothing about the service has changed, so there is
nothing a restart would recreate on its own, and --all is what recreates it
anyway.
When everything registered
When every model is registered there is nothing to fix. One thing is left to
do, and -v shows it as a hint under the closing line:
all 5 models registered
hint: run `chap-devops models test --all` to check they can run
Registration is a heartbeat. It says the service is alive and talking to
chap-core, not that it can train or predict - a model whose runtime is broken,
or whose account cannot write, registers exactly like a working one and fails
on the first real job. chap-devops status cannot settle that without running the
models, which takes minutes; chap-devops models test does, at two levels. See
Testing a model.
What up means
up means chap-core answered as chap-core: /health has to be JSON with a
status field, and /v2/services has to parse as {count, services}. A bare
200 from something else holding the port, a dev server, a proxy, an older
deployment, is reported as down, naming what answered instead:
chap-core down http://localhost:8700 v2.3.1 (pinned) auth: off
...
error: chap-core at http://localhost:8700 is not responding: port 8700 answers
but it is not chap-core (got text/html)
The check is lenient about the fields chap-core sends, since it may add fields,
rename optional ones or leave one empty and none of that should turn
chap-devops status into a crash, and strict about who is answering.
A chap-core whose own container is up and failing its healthcheck is a different answer from a port nobody is listening on, and it says so:
chap-core down (container unhealthy) http://localhost:8700 v2.3.1 (pinned) auth: off
...
error: chap-core at http://localhost:8700 is not responding: ...
why chap is unhealthy:
sqlalchemy.exc.OperationalError: (psycopg2.OperationalError) ... password authentication
failed for user "chap"
the database volume holds a different password than .env (a previous deployment with the
same name, or --fresh-env); the `volumes` line of `chap-devops doctor` says which, and
`chap-devops down --volumes` removes the volume if this deployment's data can go
The lines are the tail of the container’s own log, filtered to the ones that look like the failure. See Troubleshooting.
Exit codes and short-circuits
chap-devops status exits non-zero when anything this deployment declares is not
where it should be: the API down, a model not registered, or a component not
up. One rule for every deployment shape, because a script polling it as a
health gate cannot know which shape it is polling, and a component that died
has to be as visible as a model that did not register.
The rows have already named what is wrong - which models are missing and what
to do about each one, which component is not up - so that exit adds nothing
further; only an API that is not answering prints its one error line, because
nothing else on the screen says why. starting counts as not up: a component
whose container is running but not answering is a deployment that is not ready,
whatever it will be a moment later.
A project whose containers do not exist at all skips the model table: nothing can have registered with a chap-core that has never started. A deployment that is chap-core and nothing else is then the one line
Chap is not running; start it with `chap-devops up`
A deployment that has components prints their rows first and puts that line
under them, in the shape a running deployment has. The rows are read from
.chap-devops/components.yaml rather than from docker, so they are known whether
anything is up or not, and they say the one thing the single line cannot: which
components this deployment is made of, and where each of them will answer.
ocs not running http://localhost:8790
s3 not running internal
nothing in this deployment is running; start it with `chap-devops up`
That wording is the other half of it: a deployment chap-core is not a component of is never told that Chap is not running, because there is no Chap in it to be running.
--url turns the short-circuit off, because then the question is about that
API and not about this machine. --url also overrides the default of
http://localhost:<api_port>, and --timeout SECONDS (5 by default) bounds
each request.
Never silent
Every command ends with a line saying what it did or found, plus the next step when there is one; empty output is a bug.
-
chap-devops logson a deployment that was never started says so instead of printing nothing, and fails, where baredocker compose logsprints nothing and exits 0. -
chap-devops logs SERVICElists the services whenSERVICEis not one of them. -
chap-devops downreports how many containers it stopped, and that the volumes are still there, under the compose project name they are prefixed with;chap-devops down --volumesreports the volumes it removed, by name. -
chap-devops upends with which services it started or recreated and which it left alone. -
chap-devops docker pullsays how many images it pulled. -
chap-devops docker pson a deployment with no containers saysnothing is running for this deployment; start Chap with `chap-devops up`rather than printing a bare header. -
chap-devops models testprints a header saying which level is being run, one row per model with the verdict, the time and what happened, and a closing line that counts the three buckets; a model that could not be tested says why and what to do about it rather than being left out.
Output that reads as status verifies what it claims rather than assuming it. Where something is wrong there is one summary line and one hint per problem, instead of the same fact three times over.
Output
On a terminal, errors arrive in a rounded box, and tables and status words
have color. Piped into a file or another program, the output is plain text.
--no-color, or NO_COLOR in the environment, turns the color off and keeps
the shapes.
Levels
Each line that a command prints when it is done has a level:
| Level | What it says | Where it goes | Shown |
|---|---|---|---|
info | What the command did or found, and a step you must do next (run \chap-devops up` to apply`). | stdout | always |
warning | Something that needs attention. The command still worked. | stderr, after warning: | always |
hint | Background and optional next commands: the files that were written, where a value came from, a command that shows more. | stdout, after hint: | with -v |
The lines print in the order of the report, so a warning comes after the lines above it, not first.
Errors are not a level. A command that fails prints the error on stderr and exits with a code that is not 0.
A short example. Without -v, chap-devops models expose prints two lines:
$ chap-devops models expose chapkit-simple-multistep-model
exposed chapkit-simple-multistep-model on http://localhost:5001
run `chap-devops up` to apply
With -v, the hints are there too:
$ chap-devops -v models expose chapkit-simple-multistep-model
exposed chapkit-simple-multistep-model on http://localhost:5001
hint: wrote compose.chapkit-simple-multistep-model.yml
run `chap-devops up` to apply
Verbosity
| Flag | Shows |
|---|---|
| (none) | The info and warning lines. |
-v, --verbose | Also the hint lines. |
-vv | Also the trace on stderr: each external command, each HTTP request with its status and time, the registry source, and the files that sync compared. |
-d, --debug | Everything -vv shows, and also the response bodies (cut to 2 KB), the raw docker compose ps JSON, and the path of the project state that the command read. |
-V is not a verbosity flag: chap-devops -V (--version) prints the version of
chap-devops. The trace never goes to stdout, so -vv and -d never change what a
script reads:
$ chap-devops -vv status
deployment: /srv/chapx (state in /srv/chapx/.chap-devops/project.yaml)
registry: https://raw.githubusercontent.com/... from the cache (2 hours old) (7 models)
asking chap-core at http://localhost:8700
GET http://localhost:8700/health -> 200 in 12ms
GET http://localhost:8700/v2/services -> 200 in 8ms
chap-core up http://localhost:8700 v2.3.1 auth: on
A token never shows in the trace: a request with a token shows
Authorization: Bearer <token>, so a -vv or -d transcript is safe to
paste into an issue.
With --json
--json shows all the levels, whatever the verbosity. The document of a
command that uses levels has a messages list with every line and its level:
{
"host_port": 5001,
"id": "chapkit_simple_multistep_model",
"messages": [
{"level": "info", "text": "exposed chapkit-simple-multistep-model on http://localhost:5001"},
{"level": "hint", "text": "wrote compose.chapkit-simple-multistep-model.yml"},
{"level": "info", "text": "run `chap-devops up` to apply"}
],
"ok": true,
"previous": null,
"service_id": "chapkit-simple-multistep-model",
"url": "http://localhost:5001",
"written": ["/srv/chapx/compose.chapkit-simple-multistep-model.yml"]
}
A script reads the fields of the document. The messages are the same words
that a person sees, so a script can show them or keep them in a log.
Global options
These options work on every command, before or after the command name.
chap-devops <command> --help lists them under “Global options”:
| Option | What it does |
|---|---|
--json | Prints one JSON document on stdout, with all the levels. |
--no-color | Prints no color. NO_COLOR in the environment does the same. |
-v, -vv, -d | Sets the verbosity, as in the table above. |
-C DIR | Uses the deployment in DIR, or in a directory above it. |
The registry options (--registry-url, --offline, --cache-dir) are also
accepted everywhere, so chap-devops --offline init works. The help lists them under
“Registry options” only for the commands that use them:
- All three:
init,run,stop,modelsand its subcommands,components enable,components disable,ui,registry,sync,update,up,backup restore,chap,doctor,auth enable,auth disable,auth rotateanddhis2 connect. --offlineonly:dhis2 apps.--offlineand--cache-dir:self update.
On the other commands they change nothing but the once-a-day notice that a
newer release exists: --offline stops it, and --cache-dir is where it keeps
its record.
--json
--json works on every command except ui and chap, and prints exactly one
document on stdout. A line the wrapper has to say for itself goes to stderr there, so a parser’s stdin stays
that one document. Warnings always go to stderr for the same reason, and an
error under --json is a JSON object with error and causes.
chap-devops --json status | jq '.models[] | select(.state != "registered")'
chap-devops --json status | jq '.components' # one entry per enabled component
chap-devops --json docker ps
chap-devops --json docker config # the merged configuration as JSON
chap-devops ui and chap-devops chap reject --json: ui owns the terminal, and chap
prints the output of chap itself, so there is nothing to serialise.
The Docker wrappers exit with Compose’s own exit code, so chap-devops up is a
drop-in for docker compose up in a script.
Jobs and the API
chap-core does everything slow on a worker: building a dataset, backtesting a
model, making a prediction. Each one of those is a job, and /v1/jobs is
where a deployment records what it has been asked to do. The Modeling App
starts jobs and watches them; chap-devops jobs is the same view from a terminal,
and chap-devops api is how a job is started from one.
chap-devops jobs # what this deployment has been doing
chap-devops jobs logs <id> # why one of them failed
chap-devops api GET /v1/jobs # the same list, unrendered
What chap-devops jobs reports
One row per job, newest first, with a running job ahead of a finished one that started in the same second:
ID TYPE NAME STATUS STARTED DURATION
e3e9f243... create_backtest eval-chapkit-rwanda-malaria-bym-model FAILURE 30m ago 10s
501d5b5d... create_backtest eval-chapkit-ghr-model SUCCESS 33m ago 2m 9s
0fa57ec9... create_dataset eval-chapkit-5ou-202101-202312 SUCCESS 47m ago 2s
3 jobs: 2 done, 1 failed
run `chap-devops jobs logs e3e9f243-2492-47cf-9d02-7de886154f70` to see why
After the evaluations and the forecast of the Modeling App walk, the list was:
ID TYPE NAME STATUS STARTED DURATION
9d70cffa... create_prediction Multistep Laos dengue forecast prediction SUCCESS 2m ago 6s
cfa5a546... create_backtest_from_data Multistep Laos dengue (Copy) SUCCESS 4m ago 21s
dca52fb6... create_backtest_from_data Multistep Laos dengue SUCCESS 6m ago 24s
3 jobs: 3 done
chap-devops jobs show <id> prints the fields of one job. A failed job adds the
chap-devops jobs logs step. For the other statuses, the next commands are hints,
shown with -v: for a finished job, the row in chap-core’s database and the
chap-devops api request that reads it.
| Column | What it holds |
|---|---|
ID | The job id, cut to its first eight characters while those tell the listed jobs apart, and printed in full when they do not. Every subcommand takes the short form. |
TYPE | chap-core’s own job type: create_dataset, create_backtest, create_backtest_from_data, create_prediction, run_baseline. |
NAME | The name whoever started the job gave it, which is the only field a person chose. |
STATUS | The worker’s state, unchanged. See the table below. |
STARTED | How long ago the job began, or queued for one that has not. |
DURATION | How long it took, for a job that has finished. - while it runs. |
From chap-core v2.5.0, a backtest also starts one run_baseline job for
each model built into chap-core, named baseline/<model>. chap-core starts
them once for each backtest setup, so that every model can be compared with
the baselines. Thus one backtest can show as four jobs.
--status filters (repeat it for more than one), --type filters by job type
and --limit keeps the newest N after the ordering, so chap-devops jobs --limit 5
is the five most recent. The filters work with or without the list verb:
chap-devops jobs --status FAILURE and chap-devops jobs list --status FAILURE are the
same command.
--json hands back the list chap-core sent, in the order the table was in,
with the fields this CLI does not render still in it.
The status values
chap-core passes its worker’s states through untouched, so the set is Celery’s.
chap-devops jobs groups them into the four things the closing line counts:
| Status | Counted as | Meaning |
|---|---|---|
PENDING | running | Queued; no worker has picked it up. The STARTED cell reads queued. |
STARTED | running | A worker is on it. |
RETRY | running | It failed and the worker is trying again. |
SUCCESS | done | Finished, and its result is in chap-core’s database. |
FAILURE | failed | It raised. The reason is in the log, not here. |
REVOKED | cancelled | Stopped by chap-devops jobs cancel or by the Modeling App. |
A status this CLI has never seen is counted as running: a new state is far more likely to be a new way of being in flight than a new way of being finished.
Where the error text lives
A job description carries a status and nothing else, so FAILURE says only
that something went wrong. The message is in GET /v1/jobs/{id}/logs, which
holds chap-core’s own log lines, then the model container’s stdout, then its
stderr behind a --- stderr --- line, and finally the Python traceback. That
is the only place the real error text exists - it is not in the job list, and
it is not in chap-devops logs, because the model wrote it inside a chapkit job that
chap-core polled.
chap-devops jobs logs e3e9f243 --tail 20
job e3e9f243-2492-47cf-9d02-7de886154f70 FAILURE (create_backtest eval-chapkit-rwanda-malaria-bym-model)
...
sh: 1: /usr/local/lib/R/site-library/INLA/bin/linux/64bit/inla.mkl.run: Permission denied
Error in inla.inlaprogram.has.crashed() :
The inla-program exited with an error. Unless you interupted it yourself, please rerun with verbose=TRUE and check the output carefully.
Execution halted
stderr: The inla-program exited with an error. Unless you interupted it yourself, please rerun with verbose=TRUE and check the output carefully.
The log itself is the only thing on stdout, so it pipes into grep or less
unchanged. The line naming the job is on stderr, and so is the stderr: hint -
the last line of the --- stderr --- section that names an error, skipping the
warnings and the runtime’s Execution halted. It is a best-effort read of
whatever the model wrote, printed only for a job that failed; the log above it
is the authority.
--tail N cuts the printed log to its last N lines. The hint is still read
from the whole log, so shortening the output never hides the reason.
Stopping and forgetting a job
chap-devops jobs cancel <id> # ask the worker to stop
chap-devops jobs delete <id> # drop a finished job from the list
Both print the sentence chap-core answers with. delete on a job that is still
running is refused by chap-core with a 400, which chap-devops reports as
job <id> is still running; cancel it first.
chap-devops api: one request, authenticated
chap-devops api METHOD PATH [--data JSON|@FILE|-] [--url URL] [--raw] [--timeout SECONDS]
chap-devops api is curl with the three things that are tedious to get right
filled in: the base URL of this deployment, the Authorization: Bearer
header from its .env, and an exit code that distinguishes Chap being down
from Chap saying no.
| Part | Rule |
|---|---|
METHOD | GET, POST, PUT, PATCH or DELETE, in any case. Anything else is a usage error before a byte is sent. |
PATH | Starts with /, and may carry a query string: /v1/analytics/evaluation-entry?backtestId=3&quantiles=0.5. |
--data | The JSON body: inline, @path to read a file, or - to read stdin. It is checked as JSON here and then sent byte for byte, with Content-Type: application/json. |
--url | Talk to another chap-core instead of this deployment’s. Works outside a project. The token sent is this deployment’s own only when the URL is this deployment’s API (same scheme, host and port; localhost and 127.0.0.1 count as one); to any other server only CHAP_API_TOKEN from the environment goes, so a typo in the host never hands it this deployment’s token. |
--raw | Write the body through untouched, without any of the rendering below. |
--timeout | How long to wait for the answer, in seconds (30 by default). |
The answer goes to stdout, rendered the way it reads best:
- A JSON object or list is pretty-printed, two spaces per level.
- A JSON string is printed as its text, so
chap-devops api GET /v1/jobs/<id>/logsreads like a log andchap-devops api GET /v1/jobs/<id>printsSUCCESSrather than"SUCCESS".--jsonturns that off, because a caller piping into a parser wants the document. - Anything that is not JSON is written as it arrived.
Exit codes are the point of the command being a command rather than a shell function:
| Code | Meaning |
|---|---|
| 0 | A 2xx. The body is on stdout; an empty body gets its status line on stderr so the run is never silent. |
| 1 | A 4xx or 5xx. The status line (HTTP 404 Not Found) is on stderr and the body is still on stdout, because a 422 from chap-core names the field it did not like. |
| 2 | chap-core could not be reached at all, with the sentence chap-devops status uses, or the command line was wrong (an unknown method, a path with no leading slash, a --data that is not JSON). |
-vv narrates the request line and the headers that were sent. The token’s
value never appears - the line reads Authorization: Bearer <token> - so a
-vv transcript is safe to paste into an issue. -d adds the bodies.
The evaluation flow, end to end
The four steps behind one number in the Modeling App. Every one of them is a job, and the job id is what carries you from one step to the next.
# 1. a dataset: observations plus the geojson they belong to
chap-devops api POST /v1/analytics/make-dataset --data @dataset.json
# -> {"id": "0fa57ec9-11b6-47a8-96ec-6c22580bbdf5"} (a job id)
# 2. the row that job wrote, once it says SUCCESS
chap-devops jobs show 0fa57ec9 # Database result 1 -> /v1/crud/datasets/1
# 3. a backtest of one configured model against that dataset
chap-devops api POST /v1/analytics/create-backtest \
--data '{"name":"eval-multistep","modelId":14,"datasetId":1,"nPeriods":3,"nSplits":3,"stride":1}'
# -> {"id": "59159ad4-893b-4bbf-a855-70a38c3fb787"}
# 4. poll it, then read what it produced
chap-devops api GET /v1/jobs/59159ad4-893b-4bbf-a855-70a38c3fb787 # SUCCESS
chap-devops jobs show 59159ad4 # Database result 3
chap-devops api GET /v1/crud/backtests/3 | head -30
chap-devops api GET /v1/visualization/metrics/3
chap-devops api GET '/v1/analytics/evaluation-entry?backtestId=3&quantiles=0.1&quantiles=0.5&quantiles=0.9'
Which model ids exist is chap-devops api GET /v1/crud/models (or the Modeling App’s
model list); chap-devops status is what says whether the model service behind an id
has registered at all, which is the first thing to check when a backtest fails
immediately.
A prediction is the same shape one step further on: POST /v1/analytics/make-prediction for a one-off, or a prediction setup promoted
from a backtest and run on a schedule.
chap-devops api POST /v1/crud/prediction-setups --data '{"backtestId":3,"name":"multistep-monthly"}'
chap-devops api POST /v1/crud/prediction-setups/1/run --data @run.json
chap-devops api GET /v1/crud/prediction-setups/1
chap-devops api PATCH /v1/crud/prediction-setups/1 --data '{"scheduleCronExpression":"17 3 * * 1"}'
chap-devops api GET /openapi.json is the whole contract, straight from the running
deployment, which is the version that matters rather than the one in any
document.
The token in a script
TOKEN=$(chap-devops auth token)
curl -H "Authorization: Bearer $TOKEN" http://localhost:8700/v2/services
chap-devops auth token prints the token and nothing else: no label, no
abbreviation, one trailing newline. On a deployment with authentication off it
prints nothing at all on stdout, says so on stderr and exits 1, so a script
that captured an empty string stops instead of carrying on with an empty
header. --json gives {"token": "..."}, or {"token": null} when there is
none.
For reading rather than capturing, chap-devops auth show --reveal prints the same
value inside the report that says what it protects. See
Authentication.
Every command that asks chap-core - jobs, models test, status, doctor
and api - takes the token from .env first and, when .env sets none, from
CHAP_API_TOKEN in the environment. Outside a deployment, and inside one
when --url names another server, chap-devops api --url reads only the
environment, which is the same variable .env sets. A redirect is followed,
and the token goes along only when it stays on the same host, on the same or
a more secure scheme (http:// to https://):
CHAP_API_TOKEN=$(chap-devops -C ~/mychap auth token) \
chap-devops api GET /v1/jobs --url https://chap.example.org
What talks to what
| Command | Requests it makes |
|---|---|
chap-devops jobs list | GET /v1/jobs, with status and type as query parameters. |
chap-devops jobs show | GET /v1/jobs, then GET /v1/jobs/{id} for the current status and GET /v1/jobs/{id}/database_result for a finished one. |
chap-devops jobs logs | GET /v1/jobs to resolve the id, then GET /v1/jobs/{id}/logs. |
chap-devops jobs cancel | POST /v1/jobs/{id}/cancel. |
chap-devops jobs delete | DELETE /v1/jobs/{id}. |
chap-devops api | Exactly the one request you asked for, and nothing else. |
Every one of them sends the API token when .env sets one, and every one of
them reports an unreachable chap-core with the sentence chap-devops status uses and
exit code 2. An id prefix is resolved against the job list, and -vv says which
full id it landed on.
The chap CLI
chap-core has its own command line, chap. It evaluates a model on a dataset
of your own, plots the result and exports the scores. It is usually installed
with uv (uvx --from chap-core chap ...). That needs Python and uv, and for
many models also R.
chap-devops chap runs the same chap in a container. You need Docker and
nothing more:
chap-devops chap eval --model-name https://github.com/dhis2-chap/minimalist_example_r \
--dataset-csv laos_subset.csv --output-file r.nc
chap-devops chap plot-backtest r.nc --output-file r.html
chap-devops chap export-metrics r.nc --output-file r.csv
For a guide from an empty machine to a comparison of two models, see Evaluating a model on your own data.
Each argument after chap goes to chap unchanged. To read chap’s own help,
run chap-devops chap with no arguments, or chap-devops chap eval --help for one
command. chap-devops chap --help shows only the options of chap-devops itself. These
come between chap and chap’s own command:
| Option | What it does |
|---|---|
--tag TAG | The chap-core version to run. See Which version. |
--image core or --image worker | The image to run in. See Which image. |
--group NAME | The chap-devops run group whose models chap can reach. See Chapkit models. |
--stop | Stop the model that this run started, after the run. See Chapkit models. |
--timeout SECONDS | How long chap-devops waits for a model that it starts (300). |
--docker | Give the container the docker socket. See Models that run in docker. |
For example: chap-devops chap --tag master --docker eval --model-name ....
Your files
chap-devops mounts the current directory in the container at the same path. Thus:
- a relative path such as
laos_subset.csvorout/r.ncmeans the same file in the container and on your machine; - every file chap writes in the current directory is on your machine after the run, and belongs to your user;
- a dataset CSV finds its GeoJSON beside it, as it does without chap-devops.
A path outside the current directory also works: an absolute path, a ../
path, or the same after = (--output-file=/data/out/r.nc). chap-devops mounts
the nearest existing directory above the path, at the same path. chap does not
make a directory that does not exist, so make the output directory first.
Before the run, chap-devops writes on stderr which directories chap can see. After the run, it names the files that are new or changed, and the next command:
running `chap eval` in ghcr.io/dhis2-chap/chap-worker:v2.3.1
files: chap reads and writes in /home/me/work
...
chap finished; it wrote r.nc
`chap-devops chap plot-backtest r.nc --output-file r.html` plots it
In a deployment, run chap-devops chap in a subdirectory, for example eval/, so
that chap’s files do not mix with the compose files. chap-devops finds the
deployment from a subdirectory too. When you run it in the deployment
directory itself, chap-devops writes a line that says so.
chap’s own output is not changed. chap-devops writes its lines on stderr, and
--json does not apply to this command. A run that only asks for help
(chap-devops chap, --help, --version) gets only the first line.
Which image
chap-core publishes two images with the same tags. chap-devops selects one from
--model-name:
| The model | The image | Size |
|---|---|---|
| A GitHub repository, or a model directory | chap-worker | about 12 GB |
A chapkit service (an http:// URL that is not GitHub, or a model id) | the image that is on this machine; else chap-core | |
A built-in model (builtin:persistence) | the image that is on this machine; else chap-core | |
No model (--help, plot-backtest, export-metrics, …) | the image that is on this machine; else chap-core |
The worker image has uv, R and INLA, and chap-core’s own worker runs models in it, so a model that needs R always runs there. Everything else is plain chap and HTTP, and both images can do it, so chap-devops takes the image that this machine has at the tag of the run, and downloads nothing:
- Only
chap-workeris here, as in a deployment that runs chap-core: chap-devops uses it. chap-coreis here, or neither image is: chap-devops useschap-core, the smaller download (about 2.4 GB). A deployment without chap-core, such aschap-devops init --only dhis2, does not pull the 12 GB worker image forchap-devops chap --help.
--image core or --image worker overrides the choice. A model that needs R
fails in the core image with Rscript: not found.
The first run pulls the image. The line before the run says so, with the size.
Which version
- In a deployment (in its directory, or with
-C), chap-devops uses the deployment’s chap-core tag. The CLI and the server are then the same version. - Outside a deployment, chap-devops uses the newest chap-core release.
- With
--offline, or when the newest release cannot be read, chap-devops uses the newest tag on this machine: ofchap-workerwhen the run needs it, and else of either image. --tag TAGoverrides all of these, for example--tag master.
Chapkit models: no chap-devops up
A chapkit model is a model service: chap sends it data over HTTP, and the
service trains and forecasts. chap eval needs only that service. It does not
need chap-core, its database or its worker. So you do not run chap-devops up for
an evaluation. Give the model’s id, and chap-devops starts the model:
chap-devops chap eval --model-name auto_arima_chapkit \
--dataset-csv laos_subset.csv --output-file arima.nc
What chap-devops does with the id:
| Where you are | The id | What chap-devops does |
|---|---|---|
| In a deployment that has the model | auto_arima_chapkit | Starts only that model’s container, with no chap-core, if it does not run. |
| In a deployment that does not have it | chapkit_ghr_model | Stops, and names chap-devops models enable chapkit_ghr_model. chap-devops does not change the deployment’s models for an evaluation. |
| Outside a deployment | any marketplace id | Starts the model in a chap-devops run group, as chap-devops run ID does, and joins that group’s network. --group NAME selects the group. |
Before the run, chap-devops checks that the model answers. Then it gives chap the
model’s URL on the network, for example http://auto-arima-chapkit:8000,
and writes:
starting auto-arima-chapkit for this run, without chap-core
model: auto_arima_chapkit at http://auto-arima-chapkit:8000 answers
A model that chap-devops started keeps running, so the next evaluation starts at
once. The last line says how to stop it. --stop stops it after the run
instead. A model that ran before the command is never stopped. --timeout SECONDS (300) is how long chap-devops waits for a model that it starts.
A model that chap-devops starts in a deployment does not register with chap-core,
because chap-core does not run. If you run chap-devops up later, compose recreates
the model’s container, and the model then registers as usual.
A word is a model id only when no file or directory of that name is in the current directory. A directory of the same name is used as a model directory, as chap does.
A model URL
You can also give a URL, for example a model on another server:
--model-name https://models.example.org. chap-devops asks the URL for
/api/v1/info before the run. If it does not answer, chap-devops stops with a note,
and chap does not start:
error: the model server at http://nothing:8000 is not running (no answer on /api/v1/info), so chap was not started; ...
The URL of a model of the deployment or group, such as
http://auto-arima-chapkit:8000, works like its id: chap-devops starts the model
if it does not run.
Do not use the http://localhost:PORT URL that chap-devops ps or chap-devops status
shows. In the container, localhost is the container itself, so that URL
reaches nothing. chap-devops refuses such a URL before the run, and names the
service URL of the same model when it knows it.
An evaluation of a chapkit service stores a model configuration and its artifacts in that service’s own database, as it does without chap-devops.
Models that run in docker
A model whose MLproject has docker_env starts a container of its own. For
that, chap needs the docker socket. --docker gives it:
chap-devops chap --docker eval --model-name https://github.com/dhis2-chap/chap_auto_ewars \
--dataset-csv laos_subset.csv --output-file ewars.nc
The socket gives the container full control of docker on this machine, so the flag is never the default, and chap-devops writes a warning when it is given. The model’s run directory is mounted at the same path on the host and in the container, so the paths that chap gives docker are correct.
chap-devops reads the MLproject before the run: from the directory, or from
GitHub. If the model uses docker_env and --docker is not given, chap-devops
stops before the run and tells you to add the flag. If chap-devops cannot read the
file (for example, with --offline), it does not check it.
Built-in models
From chap-core v2.5.0, chap has models of its own. They are baselines to
compare a model with. Give them with the builtin: prefix:
chap-devops chap eval --model-name builtin:seasonal_median \
--dataset-csv laos_subset.csv --output-file seasonal_median.nc
The names are persistence, global_median and seasonal_median. A
built-in model runs in chap itself, so chap-devops starts no model and uses either
image. An older tag does not have these models, and chap-devops refuses them there.
An MLproject as a chapkit service
From chap-core v2.5.0, --run-config.as-chapkit makes chap start a model
directory or a GitHub repository as a chapkit service in the container. chap
then talks to it over HTTP, as it does to a deployed model:
chap-devops chap eval --model-name ./my_model --run-config.as-chapkit \
--dataset-csv laos_subset.csv --output-file my_model.nc
The MLproject can use uv_env, renv_env, or no environment. chap refuses
docker_env, conda_env and python_env in this mode, so chap-devops does not ask
for --docker. An MLproject from chapkit mlproject export needs this mode:
without it, its commands do not find config.yml. To serve the same model
outside chap, use chap-devops chapkit mlproject run
(An MLproject model, with no image to build).
Simulate data
From chap-core v2.5.0, chap simulate runs the simulate entry point of a
model, and writes the covariates with a simulated target to a CSV file:
chap-devops chap simulate --model-name ./my_model --dataset-csv covariates.csv \
--out-file sim/simulated.csv
chap-devops makes the directory of --out-file, as it does for --output-file.
The caches
The model runs and the uv, Python and R caches are in chap/ under the data
directory of chap-devops: $CHAP_DEVOPS_DATA_DIR, else $XDG_DATA_HOME/chap-devops, else
~/.local/share/chap-devops. The second run of a model thus installs nothing again. There is one directory for
each model in chap/runs, and the R packages can use some hundred megabytes.
To get the space back, delete the directory:
rm -rf ~/.local/share/chap-devops/chap
Limits
- chap-devops runs the amd64 images. On Apple silicon, Docker runs them under emulation, which is slower.
chap uiis not part of chap-core yet, sochap-devops chap uidoes not work.- On Windows, use WSL with the Linux binary, as for the rest of chap-devops.
The chapkit CLI
chapkit is the framework of the model services. Its command line, chapkit,
creates a model project, changes an MLproject model to a chapkit service and
back, and serves an MLproject model with no image to build. It is usually installed with uv, which needs Python on the machine.
chap-devops chapkit runs the same chapkit in a container. You need Docker and
nothing more:
chap-devops chapkit mlproject migrate my-model
chap-devops chapkit mlproject run my-model
Each argument after chapkit goes to chapkit unchanged. To read chapkit’s own
help, run chap-devops chapkit with no arguments. chap-devops chapkit --help shows only
the options of chap-devops itself.
Which image
chapkit is a dependency of chap-core, so the chap-core image has the chapkit
CLI. chap-devops runs it in that image, and there is no other image to pin or to
update. The chap-core image v2.4.0 has chapkit 2.1.0. From v2.5.0 it has
chapkit 2.4.0, which adds chapkit mlproject export and writes the full
MLproject contract with migrate; v2.5.1 has chapkit 2.4.1. In a deployment
on an older tag, add --tag v2.5.1 for these.
The version follows the rules of chap-devops chap: in a deployment, the
deployment’s chap-core tag; outside a deployment, the newest chap-core
release; --tag TAG overrides both. See
Which version. The image is amd64 only, so on
an arm64 machine, such as a Mac with Apple silicon, it runs under emulation.
chapkit mlproject run is the exception: it runs in a chapkit -cli image.
See Serve an MLproject model.
Your files
chap-devops mounts the current directory in the container at the same path, and the container runs as your user. So the paths in the arguments mean the same in the container, and the files that chapkit writes are yours. A path outside the current directory is mounted too. See Your files.
Migrate an MLproject model
chapkit mlproject migrate DIR makes a chapkit service from the MLproject
file in DIR. It moves the files that the service does not need to _old/,
and writes main.py, pyproject.toml, Dockerfile and the other files of a
chapkit project. Without --yes, it asks first; in a terminal, you answer the
question as with a local chapkit.
chap-devops chapkit mlproject migrate my-model --yes
The output ends with the lines of chap-devops:
chapkit finished; it wrote my-model/.dockerignore, my-model/.gitignore, my-model/CHAPKIT.md, my-model/Dockerfile, my-model/Makefile, my-model/README.md, my-model/_old/.gitignore, my-model/_old/MLproject, and 7 more
wrote my-model/uv.lock with `uv lock` in the same image, so `docker build` works
The Dockerfile of a chapkit project copies uv.lock, and chapkit tells you
to run uv lock on your machine. chap-devops does this for you: when the run writes
a pyproject.toml and the Dockerfile next to it copies a uv.lock that is
not there, chap-devops runs uv lock in the same image.
Then build the image and test it, as for a new model project:
cd my-model
docker build --platform linux/amd64 --build-arg GIT_REVISION=$(git rev-parse HEAD) -t my-model:dev .
chap-devops run my-model:dev
chap-devops -C ~/.local/share/chap-devops/run/default models test my_model
chapkit selects the base image of the service from the scripts: the ones at
the root of DIR, and the ones that the train and predict commands name, such
as scripts/train.py in the mlproject-* templates. R scripts give an R
image, Python scripts give chapkit-py. If it finds none, chapkit stops with
No Python (.py) or R (.r/.R) scripts at the project root or in the entry-point commands. Then give the image:
chap-devops chapkit mlproject migrate my-model --yes --base-image chapkit-py
The values are chapkit-py, chapkit-r, chapkit-r-tidyverse and
chapkit-r-inla. chap-core v2.5.0 and older (chapkit 2.4.0) look at the root
only, so there the mlproject-* templates need --base-image.
On chap-core master before v2.5.0 (chapkit 2.4.0), these models were
migrated, built, and passed the model-level test with 1 training and 1
prediction: minimalist_configurable_model and mean from chap-models, and
the mlproject-py and mlproject-r templates with --base-image. On
v2.5.0, the mlproject-py template passed again. On v2.5.1, the mlproject-r
template was migrated with no --base-image, built, and passed.
Export a service as an MLproject
chapkit mlproject export reads the main.py of a chapkit service, and
writes the MLproject file that describes the same model. It needs chapkit
2.4.0, so it needs chap-core v2.5.0 or later:
cd my-service
chap-devops chapkit mlproject export main.py -o MLproject
chapkit imports main.py to read the service. This works in the chap-core
image when main.py imports only chapkit and the Python standard library,
as a service that runs scripts does. If main.py imports the packages of
the model, the import can fail in the chap-core image.
Only a service that runs scripts can be exported, such as one from the
shell-py or shell-r template. For a model in main.py (the fn-py
template), chapkit stops with Only ShellModelRunner services can be exported.
The exported MLproject gives the train and predict commands a fixed
config.yml, which chapkit writes. So it runs where chapkit runs it: with
chap-devops chapkit mlproject run, and with chap-devops chap --run-config.as-chapkit.
Without --run-config.as-chapkit, chap does not write config.yml, and the
train command stops with No such file or directory: 'config.yml'.
Serve an MLproject model
chapkit mlproject run DIR serves an MLproject model as a chapkit service,
with no image to build:
chap-devops chapkit mlproject run my-model
chap-devops chapkit --image r-inla mlproject run my-model
chap-devops chapkit --image chapkit_ghr_model:dev mlproject run .
The model needs its language and chapkit in one image. The chap-core image has
no R, so chap-devops runs a chapkit -cli image instead:
--image | Image |
|---|---|
py | ghcr.io/dhis2-chap/chapkit-py-cli |
py-data | ghcr.io/dhis2-chap/chapkit-py-data-cli, with numpy, pandas, scipy, scikit-learn, statsmodels, joblib, pyyaml and xarray |
r | ghcr.io/dhis2-chap/chapkit-r-cli |
r-tidyverse | ghcr.io/dhis2-chap/chapkit-r-tidyverse-cli |
r-inla | ghcr.io/dhis2-chap/chapkit-r-inla-cli (amd64 only) |
| any other value | That image, such as one you built yourself |
chap-devops chapkit --image, with no value, lists the short names, what each image
has, and whether this docker has it already.
Without --image, the train command in MLproject selects the image: a
python or uv command gets py-data, and an Rscript command gets
r-tidyverse. A model that needs INLA, or packages that no -cli image has,
needs --image. An image of your own must have chapkit 2.4.0 or later, such
as an image built FROM a -cli image. chap-devops pulls the image if this docker
does not have it.
chap-devops mounts the model directory at /work and publishes the service on the
first free port from 5001 (in a deployment, from its port range). It prints
the URL when the service answers at /health. The logs follow in the
terminal, and Ctrl-C stops the service and removes its container. A kill
of the chap-devops process does the same, and so does docker stop of the
container.
The image has chapkit and the packages in the table above. chap-devops does not
install the dependencies that the MLproject names in uv_env or
renv_env. If the model needs a package that the image does not have, the
train job stops with an error such as No module named 'cyclopts'. Then
give --image with an image that has the package, or let chap install
them with chap-devops chap --run-config.as-chapkit
(The chap CLI).
If the model directory is a git checkout, chap-devops gives chapkit its commit as
GIT_REVISION, as the docker build of an image does. chap-core stores a
model only with a revision. If the directory has changes that are not
committed, chap-devops warns: the service reports the last commit, so commit the
changes and raise version in MLproject first.
The options of chapkit mlproject run after DIR go to chapkit unchanged,
such as --config-format flat or --log-level debug. A relative --database
path is in the model directory. chap-devops sets --host and --port itself.
In a deployment that runs chap-core, the service joins the deployment network
and registers with chap-core while it runs, with the registration key from
.env. Then the commands of the deployment use it by its id, the name in
MLproject:
chap-devops models configs sync my-model
chap-devops models test my-model --backtest
chap-devops status lists the service as unmanaged, with the address where chap-core
reaches it, because chap-devops does not manage it.
Outside a deployment, or with an external chap-core, it does not register;
use the URL that chap-devops prints. chap-devops chap runs in a container, where
localhost is the container, so give it the same port at
host.docker.internal:
chap-devops chap eval --model-name http://host.docker.internal:5001 \
--dataset-csv laos_subset.csv --output-file my-model.nc
What chap-devops refuses
chapkit test needs a model service that the container can reach. chap-devops
refuses it and names the command that does the work:
| Command | Use instead |
|---|---|
chapkit test | chap-devops models test ID, which runs chapkit test in the model’s own container. |
If uv lock fails, chap-devops gives a warning; see
could not write DIR/uv.lock.
For a new project, chap-devops models new DIR is the same as chapkit init, with
the templates in chap-devops. See A new model project.
chap-devops chapkit init NAME also works, and gets a uv.lock the same way.
--json does not apply: the output is chapkit’s own.
Doctor
chap-devops doctor is the first command to run on a machine you have not deployed
on before, and the first one to run when something is wrong. It asks, in one
pass, every question the other commands assume the answer to: is Docker there,
is its daemon up, is Compose new enough, is there disk, can this machine reach
the hosts Chap pulls from, and is this the newest chap-devops. Inside a deployment
directory it goes on to the project: its compose project name, the files, the
compose files, .env, the volumes, the host ports, the pins, the images and
whether Chap is up.
chap-devops doctor
ok docker cli docker 29.9.0
ok docker daemon Docker Engine 29.8.2
ok docker compose v5.5.1
warn os and arch macos/aarch64; Chap images are linux/amd64 only, so they run under emulation
nothing to do: Rosetta runs the amd64 images, they are only slower
ok disk space 1067.0 GB free on /Users/you/mychap
ok leftovers no volumes left by removed deployments
ok network ghcr.io reachable (HTTP 401)
ok network marketplace reachable (HTTP 200)
ok network releases reachable, newest chap-core is v2.4.0
ok github api reachable, 41 of 60 requests left this hour (no token; set GITHUB_TOKEN for 5000)
ok chap-devops v0.102.0, aarch64-apple-darwin, cargo build
ok compose project mychap-1ab2c3 (recorded in .chap-devops/project.yaml)
ok deployment files all 7 present
ok components chap-core
ok compose files in sync (0 to write, 4 unchanged, 0 to remove)
ok .env auth off, POSTGRES_PASSWORD set, CHAP_IMAGE_TAG present
ok volumes no mychap-1ab2c3_* volume yet; `chap-devops up` creates them
fail api port 8700 is in use by something else
port 8700 is already in use on this machine (needed by chap); free it, or set CHAP_API_PORT=8701 in `.env`
ok chap-core pin v2.4.0 is the newest release
ok image chapkit-simple-multistep-model ghcr.io/chap-models/chapkit_simple_multistep_model:sha-10b2bc7 (linux/amd64)
ok registry pin chapkit_simple_multistep_model sha-10b2bc7 is the newest build on main
ok user chapkit-simple-multistep-model 1001:1001, as the image declares
skip health no container of this deployment is running
run `chap-devops up` to start Chap
23 checks: 20 ok, 1 warn, 1 fail, 1 skipped
Four words carry the verdict, and only one of them is a problem:
| Word | Colour | What it means |
|---|---|---|
ok | green | Nothing to do. |
warn | yellow | It works, but something will bite later. |
fail | red | Chap will not work until this is dealt with. |
skip | dim | Not evaluated: there was nothing to ask, or asking was ruled out. |
A line that is not ok carries an indented line under it saying what to do
about it, and that line is the same sentence the command it belongs to would
have failed with. In a deployment without chap-core, the health line says
run `chap-devops up` to start the deployment. The exit code is non-zero only when something failed, so
chap-devops doctor in a CI step goes red on the checks that would have stopped the
deployment and stays green on the ones that are merely worth knowing.
Every check is bounded. Each external command is killed if it overruns, each
network probe has a three-second timeout, and the probes run in parallel, so
chap-devops doctor always finishes and normally does so in a few seconds.
-vv prints every command and request it made.
Machine checks
These run everywhere, inside a deployment directory or not.
| Check | What it asks | What a bad answer means |
|---|---|---|
docker cli | docker --version answers | Asked without the daemon, so a Docker Desktop that is not started reads as such on the docker daemon line below rather than as a missing CLI. Fails when docker is not on PATH. Install Docker Desktop or the docker CLI from https://docs.docker.com/get-docker/. |
docker daemon | docker info reaches a daemon | Either nothing is listening, which is open Docker Desktop or sudo systemctl start docker, or the socket refuses this user, which is sudo usermod -aG docker $USER and a fresh login. The two are told apart by what Docker printed, because the fixes have nothing in common. |
docker compose | docker compose version and how old it is | Below 2.24.4 the API port override will not work: compose.chap-devops.yml uses !override, which arrived in 2.24.4, so the API is published on two ports. Below 2.20 the model overlays will not load either: compose.marketplace.yml uses include:, which arrived in 2.20. A warning rather than a failure, because the base services still run. |
os and arch | what this host is | On an arm64 host it warns: chap-core and every marketplace image are published for amd64 only. On macOS that is nothing to do, Rosetta runs them. On arm64 Linux it needs qemu/binfmt: docker run --privileged --rm tonistiigi/binfmt --install amd64. Nothing is probed, because probing means running an amd64 container, which is far too slow for a checklist. |
disk space | free space where the images land | Warns below 10 GB and fails below 3 GB. The measured filesystem is Docker’s own data root when this host can see it, and the working directory otherwise: Docker Desktop reports a path inside its Linux VM, which says nothing about the disk out here. |
memory | how much memory docker says it can give a container | Only asked on a deployment that has the dhis2 component enabled: DHIS2 is the one service here whose failure for want of a few gigabytes is a silent kill rather than an error, and a line about a limit nothing else comes near would be noise everywhere else. Warns below 6 GB and fails below 4 GB, and names both ways out - give docker more (Docker Desktop: Settings, Resources), or size the heap down with DHIS2_JAVA_TOOL_OPTIONS in .env - because below 4 GB the JVM is killed part-way through the analytics populate phase and the run never finishes. The figure is docker’s own rather than this host’s memory: on macOS and Windows the containers live in a Linux VM whose size a large host says nothing about, and on Linux the daemon reports the host’s memory anyway. An engine that will not say is a skip, as is having no docker CLI to ask through, because a guess here sends an operator after a memory problem they do not have - or leaves them without the one they do. See Memory. |
network ghcr.io | can this machine reach the image registry | Without it no image can be pulled. A warning, not a failure: --offline keeps chap-devops itself working from the cache or the catalogue built into the binary. |
network marketplace | can it reach registry.yaml | The catalogue falls back to the cache and then to the embedded snapshot, so this is a warning too. chap-devops registry show says which one is in use. |
network releases | can it reach the chap-core release list | chap-devops update needs it; everything else works without it. The answer doubles as the newest release, which chap-core pin below compares against. |
github api | how much of this address’s hour is left on GitHub’s REST API | GET /rate_limit, which GitHub documents as not counting against the limit it reports. Unauthenticated that limit is 60 requests an hour per address, which the release lookup, the chap-core pin line, every registry pin <id> line and every chap-devops models add all spend - a busy operator, or a CI runner sharing its egress address, runs out before lunch. The line says which of the two limits this is: 4990 of 5000 requests left this hour (token), or 43 of 60 requests left this hour (no token; set GITHUB_TOKEN for 5000). It warns when nothing is left, with the clock time the window resets at; it never fails, because every check that needs GitHub already degrades to a skip with the reason on its own line. See a used-up rate limit. |
leftovers | volumes that deployments whose directory is gone left in docker | Warns with how many volumes of how many removed deployments are still there: chap-devops cleanup deletes them, chap-devops cleanup --dry-run lists them first. Nothing else in chap-devops would ever name those volumes again. See Cleaning up after removed deployments. Skipped when there is no docker to ask. |
chap-devops | version, target, install method, newer release | Warns when a newer release exists: chap-devops self update. Under --offline the line still names the build, as a skip: whether there is an update is the one thing this run did not ask. |
An anonymous request to ghcr.io/v2/ answers 401, which is the registry
answering; any HTTP status counts as reachable, because a host that refuses us
is still a host this machine can reach.
Raising the GitHub limit
Set GITHUB_TOKEN (or GH_TOKEN, which is what the gh CLI uses) and every
GitHub request chap-devops makes carries it, for 5000 requests an hour instead of
60:
GITHUB_TOKEN=$(gh auth token) chap-devops doctor
ok github api reachable, 4989 of 5000 requests left this hour (token)
ok registry pin chapkit_simple_multistep_model sha-10b2bc7 is the newest build on main
A classic token with no scopes at all, or a fine-grained token with read
access to public repositories, is enough: everything chap-devops asks GitHub is a
public read. chap-devops never writes the token anywhere - not to .chap-devops/, not
to .env, not to the cache - and never prints it: a -vv trace says
Authorization: Bearer <token> and nothing more. Nothing on the command line
sets it, because a token on a command line is a token in the shell history.
Cleaning up after removed deployments
Deleting a deployment’s directory does not delete what docker holds for it:
its volumes, with the models’ data and any database, stay until something
removes them, and once the directory is gone no chap-devops command names them any
more. chap-devops cleanup is that something:
chap-devops cleanup --dry-run # list what would go, change nothing
chap-devops cleanup # list it, ask, then delete
chap-devops cleanup --yes # delete without asking, for a script
A deployment counts as removed only when chap-devops can prove it. Every time
chap-devops saves a deployment it records its compose project name and its
directory in deployments.yaml in its data directory ($CHAP_DEVOPS_DATA_DIR,
else $XDG_DATA_HOME/chap-devops, else ~/.local/share/chap-devops). A recorded
deployment is removed when its directory was deleted from a parent directory
that is still there, or when the directory now holds a deployment of another
name (chap-devops init --force wrote over it). Only the name in
.chap-devops/project.yaml is read, so a typo in another state file never makes a
deployment look removed. Everything else is kept and named in a warning of
its own: a project.yaml that cannot be read, a directory missing along with its
parent (as on a disk that is not mounted), and a removed deployment that
still has containers (the line names the docker compose -p <project> down
that removes them). Volumes of a compose project chap-devops never recorded are not
touched, and neither is a volume any container still mounts.
A deployment you moved to another directory is recorded again the next time
any chap-devops command saves it there; until then it looks removed, so run
chap-devops sync in its new place before chap-devops cleanup.
Project checks
When docker info does not answer - Docker Desktop not started, or a daemon
that is wedged - the project lines that would have to ask it (volumes,
user <model>, health) are skipped with “the docker daemon is not
answering” rather than asked: those calls have no deadline of their own, and
an answer of nothing from a daemon that is down would read as “no volumes” or
“nothing running”.
These only run when the command found a deployment, which it does the way git
finds .git: from the current directory (or -C DIR) upwards. Outside one,
doctor prints the machine half. With -v, a hint under the summary says why
the project checks are not there:
hint: no deployment here, so the deployment checks did not run; run `chap-devops doctor` in a deployment directory for them
| Check | What it asks | What a bad answer means |
|---|---|---|
compose project | what compose calls this deployment, and whether that name is its own | Warns when another directory records the same compose project name, as chap-devops backup restore --adopt-identity on the same machine leaves it: the two directories then use the same containers and volumes. Warns when compose_project in .chap-devops/project.yaml is empty. Then compose names the deployment after its directory, and another deployment in a directory of the same name shares every container name and every named volume. See the compose project name. |
volumes | which named volumes docker holds under this deployment’s name, whether the database one is this deployment’s, and whether the rest still belong to something it enables | Warns when <project>_chap-db was created before the .chap-devops/ directory was: it belongs to something else, most often an earlier deployment of the same name, and it still holds that deployment’s role password - which is exactly the state in which chap-core cannot log in and never becomes healthy. That comparison is skipped where the filesystem records no creation time. Failing that, it warns about leftover volumes: a ck_<id>_data whose model is no longer in .chap-devops/models.yaml, or an ocs_data, s3_data, dhis2_home, dhis2_db or dhis2_dump whose component is off. Their data is kept on purpose, so that a model or component enabled again finds it, but nothing declares those volumes any more and chap-devops down --volumes no longer reaches them - chap-devops models disable <id> --purge, chap-devops components disable <name> --purge or docker volume rm <name> is what removes one. While the ocs container is down, the line also says how much its data volume holds (; mychap-1ab2c3_ocs_data holds 3.0 GB), so an operator can see how much data a chap-devops down --volumes would destroy before typing it. While that container is up the size is not measured here at all, because the components line already has it from inside the container. The size comes from a du -sk in a throwaway busybox mounting the volume read-only, bounded at ten seconds, because neither docker volume inspect nor docker system df -v reports one reliably. |
deployment files | are .chap-devops/project.yaml, .chap-devops/models.yaml, .chap-devops/components.yaml, .env, compose.yml, compose.chap-devops.yml and compose.marketplace.yml all there, plus one file per enabled component | It names the missing ones. chap-devops sync renders the compose files again; chap-devops init --force writes the whole deployment. A deployment with chap-core disabled is not asked for the base compose file, which it does not have. Files in a component’s own directory (ocs/, s3/, dhis2/) are not counted here. The ok line names those directories, because the components line checks them. |
components | which components this deployment has, and whether the files they need are there | Fails when ocs is on and ocs/climate-service.yaml is missing, or when dhis2 is on and dhis2/dhis.conf is missing. Warns while ocs/climate-service.yaml holds the example values. Also reports facts it does not judge; see The components line. |
compose files | chap-devops sync --check, without its exit code | The rendered files no longer match .chap-devops/, which is drift the next chap-devops up would undo anyway. Run chap-devops sync. |
.env | is it readable, and does it still say what it should | Warns when POSTGRES_PASSWORD is the default chap or unset, and when the CHAP_IMAGE_TAG pin comment is gone. Whether authentication is on is reported either way: auth: off is a fact to be able to see, not a fault. |
api port, port <model>, port <component> | is every host port the deployment publishes free | Fails when something else holds one, with the same line chap-devops up’s preflight prints for that port: free it, or move it. A port held by this deployment’s own running container is not a conflict, exactly as up treats it: the line says is held by this deployment's own container. See Ports. |
manual <id> | is a model added with chap-devops models add still on the newest build its branch has published (one line per such model) | Warns when the branch has moved on: chap-devops update moves the pin. A model added from an image reference is pinned on purpose and skipped as such, and a repository or registry that would not answer is a check that was not made rather than a fault in the deployment. Skipped under --offline. See Models outside the marketplace. |
chap-core pin | is the pinned tag still the newest release (left out when chap-core is not a component of this deployment) | Warns when a newer chap-core has been released: chap-devops update --dry-run says what would move. A moving tag (latest, master, dev) is not behind anything, so it passes whether or not the list was read, and says which build it is on instead - dev (moving tag, running 7f3a1c2e9b4d) - plus the two commands that leave it: chap-devops update re-pulls it, chap-devops update --pin-chap-core pins a release. The digest comes from the image chap-core’s container runs and is left out when nothing is running. See Switching chap-core’s tag. Skipped when the release list was not reachable, and under --offline, where it was never asked for. |
image <model> | does each enabled model’s exact tag exist on ghcr, with a linux/amd64 image | docker manifest inspect per model, in parallel, ten seconds each, of the tag compose will pull: the model’s tag variable in .env when it sets one, the pin otherwise. A tag that is gone (“manifest unknown”) fails and names the model to re-resolve with chap-devops models enable <id>; a registry that refuses or fails without saying - a rate limit, a 5xx, unauthorized - is a warning that quotes it, because it says nothing about the tag. Skipped, with the reason on the line, under --offline, when there is no docker CLI to ask through, and when ghcr.io was unreachable. --offline is the reason given first, so the line reads the same on a machine with Docker and one without. |
registry pin <id> | is the commit the marketplace pins for each enabled model still the newest build that model’s own repository has published (one line per enabled marketplace model) | Warns when the catalogue is behind: the model repository has built a newer commit than the version this deployment’s channel resolves to, which is the state in which every deployment runs a fortnight-old image without anything saying so. No chap-devops command moves that pin - it lives in the marketplace, and chap-devops models add refuses an id the catalogue already lists - so the line asks for a new pin there. A commit the branch has not built yet, and one taken from a branch or a tag of its own, are ahead rather than behind and pass. The default branch, one commit listing and at most five registry lookups per model, run in parallel; a repository that will not answer, a rate limit and a branch with no published build are all skips with the reason on the line. Models added with chap-devops models add are left out, because manual <id> above already asks the same question. Skipped under --offline. See Channels and versions. |
user <model> | does the account the overlay runs each enabled model as still match what its image declares (one line per enabled model) | docker image inspect --platform linux/amd64 of the exact tag the overlay pins, read from the local daemon only - no network, so this line is answered under --offline too. Warns when the two have drifted apart, which is what a user: written by hand, or an image that changed its account, looks like: an image that runs as root under user: 1000:1000 starts fine and then fails on its own binaries. chap-devops models enable <id> reads the account off the image again, and chap-devops update does it for a model it moves to a new tag. Skipped when the amd64 variant of the tag is not in this machine’s image store - the line names the chap-devops docker pull that would put it there, which is also what an arm64 host holding only its own architecture is told - when the docker daemon is not answering, and when the image runs as an account name only the image itself can turn into numbers. See Data directories and users. |
image <component> | does each enabled component’s image exist | The same docker manifest inspect, of the tag OCS_IMAGE_TAG, S3_IMAGE_TAG or DHIS2_IMAGE_TAG in .env sets when it sets one, with the same split between a missing tag and a registry that did not say, and without the linux/amd64 question: every component image is multi-arch and no component pins a platform, so “the tag exists” is the whole answer. Skipped for the same three reasons. |
health | chap-core’s health and whether every model registered | Skipped when no container of this deployment is running, which is chap-devops up. Otherwise it is the verdict of chap-devops status as one line. A chap container that is up and failing its healthcheck is reported as such, with the last error line from its own log and the fix that goes with it. A deployment without chap-core is judged by its components instead. When every model is registered the line is ok and still carries a next step - chap-devops models test --all - because registration is only a heartbeat: the check that settles whether a model can predict has to run the models, which takes minutes and is therefore not something doctor does itself. See Testing a model. |
The components line
The line starts with the names of the enabled components. Then it adds a part
for ocs and a part for dhis2, if they are enabled. The line takes the worse
status of the two parts, so one part cannot hide the other.
For ocs:
- If
ocs/climate-service.yamlis missing, the line fails: the container starts with no instance configuration.chap-devops syncwrites the file again. - If the file still holds the Laos example values, the line warns. Edit the file and delete the note at the top.
- The line reports
ERA5-Land: credentials unset (WorldPop and CHIRPS3 work without them)when.envsets none of the data source variables. This is not a problem: WorldPop and CHIRPS3 need no credentials. - The line reports
plugins/: N fileswhenocs/plugins/exists. - While the
ocscontainer runs, the line also reports the same two facts thatchap-devops statusshows:3 datasetsfromGET /datasets?f=json, and212.0 MB datafromdu -sk /app/data. Both have a time limit, and the line leaves out a fact it could not get.
These facts come with the ok line and with the warning. The line under the
warning stays the one thing to do. A missing file shows none of the facts.
For dhis2:
- If
dhis2/dhis.confis missing, the line fails: DHIS2 does not start without it.chap-devops syncwrites the file again. - The line reports the seed:
none, the dump, ordefaultwith the dump thatdefaultgives. If chap-devops has no dump for the minor version, the line says that the database starts empty. An empty database is not an error. - If chap-core is enabled, the line reports the last
chap-devops dhis2 connect, orno `chap-devops dhis2 connect` recorded. This is not a verdict:doctordoes not ask DHIS2.chap-devops dhis2 showasks DHIS2.
A deployment with dhis2 enabled also gets the memory check. See
The seed and Components.
--json
The same checklist as one document: an array of checks, the counts they add
up to, and the closing lines in messages.
chap-devops doctor --json
{
"checks": [
{
"id": "docker-cli",
"name": "docker cli",
"status": "ok",
"detail": "docker 29.8.1",
"fix": null
},
{
"id": "api-port",
"name": "api port",
"status": "fail",
"detail": "8700 is in use by something else",
"fix": "port 8700 is already in use on this machine (needed by chap); free it, or set CHAP_API_PORT=8701 in `.env`"
}
],
"summary": { "ok": 1, "warn": 0, "fail": 1, "skip": 0 },
"messages": [
{ "level": "info", "text": "2 checks: 1 ok, 0 warn, 1 fail" }
]
}
id is stable, so a script can pick one line out of the report:
chap-devops doctor --json | jq -r '.checks[] | select(.status == "fail") | "\(.id): \(.detail)"'
Outside a deployment directory the project checks are simply absent from
checks, which is how a consumer tells the two cases apart.
--offline
--offline skips every check that would touch the network, reporting each as
skip with the reason, and the reason always names the flag:
skip network ghcr.io --offline: the image registry was not probed
skip network marketplace --offline: the model catalogue was not probed
skip network releases --offline: the chap-core release list was not probed
skip github api --offline: the GitHub API rate limit was not asked
skip chap-devops v0.102.0, aarch64-apple-darwin, cargo build; --offline: the release list was not asked
skip chap-core pin v2.4.0 pinned; --offline: the release list was not asked
skip image chapkit-simple-multistep-model --offline: the registry was not asked
skip registry pin chapkit_simple_multistep_model --offline, so the model repositories were not asked about newer builds
--offline is the reason on every one of those lines whatever else the
machine has: a host without a docker CLI has no image manifest to ask for
either, but the flag is the reason the user gave, so it is the reason
reported, and the missing CLI is already a failure on the docker cli line.
That makes the offline report the same everywhere, which is what a test or a
CI step can rely on.
Everything else still runs, so chap-devops doctor --offline is the whole local
half of the checklist on a machine with no way out.
Troubleshooting
Start with chap-devops doctor. It runs the whole checklist in one pass - Docker,
Compose, the architecture, free disk, the hosts Chap pulls from, and, inside a
deployment, the files, the ports, the pins, the images and whether Chap is up -
and prints what to do about every line that is not ok. Most of the sections
below are one of its lines with the reasoning spelled out. See
Doctor.
When a command does something you did not expect, run it again with -vv. It
prints every external command, every HTTP request with its status and timing,
which catalogue it loaded and which files sync compared, all on stderr and
all out of the way of --json. -d adds the response bodies and the resolved
project paths.
the restore of the DHIS2 seed stopped before the end
chap-devops up printed this under why dhis2-db is unhealthy:. The restore of the
seed dump into dhis2_db stopped with an error, so the database is
incomplete. The lines above the hint say why. Two causes are common:
- The dump is not a complete gzip file. Download it again.
- The dump needs a newer PostgreSQL than 16, the version of
dhis2-db. chap-devops removes the lines ofpg_dump17 that PostgreSQL 16 does not know (\restrict,\unrestrict,SET transaction_timeout), but a dump can use other new features.
Fix the cause, then start the restore again:
chap-devops components disable dhis2 --purge
chap-devops components enable dhis2
chap-devops up
--purge removes only the three DHIS2 volumes. seed: and seed_password:
in .chap-devops/components.yaml stay. Do not use chap-devops down --volumes for this:
it also removes the chap-core database. See
The seed.
A host port is already in use
chap-devops up refuses before it calls Docker, with one line per conflict:
error: 2 host ports this deployment needs are already in use; nothing was started
port 8700 is already in use on this machine (needed by chap); free it, or set CHAP_API_PORT=8701 in `.env`
port 5001 is already in use on this machine (needed by chapkit-simple-multistep-model); free it, or run `chap-devops models unexpose chapkit-simple-multistep-model` (the model stays reachable through chap-core) / `chap-devops models expose chapkit-simple-multistep-model --port auto`
or run `chap-devops up --no-preflight` to hand the conflict to Docker
Pick one of the three: free the port, move the API with CHAP_API_PORT in
.env, or move (or drop) the model’s port. Ports held by this deployment’s own
running containers are not conflicts, so chap-devops up on a running deployment is still
a no-op. chap-devops up --no-preflight hands the question back to Docker.
When the port belongs to another chap-devops deployment that is running, that one is named, with the command that stops it. A stopped deployment is not named:
1 host port this deployment needs is already in use; nothing was started
port 8790 (needed by ocs) is in use, and climate (/srv/climate) publishes it too; stop it with `chap-devops -C /srv/climate down`, or move this one: run `chap-devops components enable ocs --port 8791`
or run `chap-devops up --no-preflight` to hand the conflict to Docker
or run `chap-devops up --replace` to stop climate first
At a terminal it asks instead of stopping there:
climate is using these ports. Stop it and start this deployment instead? Its data is kept. [y/N]
y runs chap-devops down on the other deployment (its volumes and data stay; its
own chap-devops up brings it back) and then starts this one. chap-devops up --replace
is the same answer given in advance, for a script. The offer is only made
when every taken port belongs to another chap-devops deployment: a port some other
program holds is never chap-devops’s to take away.
The port answers, but it is not chap-core
chap-core down http://localhost:8700 v2.3.1 (pinned) auth: off
...
error: chap-core at http://localhost:8700 is not responding: port 8700 answers
but it is not chap-core (got text/html)
Something else holds the port: a dev server, a proxy, an older deployment.
chap-devops status reports it as down on purpose, because up has to mean that the
deployment works and not that the port is taken. Find the listener, or point
the deployment somewhere else with CHAP_API_PORT in .env.
this deployment has no container for `chap`, so there is no log
error: this deployment has no container for `chap`, so there is no log; start it with `chap-devops up`
chap-devops logs chap found no container for the service, so the service never
started and has no log. The command exits with status 1. Run chap-devops up, then
chap-devops logs chap again.
chap-devops status says the same thing about chap-core:
error: chap-core at http://localhost:8700 is not responding: ...; this deployment has no `chap` container yet, so start it with `chap-devops up`
Run chap-devops up.
this deployment's chap-core is not running
this deployment's chap-core is not running; http://localhost:8700 is first (/srv/first) answering on the same port; stop it with `chap-devops -C /srv/first down`, or run `chap-devops up --replace` here
Two deployments made with the same ports take turns on them, and the one that
is up answers on the port for both. chap-devops status asks docker whether this
deployment’s own chap container is running before it believes an answer on
its port: when it is not, the answer is the other deployment’s, and none of what
it says (health, version, registered models) is about this one. A browser on
http://localhost:8700 shows the other deployment for the same reason.
Stop the other one with the chap-devops -C ... down it names, or let this one take
the ports with chap-devops up --replace. chap-devops status --url asks whatever URL it
is given and skips this check. See
Several deployments on one machine.
A model is running but not registered
chapkit-rwanda-malaria-bym-model running, not registered via chap-core -
A model that started under two minutes ago is still starting, and registering
is the last part of that; chap-devops status says so for that row
(started under two minutes ago and registers once it is ready) and the fix is
to run it again in a minute. Past that, it is stuck.
chapkit tries to register with chap-core five times during its startup and then stops. A model that came up before chap-core was healthy therefore stays invisible until it is restarted:
chap-devops restart --all chapkit-rwanda-malaria-bym-model
chap-devops status
--all because nothing about the service has changed: it is running the image
and the configuration it should be, and the restart is only there to make it
introduce itself again. A plain chap-devops restart recreates what moved, which
here is nothing.
The overlay’s depends_on: chap: {condition: service_healthy} is there to stop
this happening in the first place, so a model in this state usually means
chap-core became unhealthy and came back after the model had given up.
If registration is failing rather than racing, and the deployment has authentication on, see the models stopped registering below.
chap-devops models test says a model failed
chapkit-rwanda-malaria-bym-model FAIL 14s predict: Error in file(file, "rb") : cannot open file 'model.rds': Permission denied
The row is one clause out of the model’s own error stream, which is as much as a row has room for. Three places have more, in the order worth reading them:
chap-devops models test chapkit_rwanda_malaria_bym_model -vv # the whole run, as it happens
chap-devops logs chapkit-rwanda-malaria-bym-model # what the service itself said
-vv streams everything chapkit test printed, which includes the phase that
failed, the diagnostic artifact it stored and the last lines of the model’s
stderr. chap-devops logs <service> is the other half: a model that cannot open a
file usually said so on startup too.
With --backtest the work happened inside chap-core, so the log is a job’s:
chap-devops jobs # the backtest that failed, newest first
chap-devops jobs logs <id> # the traceback and the model's own output
The failure row already names that command with the id filled in. See Jobs and the API.
The two levels fail for different reasons, which is what makes running both worth it:
-
Model level fails,
--backtestnot tried: the model itself is broken. Permission denied on its own files is the common one - seePermission deniedfrom a model’s own binaries- followed by a missing runtime library, which is a fault in the image and
needs a newer pin (
chap-devops update).
- followed by a missing runtime library, which is a fault in the image and
needs a newer pin (
-
Model level passes,
--backtestfails: the model works and the round trip does not. The usual cause is a covariate the model reads and does not declare, which shows up as aKeyErrorfrom chap-core rather than as anything the model said:chapkit-simple-multistep-model FAIL 8s KeyError: "['mean_relative_humidity'] not in index"The sample data is generated from the covariates the service declares (
chap-devops models info <id>, orchap-devops api GET /v2/services/<service_id>), so a model whose configured model in chap-core asks for more than that gets a frame without it. That is a fault in the model’s own declaration, not in the deployment. -
Both fail the same way: the model, again. Fix it at the model level, where the loop is seconds rather than minutes.
A --seed N makes the generated data the same on every run, which is what to
add when a failure only happens sometimes.
an MLproject service has no `chapkit test`
rain-model skip 1s an MLproject service has no `chapkit test`
go through chap-core with `chap-devops models test rain_model --backtest`
The image serves an MLproject file with chapkit mlproject run, as the
mlproject-* types of chap-devops models new do. The image has chapkit, but
chapkit has chapkit test only in a chapkit service project. A newer image
does not change this. Test the model through chap-core with --backtest, or
make the model a chapkit service with chap-devops chapkit mlproject migrate DIR. See
Migrate an MLproject model.
`chapkit test` in this container cannot reach a model service
chap-devops chapkit runs chapkit in a container that is on no network of a model,
and the container stops when the command ends. So chapkit test has no
service to test. Use chap-devops models test ID: it runs chapkit test in the
model’s own container. For a model image that you built, start it first with
chap-devops run IMAGE.
No Python (.py) or R (.r/.R) scripts at the project root or in the entry-point commands; cannot pick a base image.
chapkit mlproject migrate selects the base image from the scripts at the root
of the model directory and the scripts that the train and predict commands
name, and found none. Give the image with --base-image: chapkit-py,
chapkit-r, chapkit-r-tidyverse or chapkit-r-inla. chap-core v2.5.0 and
older look at the root only, and say No Python (.py) or R (.r/.R) scripts at the project root; there, the mlproject-* templates need --base-image, or
--tag v2.5.1. See
Migrate an MLproject model.
Only ShellModelRunner services can be exported
chapkit mlproject export writes an MLproject with train and predict
commands, so the service must run scripts. A model in main.py, as in the
fn-py template, has no commands to write. Move the model to scripts, as in
the shell-py template, or keep it a chapkit service.
No such file or directory: 'config.yml' from chap-devops chap
The MLproject came from chapkit mlproject export. Its commands read a
config.yml that chapkit writes, and chap writes it only with
--run-config.as-chapkit. Add that option, or serve the model with
chap-devops chapkit mlproject run DIR. See
Export a service as an MLproject.
the MLproject service on ... stopped, because its container got SIGTERM
chap-devops chapkit mlproject run served a model, and something outside chap-devops
stopped its container: docker stop, or a restart of Docker. chapkit shut
down normally, and --rm removed the container. To serve the model again, run
the same chap-devops chapkit mlproject run command.
has changes that are not committed, and the service reports the last commit
chap-devops chapkit mlproject run gives chapkit the git commit of the model
directory. chap-core stores each version of a model with one revision, so a
change that is not committed would run under the revision of the last commit.
- Commit the changes.
- Raise
versioninMLproject. - Run
chap-devops chapkit mlproject run DIRagain.
is not a git checkout, so chap-core cannot store the model
The service registers with chap-core, but chap-core stores a model only with
a git revision. Without it, chap-devops models configs sync and
chap-devops models test ID --backtest fail with
is stored from revision None. In the model directory, run git init,
then git add . and git commit. Then run chap-devops chapkit mlproject run DIR
again.
chapkit did not accept the arguments, and its usage is above
chapkit exited with status 2, which is a usage error: a command, an argument
or an option is missing or wrong, for example an option that the command does
not have. The usage that chapkit printed is above the line. The line names the
help to read, such as chap-devops chapkit artifact --help.
A group with no command after it, chap-devops chapkit artifact or
chap-devops chapkit mlproject, is not an error: chap-devops asks chapkit for the help of
the group.
could not write DIR/uv.lock, and the `Dockerfile` copies it
chapkit init and chapkit mlproject migrate write a Dockerfile that copies
uv.lock. chap-devops runs uv lock in the chap-core image to write it, and this
time it failed. Without the file, docker build fails with
"/uv.lock": not found.
- Run
chap-devops chapkit --helpto make sure that the image runs. - Run
chap-devops chapkit initorchap-devops chapkit mlproject migrateagain on a new copy.uv lockneeds the network to read PyPI. - If it fails again, run
uv lockinDIRon a machine that has uv.
is stored from revision ..., but its source now reports revision ...
dengue-lags FAIL 8s ValueError: Model template 'dengue-lags' version '0.1.0' is stored from revision 'db139b6b09185e0be0971d31cafb39f126c910...
chap-core stores each version of a model template only once, with the git
revision of the image that first registered it. A new image under the same
version reports another revision, and chap-core refuses to run it. The model
still registers, so chap-devops status shows it as registered.
- Raise the version of the model:
versioninmain.py, orversioninMLproject. - Commit, and build the image again with
GIT_REVISION. - Run
chap-devops restart. - Run
chap-devops models test ID --backtest, orchap-devops models configs sync ID. This creates the configured models of the new version.
already exists and is not empty; give a new directory
chap-devops models new DIR writes a project only into a directory that is not
there, or that is empty. It writes no file when the directory has files.
Give another directory, or remove the files in it first.
could not delete /v1/crud/datasets/N (HTTP 500 Internal Server Error)
chap-devops models test --backtest deletes the dataset it made, and chap-core
refused, because a backtest still points at it. From chap-core v2.5.0, the
built-in baselines write backtests on the dataset, and chap-devops deletes them
first. If a baseline was still running a minute after the test, chap-devops could
not delete its backtest yet.
- Run
chap-devops jobs, and wait until norun_baselinejob is running. - Run
chap-devops api GET /v1/jobs, and note theresultof eachrun_baselinejob: it is the id of its backtest. - Run
chap-devops api DELETE '/v1/crud/backtests?ids=ID,ID,ID'with these ids. - Run the
chap-devops api DELETE /v1/crud/datasets/Ncommand that the warning names.
chap-core has no configured model for
SERVICE skip 0s chap-core has no configured model for SERVICE VERSION
it is registered, and chap-core has nothing to run it with; run `chap-devops models configs sync ID`, then `chap-devops models test ID --backtest`
The model is registered, but chap-core has no configured model for it. chap-core
2.4 and later makes no configured model from a registration, so a model without
one cannot run, and the Modeling App does not list it. chap-devops status shows such
a model as registered, not configured.
SERVICE is the service id, ID the model id, and VERSION the version that the model registered
with. A backtest uses only a configured model of that version that chap-core
has not archived. If the model registered with a new version, the configured
models of the old version do not count, because chap-core does not run them.
For a model registered from outside the deployment, chap-devops status then warns
chap-core has configured models of it of other versions only.
chap-devops models test --backtest makes the configured models itself before it
starts. So this skip comes only when that step failed, and the lines above the
table say why.
Make the configured models, then test again:
chap-devops models configs sync ID
chap-devops models test ID --backtest
See Configured models.
If chap-devops models configs sync says that it could not create the configured
models and the reason is answered HTTP 409 Conflict, chap-core refused the
template. The model reports no git revision, or another revision than the
template chap-core stored for its version.
- Read the reason in
chap-devops logs chap. - If the reason says
is stored from revision None, the model image reports no git revision. Seeit reports no git revision. - If the model image changed, run
chap-devops updateto move the pin, or runchap-devops restart --all <service>to make the model register again. - Run
chap-devops models configs syncagain.
could not create its configured models
warning: chapkit_ewars_model: could not create its configured models: ... (after monthly_climate); the template version it stored is retired again, with the configured models made so far; run `chap-devops models configs sync chapkit_ewars_model` again, and `chap-devops logs chap` says why
chap-core refused a configured model of the marketplace entry, or stopped answering. chap-devops retired the template version that it stored, so the model has no configured model of this version that is half done.
- Read
chap-devops logs chapfor the reason. - Run
chap-devops models configs sync IDagain. The run starts again from the template.
If the line says retiring the template version it stored failed too, the
configured models made so far are still live. Run the chap-devops api DELETE
command in the line, then run chap-devops models configs sync ID.
chap-core has no configured model NAME for
chapkit-simple-multistep-model skip 0s chap-core has no configured model weekly for chapkit-simple-multistep-model 0.1.2; it has monthly_climate, monthly_selfhistory, short_lags
run `chap-devops models configs list chapkit_simple_multistep_model` to see its configured models
chap-devops models test --backtest --config weekly asked for the configured model
weekly, and the model has no such configured model of the version it
registered with. The line names the variant names that it has.
- Run
chap-devops models configs list IDto see the configured models. - Run the test again with one of the names after
--config. - To test a new configuration, add it with
chap-devops models configs add ID.
With --all, --config applies to each model. A model without that
configured model is a skip, and the other models are tested.
chap-core did not list its configured models, so chap-devops cannot find --config
SERVICE skip 0s chap-core did not list its configured models, so chap-devops cannot find --config weekly
run `chap-devops status` to see why
chap-devops asked chap-core for its configured models
(GET /v1/crud/configured-models), and chap-core did not answer with a list.
Without the list, chap-devops cannot find the configured model that --config
names.
- Run
chap-devops status. - If chap-core is not
up, readchap-devops logs chap. - Add
-vvto the test. The trace on stderr then shows what chap-core answered.
Without --config, the backtest does not skip. It sends the service id, and
the line under the row says (by name, as chap-core could not list them).
it reports no git revision, so running this again does not help
warning: my_model: could not create its configured models: ...; it reports no git revision, so running this again does not help; build its image again with `--build-arg GIT_REVISION=$(git rev-parse HEAD)`, then run `chap-devops restart`
For a model registered from outside the deployment, the line ends differently:
warning: my-model: could not create its configured models: ...; it reports no git revision, so running this again does not help; set `GIT_REVISION` where it runs, then start it again
chap-devops models configs sync (and the same step in chap-devops up --wait and chap-devops models test --backtest) asked chap-core for the model template, and chap-core
refused it. The model reports no git revision, and chap-core 2.4 stores no
template for such a model. In --json, the model has the state no-revision.
chap-devops status gives the same way out on the line under the table.
A model reports no git revision when its registration has git_revision: null. On chap-core 2.4 and later, a registration with no git_revision key
counts too. chapkit 2.1 leaves out the key when it has no revision.
- In the model’s checkout, build the image again with
docker build --platform linux/amd64 --build-arg GIT_REVISION=$(git rev-parse HEAD) -t my-model:dev .. - Run
chap-devops restart. - Run
chap-devops models configs sync.
For a model that chap-devops does not run, set GIT_REVISION where the model runs,
then start it again.
there is no terminal here; name the configuration with --name and its values with --set KEY=VALUE
error: there is no terminal here; name the configuration with --name and its values with --set KEY=VALUE, or run it at a terminal for the form
chap-devops models configs add ID without options opens a form, and the form
needs a terminal. In a pipe, in a script or with --json, there is no
terminal, so the command exits with status 2. update gives the same error
with --set, --unset and --covariates as the way out.
- Give the values as options:
chap-devops models configs add ID --name NAME --set KEY=VALUE. - Or run the command in a terminal to get the form.
has a configured model NAME already; give another --name, or archive it first
error: chapkit_simple_multistep_model has a configured model monthly_climate already; give another --name, or archive it first with `chap-devops models configs archive chapkit_simple_multistep_model monthly_climate`
A configured model with this variant name is live. If chap-devops sent the new one, chap-core would show the new one and hide the old one, and give no message. So chap-devops refuses the name and exits with status 2.
- To keep both, give another
--name. - To change the values of the existing one, use
chap-devops models configs update ID NAME. - To replace it, archive it first with the command in the message.
add --from FILE gives the same refusal, with “remove it from FILE” as the
other way out.
is not an option of ID; its options are
error: `lag` is not an option of chapkit_simple_multistep_model; its options are n_samples (integer), n_target_lags (integer), rf_max_depth (integer), rf_min_samples_leaf (integer)
error: `two` is not a value of `n_target_lags`, which takes an integer, such as `3`
chap-devops checks each --set against the options of the model template before it
asks chap-core. chap-core does not check the values of a chapkit model, and
chapkit accepts keys that it does not know. Without the check, a wrong key
would have no effect, and a wrong value would show only when a run fails.
- Use a key from the list in the message.
- Give a value of the kind in the message. Write a list with commas, such as
n_lags=2,2.
takes only these additional covariates
error: my_model takes only these additional covariates: rainfall, mean_temperature; remove `foo`
error: my_model takes no additional covariates; remove --covariates
error: `population` is a required covariate of chapkit_ghr_model, which every run gets; remove it from --covariates
The model template does not allow free covariates, so a configured model can
use only the defaults of its marketplace entry. A required covariate is never
in --covariates, because every run gets it. Remove the covariate that the
message names.
an export is the configurations of one model; name one of
error: an export is the configurations of one model; name one of auto_arima_chapkit, chapkit_ghr_model, chapkit_rwanda_malaria_bym_model, chapkit_simple_multistep_model
The deployment runs more than one model, and chap-devops models configs export
got no ID. An export goes under one marketplace entry, so it holds one model.
Run the command again with one of the ids in the message.
the new values are live, but the old row N was not archived
warning: the new values are live, but the old row 104 was not archived: ...; archive it with `chap-devops api DELETE /v1/crud/configured-models/104`
chap-devops models configs update creates a configured model with the new values,
then archives the old one. The first step worked and the second did not. So
chap-core may show two configured models with this variant name. Run the
chap-devops api DELETE command in the message to archive the old row.
unknown model `X`; run `chap-devops models list` to see the model ids
error: unknown model `nosuch`; run `chap-devops models list` to see the model ids
The marketplace has no model with that id, and no model added with chap-devops models add has it. The command exits with status 1. Run chap-devops models list
and use an id from the ID column. Both the marketplace id
(chapkit_simple_multistep_model) and the service id
(chapkit-simple-multistep-model) work.
chap-devops 0.102.0 and older say this for chapkit_ewars_model on a plain
chap-devops init. The marketplace no longer lists CHAP-EWARS, and those versions
enabled it when --models was not given. Update chap-devops with chap-devops self update. Or give the models by name, for example chap-devops init mychap --models chapkit_simple_multistep_model.
401 from the Modeling App
{"detail": "Missing or invalid API token"}
The token DHIS2’s chap route is sending is not the one chap-core is
enforcing, or the route sends none. chap-devops dhis2 connect finds out by asking
through the route, and writes the token this deployment holds into it:
chap-devops dhis2 connect
chap-devops dhis2 show reports the same thing without changing anything:
the `chap` route does not carry chap-core's API token.
Two ways to get here. Either chap-devops auth rotate was run and the clients were
never updated, which is the second half of rotating; or chap-devops auth enable was
run and chap-devops up was not, so chap-core is still running without the token
while .env already has one. chap-devops status tells the two apart:
chap-core up, token rejected http://localhost:8700 v2.3.1 (pinned) auth: on
error: chap-core at http://localhost:8700 is up and did not accept the API token:
port 8700 answers /v2/services with HTTP 401: the API token in .env is not accepted
means the running chap-core has a different token - it is up, which is why no container log is printed under it and no model table: the registry is behind the same token, so whether a model registered cannot be read - and
chap-core up http://localhost:8700 v2.3.1 auth: on
means .env and the running container agree, so the mismatch is in the client.
Either way chap-devops up is what hands a changed .env to the containers. See
Authentication.
The models stopped registering after I turned authentication on
chapkit-simple-multistep-model running, not registered via chap-core -
A protected chap-core rejects an unauthenticated registration like any other
request, so a model that came up without the shared secret never appears in
GET /v2/services. chap-devops auth enable writes SERVICEKIT_REGISTRATION_KEY
alongside the API token for exactly this reason, and re-renders every overlay
to pass it on, but the containers only read .env when Compose creates them:
chap-devops up
chap-devops status
If it persists, check that both ends carry the line:
grep SERVICEKIT_REGISTRATION_KEY compose.chapkit-simple-multistep-model.yml compose.chap-devops.yml
An active SERVICEKIT_REGISTRATION_KEY: ${SERVICEKIT_REGISTRATION_KEY:-} in
the overlay is what the model sends; a commented one means
.chap-devops/project.yaml does not know the project has a key, which
chap-devops auth show will report as a mismatch and chap-devops auth enable puts right.
The same line in compose.chap-devops.yml is what chap-core checks it against.
Upstream’s compose.ghcr.yml passes only CHAP_API_TOKEN into the chap
service, so without that line the container has no key, and the model’s
X-Service-Key is rejected as an invalid API token - a 401 in
chap-devops logs chapkit-simple-multistep-model. chap-devops sync then chap-devops restart puts the
line back.
unable to open database file
sqlite3.OperationalError: unable to open database file
Docker seeds a fresh named volume from whatever the image has at the mount point, ownership included, so an image that never creates its data directory yields a root-owned volume the unprivileged model cannot write to.
Every model overlay ships a one-shot <service_id>-init container that chowns
the volume to the model’s numeric uid:gid before the model starts (0:0 for a
root image), so this is fixed by construction. If you see it anyway:
- the overlay was hand-edited, or the init container was removed. Run
chap-devops syncto render it again. --usernames an accountchap-devopsdoes not know, so it fell back to1000:1000.chap-devops syncwarns when it does. Pass the numbers instead:chap-devops models enable ID --user 1001:1001.- the model writes somewhere other than its data directory, on the read-only
root filesystem.
--data-dirpoints the volume at the right path; see Data directories and users.
A model that crash-loops right after starting is almost always one of the last two.
Permission denied from a model’s own binaries
sh: /usr/local/lib/R/site-library/INLA/bin/linux/64bit/inla.run: Permission denied
The model started, registered, and then could not execute a file it ships itself. The overlay is running it as an account the image does not use.
Some model images end their Dockerfile on USER root and keep their binaries
root-owned and not world-executable - the Rwanda BYM model’s INLA binaries were
mode 744 up to 0.1.1 - so an overlay that hardens such an image down to
user: 1000:1000 takes away the one permission it needed. The container comes
up either way, which is why this surfaces as a failed prediction rather than a
failed start.
chap-devops models enable <id> reads the account off the image again and rewrites
the overlay; chap-devops up then restarts the service with it:
$ chap-devops models enable chapkit_rwanda_malaria_bym_model
$ chap-devops up
The rendered overlay should then carry no user: line for a root image, and
its <service_id>-init container should chown the volume to 0:0. For an
image that does drop to an account of its own - which Rwanda BYM 0.1.2 now
does, as chapkit - it carries that account’s uid:gid instead, and the init
container chowns to the same two numbers.
chap-devops models info <id> shows what was recorded and where it came from, and
chap-devops doctor has one user <service> line per enabled model that compares
the two whenever the image’s amd64 variant is pulled here.
--user <uid>:<gid> overrides the image, for the rare case where the image is
wrong about itself. See
Data directories and users.
dependency failed to start: container ... is unhealthy
chap-devops up reads that line and then says why, out of the failing container’s
own log:
dependency failed to start: container demo-1ab2c3-chap-1 is unhealthy
why chap is unhealthy:
sqlalchemy.exc.OperationalError: (psycopg2.OperationalError) connection to server at
"postgres" (192.168.32.2), port 5432 failed: FATAL: password authentication failed for
user "chap"
ERROR: Application startup failed. Exiting.
the database volume holds a different password than .env (a previous deployment with the
same name, or --fresh-env); the `volumes` line of `chap-devops doctor` says which, and
`chap-devops down --volumes` removes the volume if this deployment's data can go
The same lines close chap-devops status and chap-devops doctor’s health check, and
the chap-core line reads down (container unhealthy) rather than plain down:
the container is there, and it is the container that is wrong.
The two causes worth knowing by name are below. chap-devops logs chap has the rest.
password authentication failed for user "chap"
The PostgreSQL volume holds a role password that is not the one in .env.
There are two ways to get there.
A volume from another deployment of the same name. The volume was made
by an earlier deployment with the same compose project name, and it still has
that deployment’s role password. The new deployment’s .env has a freshly
generated password. chap-devops doctor says so:
warn volumes the database volume demo-1ab2c3_chap-db predates this deployment; if chap-core
cannot log in, it belongs to an earlier deployment with the same name
remove it with `chap-devops down --volumes` if this deployment's data can go, or keep both
by giving one of them a name of its own
chap-devops init gives each deployment a name of its own (demo-1ab2c3), so this
happens only when two deployments record the same name; see
the compose project name.
init --fresh-env. It rotates the PostgreSQL password on purpose, and the
existing volume was created with the old one.
Either way: drop the volume and start over,
chap-devops down --volumes
chap-devops up
or change the role to match the new password with ALTER USER inside the
running postgres container:
chap-devops docker exec postgres psql -U chap -d chap_core
This is why init never rewrites a .env it finds, --force included. See
the .env contract.
no matching manifest for linux/arm64
The marketplace images and chap-core are published for amd64 only. Every
overlay chap-devops writes pins platform: linux/amd64, so an arm64 host such as
Apple silicon pulls that variant and runs it under emulation instead of
failing.
Seeing this error means the pin is missing: a hand-edited overlay, or a compose
file not written by chap-devops. Run chap-devops sync to render the overlays again, and
check chap-devops docker config for the service that has no platform.
Compose is older than 2.24.4
warning: docker compose 2.18.1 is older than 2.24.4; compose.chap-devops.yml uses `!override`, which needs 2.24.4 or newer, and compose.marketplace.yml uses `include:`, which needs 2.20.0
compose.chap-devops.yml uses !override, which arrived in Compose 2.24.4, and
compose.marketplace.yml uses include:, which arrived in 2.20. chap-devops warns
rather than failing, because the base services still run.
What an older Compose loses, in order: below 2.24.4 the API port override is
merged into chap-core’s own ports: instead of replacing it, so the API is
published on two host ports; below 2.20 the model overlays do not load at all.
Upgrade Docker Compose.
needs chap-core, and this deployment has no chap-core
error: `chap-devops jobs` needs chap-core, and this deployment has no chap-core; `chap-devops components enable chap-core` adds one, or `chap-devops components enable chap-core --url URL` names one elsewhere
The deployment was created without chap-core (--without chap-core), so there
is no chap-core to ask. chap-devops jobs, chap-devops api (without --url),
chap-devops models test --backtest and chap-devops update --chap-tag all say this
(chap-devops models test without --backtest does not need it). If you meant to
talk to a chap-core somewhere else, chap-devops api --url <base> sends the request
there. Otherwise add chap-core with chap-devops components enable chap-core, then
chap-devops up. See Standalone OCS.
registration.missing_orchestrator_url in a model’s log
[error ] registration.missing_orchestrator_url [servicekit.api.registration] env_var=SERVICEKIT_ORCHESTRATOR_URL
Expected in a deployment without chap-core, and harmless. The overlay leaves
SERVICEKIT_ORCHESTRATOR_URL out on purpose because there is nothing to
register with. servicekit logs this line at error level, then skips registration
and serves as usual. chap-devops status asks the model’s own /health, which is the
check that matters there. In a deployment with chap-core the line means the
overlay is out of date: run chap-devops sync, then chap-devops restart.
running and not answering on /health
1 of 1 model is running and not answering on /health; a model that just started answers in a minute, so run `chap-devops status` again, or read `chap-devops logs chapkit-simple-multistep-model`
A deployment without chap-core asks each model’s own /health on its host
port. A model image can take a minute to start, so ask again first. If it stays,
chap-devops logs <service_id> shows why the service did not come up. A model enabled
with --port none has no host port to ask and is judged by its container alone.
App never became ready, skipping registration
[error ] registration.aborted [servicekit.api.service_builder] message='App never became ready, skipping registration' port=5001
Before servicekit registers a model, it checks that the app answers inside the container. With a chap-core elsewhere, chap-devops reads the image’s command when it enables the model, and decides where the app listens:
- An image that starts with
--port 8000listens on 8000, and its host port maps to 8000. - An image whose command names no port gets
PORTset to its host port.
So this message means that chap-devops could not read the command, for example
because the model was enabled --offline, or that the image does something
else. Enable the model again with a network (chap-devops models enable <id>), then
run chap-devops up. chap-devops logs <service> shows the port that servicekit checked.
not registered with ... from chap-devops run
chap-devops run --chap-core URL started the model, and the chap-core at URL did
not list it within 30 seconds. The model’s log says why: run the
chap-devops -C ... logs command that the message names.
registration.attempt_failedwithName or service not knownorConnection refused: the model cannot reach chap-core. Check that chap-core runs, and that it listens on0.0.0.0when it is a process on this machine.App never became ready: see the entry above.- A 401: chap-core asks for a registration key, and the model has none. See Authentication for how a deployment passes the key to its models.
registered, unreachable / chap-core cannot reach it at
chap-devops status asks chap-core’s proxy for each registered model’s /health
(/v2/services/<id>/run/health). A model is registered, unreachable when
that fails with a 5xx or no answer: the model registered, and chap-core cannot
call it back at the URL it registered under. Every backtest and prediction
through chap-core would fail the same way. The line under the table names the
URL and what came back, and the fix depends on where chap-core runs; see the
next entry.
A model registers with an external chap-core, and its jobs fail to connect
The model shows as registered in chap-devops status, but a job fails with a
connection error to localhost:<port> or host.docker.internal:<port>. The
registration worked, and chap-core cannot reach the address the model
registered under. That address is models_host in .chap-devops/components.yaml
plus the model’s host port:
- chap-core runs as a process on this machine:
localhostis right. - chap-core runs in a container: run
chap-devops components enable chap-core --url URL --models-host host.docker.internal, thenchap-devops up. - chap-core runs on another machine: use the name or address it reaches this machine by, and make sure the model ports (5001 up) are open to it.
The same applies the other way round, to a model you run from its checkout
against a chap-core in chap-devops: SERVICEKIT_HOST must be host.docker.internal
and the model must listen on 0.0.0.0. See
Your model from its checkout, with Chap.
publishes no public image at
chap-devops models add https://github.com/OWNER/REPO runs the image that
repository publishes on ghcr.io, and ghcr refused to hand out an anonymous pull
for it. It answers the same for a package that is private and for one that was
never published, so either way there is nothing chap-devops can pull from that URL.
Build the image from a checkout and add it by name:
docker build --platform linux/amd64 -t my-model:dev .
chap-devops models add my-model:dev
See A model image you built yourself. If the package exists and is private, making it public on GitHub (the package’s settings) is the other way.
is not in the local image store for linux/amd64
error: my-model:dev is not in the local image store for linux/amd64; build it with `docker build --platform linux/amd64 -t my-model:dev .` in the model's checkout, then add it again
chap-devops models add read my-model:dev as a local image, because it names no
registry, and the local store has no amd64 build under that name. Either it was
never built, it was built for another platform (a plain docker build on an
Apple Silicon Mac makes an arm64 image), or the tag differs. Run the command in
the message. If you meant a published image, give its full reference, such as
ghcr.io/my-org/my-model:sha-1eb8cf1.
is not a chap-core checkout
error: /Users/me/src/chap is not a chap-core checkout: it has no Dockerfile.worker; pass the directory you cloned github.com/dhis2-chap/chap-core into
chap-devops init --source builds the chap-core API from Dockerfile, the worker
from Dockerfile.worker, and renders compose.yml from compose.ghcr.yml, all
at the top of the checkout. Point it at the directory git clone created, not
a subdirectory. A very old chap-core may predate one of the three files; check
out a newer commit.
another chap-devops command is changing this deployment; waiting for it to finish
Another chap-devops command on the same deployment holds the state lock (see
One change at a time), and this one waits
until it exits. It is usually a chap-devops up rendering its files or an
chap-devops update checking the marketplace, and it ends on its own. If it does not,
find the command still running against this directory (ps aux | grep chap-devops)
and let it finish or stop it; the lock goes with the process.
did not start: the registry answered denied for IMAGE, so the image does not exist or is private
error: my-model did not start: the registry answered `denied` for ghcr.io/my-org/my-model:dev, so the image does not exist or is private; check the reference, or run `docker login ghcr.io`, then `chap-devops run ghcr.io/my-org/my-model:dev` tries again
chap-devops run asked compose to start the model, and the registry refused the
pull. A registry gives the same answer for an image that does not exist and for
a private image.
- Check the reference. For a marketplace id, use
chap-devops models search. - If the image is private, run
docker login <registry>with an account that can read it. - Run the
chap-devops runcommand that the message names.
did not start (...) from chap-devops run
chap-devops run asked compose to start the model and compose refused; the
parentheses hold the line compose said it with. A denied from the registry
has its own message; see the entry above. port is already allocated is a
host port something else took between the pick and the start; run again. The
model was taken back out, so the chap-devops run the message names starts afresh.
leftovers: N volumes of M removed deployments still in docker
chap-devops doctor found volumes of deployments whose directory is gone. Nothing
uses them and no chap-devops command would name them again, so they only take disk
space. chap-devops cleanup --dry-run lists them, and chap-devops cleanup deletes them
after asking. A deployment you moved rather than deleted counts as removed
until a chap-devops command runs in its new directory; run chap-devops sync there first.
See Cleaning up after removed deployments.
--json cannot ask before deleting data from chap-devops cleanup
chap-devops cleanup deletes data and asks first, and a --json run (or one with
no terminal) has nobody to ask. chap-devops cleanup --dry-run lists what would go;
chap-devops cleanup --yes deletes it without asking.
volume X is still there; remove it with `docker volume rm X`
chap-devops down --volumes removed the volumes that compose declares and the
leftover volumes that docker labels with this deployment’s compose project.
A volume with the deployment prefix that has no such label, or that a
container still uses, stays. The warning names each one, with the reason in
parentheses when docker gave one. If you do not need its data, run the
docker volume rm command in the line.
If the reason is volume is in use, a container still holds the volume. A
backup or restore that was killed can leave its reader container behind, with
a name that starts with chap-devops-volume-. docker ps -a --filter volume=X shows
the container. Remove it with docker rm -f NAME, then run chap-devops down --volumes again. See Taking a backup.
docker would not remove volume ... from chap-devops stop --purge
Purging a group removes every volume compose made for it, and docker refused
one, usually because a container still uses it. The group was kept, so nothing
is left behind that chap-devops cannot reach. docker ps -a --filter volume=<volume>
shows what holds it; stop that, run the docker volume rm the message names,
then the same chap-devops stop --group <group> --purge again.
resuming ..., left paused from chap-devops up
A service of this deployment was paused, which is what a chap-devops backup create that was killed while it held the service still leaves behind. chap-devops up resumed it before starting anything else; nothing more to do. Before that,
chap-devops status shows the service as paused, with the warning dhis2 is paused, so it does not answer; run `chap-devops up` to resume it.
there is no marketplace model ...
chap-devops run was given a word with no tag, which is a marketplace id, and the
catalogue has no such id. chap-devops models search <word> finds the right one. A
local image needs its tag: chap-devops run my-model:dev.
not ready after 300s
chap-devops up --wait gave up: the names after the colon are what never answered,
each with its chap-devops status state. chap-core means its /health never
answered; see this deployment's chap-core is not running.
running, not registered is a model whose container is up and that never
registered; see A model is running but not registered.
running, not answering is a model without chap-core whose /health never
answered on its host port: chap-devops logs <service> says why. A first start that
pulls large images can simply take longer than the default; --timeout 900
waits fifteen minutes.
A hand edit disappeared
chap-devops sync re-renders the artifacts from .chap-devops/, so an edit to
compose.yml, compose.chap-devops.yml, compose.marketplace.yml or a
compose.<service_id>.yml is drift that the next sync (and therefore the next
chap-devops up) undoes.
- To change the base services, edit
.chap-devops/compose.chap-core.<tag>.yml.syncfollows it, and says that the recorded checksum no longer matches. - To change a model, edit
.chap-devops/models.yamland runchap-devops sync. - To add something of your own, write a
compose.custom.yml.synconly ever removes overlays it wrote itself, so yours is left alone; add it to the-flist or to the umbrella by hand if you want it included.
chap-devops sync --check reports drift without writing, and exits non-zero, which
makes it a usable pre-commit or CI check.
scheduler.dataset_sync is no longer supported
OCS does not start, and chap-devops logs ocs shows this line. The current OCS
keeps its schedules in its data volume, not in ocs/climate-service.yaml.
- Write down the datasets and the times in the
dataset_sync:list. - Remove the
dataset_sync:list fromocs/climate-service.yaml. Keepenabled: true. - Run
chap-devops restart ocs. - Make the schedules again in the Schedule tab of each dataset in the OCS web UI. OCS does not import the old list.
See Schedules.
An OCS dataset plugin does not appear after a restart
You put a plugin in ocs/plugins/datasets/, ran chap-devops restart - or
chap-devops restart --all ocs - and OCS still serves the datasets it served before.
Nothing failed: the plugin directory was never mounted.
The mount is rendered from the directory being there, and chap-devops up is the only
wrapper that re-renders the compose files before it calls Docker. On a deployment
where ocs was enabled before ocs/plugins/ existed, compose.ocs.yml carries
no plugin mount, so chap-devops restart compares the container against a file it
already matches and does nothing at all.
chap-devops up # syncs first, then recreates ocs
chap-devops docker exec ocs ls /app/plugins/datasets # the plugin is inside
chap-devops doctor finds this on its own, on the compose files line, which is
chap-devops sync --check:
warn compose files out of date with .chap-devops/ (2 to write, 5 unchanged, 0 to remove)
run `chap-devops sync`, or `chap-devops up`, which syncs first
Two files, because the mount in compose.ocs.yml and the plugins_dir key in
ocs/climate-service.yaml are written by the same sync.
chap-devops restart ocs is the right command for a change to
ocs/climate-service.yaml, and for the opposite reason: that file is a bind
mount, so its new text is already inside the container and the compose files have
not changed - all that is wrong is that OCS read the old text at startup, and
restart recreates a service whose mounted config is newer than its container. A
directory that did not exist when the compose file was rendered is the other
case: the file itself is out of date, and recreating a container cannot mount
what the file does not mention. See
Adding a plugin to a running deployment.
A DHIS2 that answers but 404s every API request
The login page loads, chap-devops status may even say dhis2 up, and every
/api/* request answers 404. Nothing in the log says “error”.
DHIS2 migrates its schema forward only, so this is almost always an older
image started against a database a newer one has already migrated. The Spring
context fails to come up, Tomcat keeps running and keeps serving pages, and every
API route is simply not registered. The other two causes have the same shape: a
dhis.conf DHIS2 could not read, and a Flyway checksum that does not match the
database.
chap-devops logs dhis2 | grep -iE "flyway|migrat|exception|failed"
chap-devops docker config | grep "image: dhis2/" # the tag that is running
grep -n "image_tag" .chap-devops/components.yaml # the tag that was asked for
grep -n "DHIS2_IMAGE_TAG" .env # and any override, which wins
Put the tag back where the database is, rather than migrating further:
chap-devops backup create # before anything, if there is data worth keeping
chap-devops components enable dhis2 --tag 2.42 # the version of the database; or fix DHIS2_IMAGE_TAG in .env
chap-devops up
If the log says that the migration of the schema failed, the database is from
an older DHIS2. chap-devops up then says:
DHIS2 could not migrate the database to its version; a database from an older DHIS2 must go up one version at a time, so run `chap-devops components enable dhis2 --tag VERSION` with the next version, then `chap-devops up`, and repeat
There is no way back down a migration. A database migrated by 2.42 does not work
under 2.41 again, so the choice is the newer image or a restore. chap-devops warns
before it happens - the warning names chap-devops backup create first for exactly this reason:
warning: the DHIS2 image moves from 2.42 to 2.41 and `dhis2_db` is already there: DHIS2 migrates a schema forward only, so run `chap-devops backup create` first - an older image on a migrated database answers healthy while every API request 404s
A deployment with nothing in DHIS2 worth keeping is quicker to start again:
chap-devops components disable dhis2 --purge takes all three volumes, and the next
chap-devops up migrates or restores from scratch. See
Changing the DHIS2 version.
DHIS2 never becomes healthy
Two quite different things look the same from outside, and the first start is where both show up.
It is still migrating. DHIS2 migrates its whole schema before it serves a
request, and on a seeded deployment it restores a dump first. That is about 40
seconds on a native arm64 host with an empty database and 8 to 15 minutes
under emulation. The health check allows 240 seconds of start_period and 60
retries for that reason. Watch it rather than restarting it:
chap-devops logs -f dhis2
It ran out of memory. DHIS2 wants roughly 4 to 5 GB for the analytics
populate phase, and below that the JVM is SIGKILLed part-way through. That
failure looks like anything except memory: a container that exits with no Java
exception in the log, an analytics run that never ends, an instance that was
healthy a minute ago and is now gone.
docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' \
"$(chap-devops docker ps -- -q dhis2)"
docker info --format '{{.MemTotal}}' # what the daemon has to give
OOMKilled true, or exit code 137, settles it. Raise the memory the Docker
daemon has - on Docker Desktop that is the VM’s limit, in Settings, Resources -
rather than trimming the heap: a smaller -Xmx makes the JVM fail inside Java
instead of being killed, which is tidier and no more successful. The heap itself
is DHIS2_JAVA_TOOL_OPTIONS in .env, commented with the default
-Xms2g -Xmx4g -XX:+UseG1GC. See Memory.
The Modeling App does not see Chap
Because nothing has connected the two yet, and one command does all of it:
chap-devops dhis2 show # which of the three pieces is missing
chap-devops dhis2 connect # the route, the apps, then analytics
The app does not reach chap-core directly. It goes through a DHIS2 Route with
code: "chap", and until that row points at this deployment the app redirects to
/get-started.
The trap is that it will look configured. The climate demo dumps ship a
chap route of their own - right code, right authority, not disabled - aimed at
an external Chap server, so a seeded instance has a route by that name resolving
to somebody else’s chap-core. chap-devops dhis2 route repoints it rather than skipping
it:
repointed the `chap` route at http://chap:8000/**; chap-core answered through it
With -v, a hint says where it pointed:
hint: the route was rewritten because it pointed at http://158.39.75.126/stable/**.
chap-devops dhis2 show is the one to run first: it names each piece that is missing
and changes nothing. If the app shows Chap but a model is missing from its
model picker, run chap-devops status. If the model is registered, not configured,
run chap-devops models configs sync. If the route is right and the app still shows no figures,
analytics has not been generated - chap-devops dhis2 analytics. If there is no Chap
entry in DHIS2’s apps menu at all, the app is not installed -
chap-devops dhis2 apps. See
Connecting the Modeling App to Chap.
Do not read a silent chap-devops up or chap-devops status as an answer here. Those
two name chap-devops dhis2 connect only until one has been recorded, and the record
is a note that the command ran rather than a check of the route - see
connected_at, and what it is not.
chap-devops dhis2 show is the command that asks DHIS2.
Oops! Sorry, an unexpected error or Unnamed evaluation in the Modeling App
The browser still holds a login to a DHIS2 that is gone. After
chap-devops down --volumes (or chap-devops components disable dhis2 --purge) and a
fresh DHIS2 on the same port, the old session cookie means nothing to the new
instance: every request the Modeling App makes is redirected to the login
page, and the app shows either this error or a dashboard of
Unnamed evaluation rows with 0 locations, sometimes after first showing
the evaluations of the deployment that was removed.
Nothing is wrong with the deployment, and chap-devops jobs shows what Chap really
holds. Open http://localhost:8780/ (or the address chap-devops open dhis2 --no-browser prints), log in again with admin / district, and open the
Modeling App from the app menu.
chap-devops has not connected this DHIS2 to Chap
chap-devops up closes with this while .chap-devops/components.yaml records no chap-devops dhis2 connect for this deployment. chap-devops status closes with it too, but only
while its dhis2 row says up. For an external DHIS2, status has no dhis2
row, so only chap-devops up says it:
chap-devops has not connected this DHIS2 to Chap; run `chap-devops dhis2 connect`
Do what it says, once DHIS2 answers. The command is idempotent, so running it on a deployment that is already connected repoints nothing, reinstalls nothing and says so - which is also why the line is safe to print without asking DHIS2 anything.
It keeps coming back in three cases, all of them deliberate:
- the component was disabled and enabled again. The record is about a DHIS2
instance, so it is forgotten with the component. A re-enabled
dhis2_dbcan be restored from the seed dump, and that dump ships achaproute of its own pointing at an external server. chap-devops down --volumesremoveddhis2_db. Same hazard, said on the spot: the record ofchap-devops dhis2 connectwent withdhis2_db.- the last
connectfound something wrong. A route nothing answered on, or an app that would not install on thedhis2component, clears the record, and with-vthe report sayscleared-chap-devops dhis2 shownames which of the two it was. A run that could not look is not one of these:chap-devops dhis2 connect --offlineskips the apps, judges nothing and leaves the record exactly as it found it.
Connecting the instance by hand, in DHIS2’s own Route administration and App
Management, does not record anything either, and the line then stays. chap-devops dhis2 connect against that instance is the way to settle it: it will find
nothing to change and record that it ran.
The route is there but nothing answers through it
chap-devops dhis2 route wrote the row and then could not prove the path:
warning: the `chap` route is in place but nothing answered through it: HTTP 502 Bad Gateway; run `chap-devops status` to see whether chap-core is up
If no answer came at all, the line says so:
warning: the `chap` route is in place but nothing answered through it: the request through DHIS2 timed out; run `chap-devops status` to see whether chap-core is up
DHIS2 answered the route write a moment before, so the request that got no answer is about chap-core behind the route, not about DHIS2. A new or repointed route gets a second request after two seconds before chap-devops gives this warning.
The route is correct, so this is not a route problem. Three things it is, in the order worth checking:
- chap-core is not running.
chap-devops statussettles it,chap-devops upfixes it, andchap-devops dhis2 routethen verifies on the next run. route.remote_servers_alloweddoes not list the target. DHIS2 42 and later default that setting tohttps://*and refuse anhttp://target. The scaffoldeddhis2/dhis.confalready permits every http and https target, so this is an instance whose file was narrowed or replaced; DHIS2 refuses the write outright in that case and the error names the line. After editing it,chap-devops restart dhis2applies it.- Something else answers on that hostname. A 200 that is not chap-core’s health document is reported as such rather than as success.
The run `chap-devops status` clause is there only when chap-core does not answer on
this machine either. When chap-core answers, the line names the more probable
cause in its place. chap-devops dhis2 show adds the same clause after missing: nothing answered through the `chap` route: ....
chap-core answers at ..., so the DHIS2 allowlist may refuse
warning: the `chap` route is in place but nothing answered through it: HTTP 503 Service Unavailable; chap-core answers at http://localhost:8700, so the DHIS2 allowlist may refuse http://chap:8000; add it to `route.remote_servers_allowed` in `dhis2/dhis.conf` and run `chap-devops restart dhis2`
chap-core is up, and DHIS2 answered 503 for the route. The usual cause is
route.remote_servers_allowed, which does not list the origin of the route.
- Add the origin that the line names to
route.remote_servers_allowedindhis2/dhis.conf. - Run
chap-devops restart dhis2. - Run
chap-devops dhis2 connectagain.
On an external DHIS2, the line names the `dhis.conf` of the DHIS2 server.
That file belongs to the admin of the DHIS2, so ask that admin to add the
origin.
chap-core answers at ..., so DHIS2 may not reach it at
warning: the `chap` route is in place but nothing answered through it: ...; chap-core answers at http://localhost:8700, so DHIS2 may not reach it at http://localhost:8700; give the address DHIS2 reaches chap-core at with `chap-devops dhis2 use --chap-url URL`
chap-core is up on this machine, but the external DHIS2 cannot reach the
chap-url. A localhost address is the DHIS2 server itself, not this machine.
- Run
chap-devops dhis2 use --chap-url URLwith an address that the DHIS2 server reaches this machine at. - Run
chap-devops dhis2 connectagain.
Allowed route URL must not have a path
DHIS2 does not start after route.remote_servers_allowed was edited, and its
log ends with
Caused by: java.lang.IllegalStateException: Allowed route URL must not have a path: http://localhost:8700/**
Each entry on that line is an origin: scheme, host and port, nothing after
them. The route target chap-devops dhis2 route writes ends in /**, and that is
not what goes on the line. Write the origin alone and restart DHIS2:
route.remote_servers_allowed = https://*,http://localhost:8700
chap-devops dhis2 connect then writes the route. When DHIS2 refuses a route, the
error line names the origin to add.
An edit to dhis2/dhis.conf changes nothing after a restart
You corrected a value in dhis2/dhis.conf - route.remote_servers_allowed,
a connection.* line, one of the commented server.https proxy lines - ran
chap-devops restart dhis2, and it said:
nothing needed a restart: every container matches its files
dhis.conf is a bind mount, which compose does not compare, so chap-devops restart looks at the file itself and recreates dhis2 when the file was
written after its container was created (recreating dhis2 to apply its edited config file). This line means the file’s modification time is older than the
container: a copy or a restore that kept the original time. Recreate it
regardless:
chap-devops restart --all dhis2
DHIS2 migrates before it serves a request again, so it is not back when compose
returns; the next chap-devops dhis2 command waits for it. Then re-run whatever
reported the problem:
chap-devops dhis2 route
ocs/climate-service.yaml is handled the same way - see
Read-only instances and
dhis2/dhis.conf. It is not the same as
An OCS dataset plugin does not appear after a restart,
where the compose file itself is out of date: a restart cannot fix that one and
chap-devops up can.
the App Hub publishes no version of ... that DHIS2 ... can run
failed Modeling App was not installed: the App Hub publishes no version of Modeling that DHIS2 2.42.6 can run; install it from DHIS2's own App Management page
Upgrade chap-devops. Up to and including 0.4.0 this sentence was wrong for every
instance there is. The App Hub sends a version bound it has not set as an empty
string rather than as an absent field, and "" was read as a maximum: every
published version looked capped below the instance, so every one was filtered
out. chap-devops now treats a missing, empty, whitespace or unreadable bound as no
bound, on both sides.
If it appears on a current chap-devops, it is what it says: check the app’s own page
on apps.dhis2.org for a version that lists this
instance’s release under minDhisVersion. chap-devops dhis2 apps -v prints the
version it resolved and the id it installed.
POST /api/appHub/{versionId} is idempotent, so installing from DHIS2’s own App
Management page in the meantime costs nothing: chap-devops dhis2 apps afterwards
reports the app as already installed rather than fighting it.
analytics may never have run on this deployment
analytics 2026-06-16 09:51 (unconfirmed on a seeded database)
missing: analytics may never have run on this deployment: a seeded database can carry the dump's timestamp, and no run has finished since DHIS2 started; run `chap-devops dhis2 analytics` to settle it
This is not an error, and it is not chap-devops saying the tables are missing. It is
chap-devops declining to say they are there.
lastAnalyticsTableSuccess is a row of DHIS2’s own settings, so a
seeded deployment restores it with the rest of the dump.
Measured on a 2.42.6 with the Laos climate demo, a freshly seeded instance
reported a last success of 2026-06-16T07:51:00.093 while analytics_2024 did
not exist at all - the table was absent, not empty, and the Modeling App saw
nothing. The timestamp was a fact about the database the dump was taken from.
chap-devops dhis2 analytics
settles it, and the row afterwards reads (a run finished on this deployment).
Restarting DHIS2 empties the notifier chap-devops reads that from, so the line can
come back on a deployment where analytics really has been generated; running it
again is idempotent and takes tens of seconds on demo data. See
What show can say about analytics.
The analytics tables are empty but the run reported success
Almost certainly lastYears. Measured on the climate demo,
POST /api/resourceTables/analytics?lastYears=8 reported success in 16.9 seconds
and wrote zero rows into every analytics table; the same request without it
wrote 146,129 rows into analytics_2024 in 15.7 seconds.
chap-devops dhis2 analytics never sends it, so a run made with
chap-devops dhis2 analytics
populates what the database holds. A reference deployment’s own
trigger-analytics.sh does send &lastYears=8, which is where this comes from.
If the tables are still empty afterwards, the database has no data in the period the apps are asking about: the Climate App is what imports climate data into DHIS2, and analytics has to run again after an import.
chap-devops dhis2 says DHIS2 did not accept the password
error: DHIS2 at http://localhost:8780 did not accept the password for `admin` (the DHIS2 default password); set `DHIS2_ADMIN_USERNAME` and `DHIS2_ADMIN_PASSWORD` in `.env`, or export `CHAP_DEVOPS_DHIS2_PASSWORD`
chap-devops holds no DHIS2 credentials of its own. On a DHIS2 it deployed it falls
back to admin / district, which is what a seeded demo dump and a
Flyway-bootstrapped empty database both give. So this is an instance whose
password has been changed, or one restored from a dump of your own. Either
uncomment and edit the lines in .env:
DHIS2_ADMIN_USERNAME=admin
DHIS2_ADMIN_PASSWORD=the-one-that-works
or keep it out of the file entirely:
export CHAP_DEVOPS_DHIS2_PASSWORD=the-one-that-works
or use a personal access token instead (DHIS2_API_TOKEN in .env, or
CHAP_DEVOPS_DHIS2_TOKEN). There is no --password flag, because a password on a
command line is in the shell history and in ps. A 403 rather than a 401
means the opposite problem: the credential is right, but that user is not
allowed to write a route or run analytics, which a DHIS2 superuser is. See
The credentials.
chap-devops dhis2 says DHIS2 did not accept the API token
error: DHIS2 at https://dhis2.example.org did not accept the API token (API token from `.env`): it is expired, revoked, or not allowed from this address; set a current one as `DHIS2_API_TOKEN` in `.env`, or export `CHAP_DEVOPS_DHIS2_TOKEN`
DHIS2 answers 401 for a token that has expired or been revoked, and for one
whose allowed IP addresses or referrers leave out the machine chap-devops runs on.
Create a new one under Profile > Personal access tokens in DHIS2. The 401
also covers a value that was never a token, such as one pasted with the
ApiToken prefix: DHIS2_API_TOKEN holds the token alone.
chap-devops has no password for DHIS2 user
error: chap-devops has no password for DHIS2 user `alice`: `.env` and `CHAP_DEVOPS_DHIS2_USERNAME` give theirs to other users; export `CHAP_DEVOPS_DHIS2_PASSWORD` for this run
error: chap-devops has no password for DHIS2 user `ops`: `.env` names the user and sets no `DHIS2_ADMIN_PASSWORD`; set it there, or export `CHAP_DEVOPS_DHIS2_PASSWORD`
A password belongs to the user it was set with, and chap-devops will not send one
user’s password in another’s name. --user alice finds only a password that is
alice’s, and the default district is admin’s alone. For a one-off run as
someone else, export theirs for the command:
CHAP_DEVOPS_DHIS2_PASSWORD=theirs chap-devops dhis2 show --user alice
chap-devops has no credentials for this DHIS2, and did not deploy it
error: chap-devops has no credentials for this DHIS2, and did not deploy it, so there is no default to try; set `DHIS2_ADMIN_USERNAME` and `DHIS2_ADMIN_PASSWORD`, or `DHIS2_API_TOKEN`, in `.env`
The DHIS2 was recorded with chap-devops dhis2 use, so admin / district is not
known to be anybody’s password there, and chap-devops does not try it. Set a personal
access token (or a username and password) as in
The credentials, then run
chap-devops dhis2 use again to check that DHIS2 accepts it.
this deployment already uses the external DHIS2
error: this deployment already uses the external DHIS2 at https://dhis2.example.org; run `chap-devops dhis2 use --clear` first to deploy one of its own
A deployment has one DHIS2, either the dhis2 component or an external one, so
chap-devops dhis2 never has to guess which. The opposite direction is refused the
same way (the dhis2 component is on, and it is this deployment's DHIS2). Clear
the one you no longer want, then add the other. See
A DHIS2 that runs elsewhere.
chap-devops dhis2 waited twenty minutes and gave up
error: DHIS2 at http://localhost:8780 did not answer /api/ping within 20 minutes; `chap-devops logs dhis2` is where the migration shows, and `--wait SECONDS` waits longer
Either DHIS2 is still migrating - under emulation a first start is a quarter of an
hour, and a seeded one is that plus the restore - or it has stopped. chap-devops logs dhis2 tells the two apart, and
DHIS2 never becomes healthy is the rest of the
list. --wait 3600 waits an hour.
chap-devops update said a restart is needed
That is the whole design, not a failure. chap-devops update moves the pins and
pulls the images and stops there: it never decides for you when a deployment
goes down. The closing line names the running services that are now behind
what the files say, and
chap-devops restart
recreates exactly those, leaving the rest running. If Chap is not running, the
line names no service, and chap-devops up starts it with the new versions (with
-v, a hint says so). See
Updating.
chap-devops update fails offline
error: `chap-devops update` refreshes the marketplace registry, which needs the network; drop --offline, or run `chap-devops update --list-tags` to see the chap-core tags offline
By design. Inside a deployment, chap-devops update fetches the registry from the
network with no cache and no fallback, --dry-run included, because a plan
made from a stale catalogue is not a plan. Outside a deployment, --dry-run
shows the cached registry instead. Every other command falls back to the cache
and then to the snapshot compiled into the binary.
the stable version VERSION of ID is STATUS, not verified
error: the stable version 0.1.4 of `chapkit_simple_multistep_model` is unstable, not verified; pass `--version 0.1.4` to enable it anyway
The stable channel of the marketplace entry points at a version that the
marketplace has not verified. chap-devops follows chap-admin install and
takes only a verified version on that channel.
- To wait for a verified version, run
chap-devops updateagain later. - To run this version now, enable it with the
--versionin the message. The model is then pinned, andchap-devops updatedoes not move it.
the deployment in DIR still has containers in compose project
error: the deployment in /srv/chap still has containers in compose project chap; stop it with `docker compose -p chap down`, which keeps its volumes, then run this again
chap-devops init --adopt takes over the database of the old deployment. If
both deployments ran, they would use one database at the same time.
- Run the
docker compose -p <project> downcommand in the message. It removes the containers and keeps the volumes. - Run
chap-devops init --adoptagain.
found no volume PROJECT_chap-db, so this would start chap-core with an empty database
error: found no volume chap_chap-db, so this would start chap-core with an empty database; if the deployment in /srv/chap ran under another compose project name, set COMPOSE_PROJECT_NAME in its .env to that name, then run this again
chap-devops init --adopt gives the new deployment the compose project name of
the old one: the directory name, as compose makes it, or COMPOSE_PROJECT_NAME
in its .env. Docker has no database volume under that name.
- Run
docker volume ls --filter name=chap-dbto find the database volume. - The part before
_chap-dbis the project name. SetCOMPOSE_PROJECT_NAMEto it in the.envof the old directory. - Run
chap-devops init --adoptagain.
could not find the template of ID in chap-core, so nothing changed
error: could not find the template of chapkit_simple_multistep_model in chap-core, so nothing changed: chap-core at http://localhost:8000 is not responding: io: Connection refused (os error 61); run `chap-devops status`; start chap-core with `chap-devops up`, or run it without --retire
chap-devops models disable --retire and chap-devops models remove look up the
model template in chap-core before they change anything. chap-core did not
answer, so the model is still as it was.
- If the deployment is down, run
chap-devops up, then run the command again. - If you do not want to change chap-core, run
models disablewithout--retire, ormodels removewith--keep-in-chap. The model then stays live in the Modeling App.
could not retire ID in chap-core
warning: could not retire chapkit_simple_multistep_model in chap-core: ...; run `chap-devops api DELETE '/v1/crud/model-templates/30?allVersions=true'`
The model left the deployment, but chap-core refused the retire or stopped
answering after the lookup. The template is still live in the Modeling App.
Run the chap-devops api command in the warning when chap-core answers again.
left out models/ID.yaml of the marketplace registry
warning: left out models/chapkit_minimalist_example_py.yaml of the marketplace registry: kind: unknown variant `notebook`, expected `model` or `starter` at line 6 column 7; this chap-devops may be older than the registry, so run `chap-devops self update`
chap-devops could not read one model file of the marketplace registry. The usual cause is a field or a value that the marketplace added after this chap-devops was built. chap-devops leaves that model out and uses the others, so the other commands still work.
- Run
chap-devops self update. A newer chap-devops usually reads the file. - If the warning stays after the update, the file is not valid. Report it against the marketplace repository.
`chap-devops registry update` refreshes the marketplace registry, which needs the network; drop --offline
error: `chap-devops registry update` refreshes the marketplace registry, which needs the network; drop --offline
chap-devops registry update only fetches the registry, so --offline leaves it
nothing to do. It is a usage error, and the exit code is 2. Run it again
without --offline.
ignoring the registry cache in DIR: meta.json is corrupt
warning: ignoring the registry cache in /home/me/.cache/chap-devops/registries/...: meta.json is corrupt: ...; run `chap-devops registry update` to write it again
A file of the cached registry cannot be read, so chap-devops does not use the cache
and the command goes on without it. The same warning names registry.yaml
when that file is the broken one. Run chap-devops registry update to write the
cache again.
GitHub's rate limit is used up
skip registry pin chapkit_simple_multistep_model could not ask https://github.com/chap-models/chapkit_simple_multistep_model
for its default branch: GitHub's rate limit is used up (unauthenticated, 60 requests an hour
per address); set GITHUB_TOKEN for 5000, or wait until 15:04
GitHub allows 60 REST requests an hour to an address that sends no
credential, and chap-devops spends them on questions it has to ask somebody:
chap-core’s release list, the default branch and commits of every model
repository behind a registry pin <id> line, and both lookups of a
chap-devops models add. One chap-devops doctor on a deployment with five models is a
dozen of them. On a shared egress address - an office, a VPN, a CI runner -
the hour is gone before chap-devops asks for anything at all.
Nothing is broken and nothing failed: every check that needs GitHub skips with
this reason and the rest of the report is exactly as it was. Either wait until
the clock time on the line, which is when the window resets, or hand chap-devops a
token:
export GITHUB_TOKEN=$(gh auth token) # or GH_TOKEN, whichever is already set
chap-devops doctor
That is 5000 requests an hour instead of 60. A token with no scopes is enough,
because every question chap-devops asks GitHub is a public read. chap-devops doctor’s
github api line says which limit is in force and how much of it is left:
ok github api reachable, 4990 of 5000 requests left this hour (token)
The token is read from the environment only. chap-devops never stores it and never
prints it - a -vv trace shows Authorization: Bearer <token> with nothing
behind it - and no command-line flag sets one, because a token on a command
line is a token in the shell history.
A 403 that arrives with a token reads differently, because the advice to set one would be wrong:
GitHub refused the request (HTTP 403 with a token; check GITHUB_TOKEN's scopes or the rate limit)
That is an expired, revoked or misspelled token, one whose fine-grained
permissions do not include reading public repositories, or a token whose own
5000 are gone. gh auth status says which.
Can't locate revision identified by in chap-devops logs chap
ERROR [chap_core.database.database] Error during Alembic migrations:
Can't locate revision identified by 'b4c5d6e7f8a3'
The database was migrated by a newer chap-core than the one now running: the
Alembic revision it was stamped with does not exist in this build. It is what
chap-devops update --chap-tag warns about before a move backwards, and the reason
it asks for an answer:
warning: moving chap-core from master to v2.3.1 can run an older schema against a
database migrated by the newer one; run `chap-devops backup create` first
chap-core logs the error and starts anyway, so chap-devops status says up while
the schema is not the one this build expects. Either go back to the newer tag
chap-devops update --chap-tag master # or whichever it was
chap-devops restart
or restore the backup taken before the move (chap-devops backup restore) and start
again from there. See
Switching chap-core’s tag.
ghcr.io has no image
error: ghcr.io has no image chap-worker:dev, so the pull would fail; nothing was changed; pick another tag, for example a release from `chap-devops update --list-tags`
chap-core is two images, chap-core and chap-worker, and a tag that exists
for one of them does not have to exist for the other: at the time of writing
ghcr serves chap-core:dev and has no chap-worker:dev at all. chap-devops update --chap-tag asks ghcr for both images before it moves the pin, in the dry run
too, so nothing was changed. Run chap-devops update --list-tags, and pick a tag
from that list. See
Switching chap-core’s tag.
the pull failed after the pins moved
error: the pull failed after the pins moved; chap-core is now pinned to v2.4.0, and
`chap-devops update --chap-tag v2.3.1 --yes` puts it back
The pin moved, and then the pull failed for another reason, such as a network
error or a full disk. The deployment now describes images that Docker could not
fetch; the containers keep running what they had. The lines above the error
give the reason. Fix it and run chap-devops docker pull, or use the command in the
line to put the pin back.
gives ... the compose file ..., which is not a file in this deployment's directory
Every command reads .chap-devops/ first, and this one stopped there:
error: `.chap-devops/models.yaml` gives chapkit_simple_multistep_model the compose file `/tmp/compose.x.yml`, which is not a file in this deployment's directory; chap-devops only writes `compose.<service>.yml` there; correct the entry by hand, or run `chap-devops init --force` to rebuild the state
chap-devops writes every compose file into the deployment directory itself, as
compose.yml or compose.<service>.yml, and writes and removes them by that
name. A name with a /, a .. or a drive in it would have it write or delete
a file somewhere else, so it is refused before anything uses it. The state was
edited by hand or came from an archive changed after it was made. Put the
plain name back (compose.chapkit-simple-multistep-model.yml for that model), or rebuild
the state with chap-devops init --force. The same message names
.chap-devops/project.yaml when the name is in its compose_files or
rendered_files list.
lists the project file ..., which is not inside a deployment directory
chap-devops backup restore refused the archive before restoring anything: its
manifest names a file outside the project directory, such as ../.bashrc.
chap-devops backup create never writes such a path, so the archive was changed
after it was made. Restore from another one.
runs in docker (`docker_env` in its MLproject), and the container has no docker socket
chap-devops chap read the model’s MLproject, and the model starts a container of
its own. In the container that chap-devops chap runs, there is no docker socket
unless you give it. Run the same command again with --docker after chap:
chap-devops chap --docker eval .... The flag gives the container full control of
docker on this machine. See Models that run in docker.
Cannot reach the Docker daemon. The model ... requires Docker to run
chap-core says this when a docker_env model runs without a docker socket. In
chap-devops chap, the cause is not a stopped Docker: the container did not get
the socket. chap-devops did not find docker_env before the run, because it could
not read the model’s MLproject (for example, under --offline). Add
--docker.
needs chap-core v2.5.0 or later, and this run uses
error: built-in models need chap-core v2.5.0 or later, and this run uses v2.4.0; give a newer tag, such as `--tag v2.5.0`
Built-in models (builtin:NAME), --run-config.as-chapkit and
chap simulate are new in chap-core v2.5.0. Outside a deployment,
chap-devops chap uses the newest release, unless this machine has only older
images and cannot look the release up; in a deployment, the tag of the
deployment. chap-devops stops before the run, because chap at an older tag ends in
a traceback. Add --tag v2.5.0. In a deployment, chap-devops update moves it to
the newest release.
Rscript: not found from chap-devops chap
The model needs R, and it ran in the chap-core image, which has no R. chap-devops
selects the worker image for a GitHub repository or a model directory, so
this happens with --image core, or with a chapkit URL whose service calls R
itself. Run it again with --image worker.
FileNotFoundError: [Errno 2] No such file or directory from chap-devops chap
chap writes an output file into a directory that does not exist. chap does
not make directories, with or without chap-devops. Make the directory first
(mkdir -p out), then run the command again. A path outside the current
directory is mounted from its nearest existing parent, so it has the same
rule.
--offline needs a ghcr.io/dhis2-chap/chap-core image on this machine
Outside a deployment, chap-devops chap asks GitHub for the newest chap-core
release. Under --offline it uses the newest image of that kind on this
machine instead, and there is none. Run the command once without --offline,
or give a tag that is on this machine with --tag.
chap exited with status N; its own message is above
chap stopped with an error. The cause is chap’s own output above this line. chap-devops exits with the same status, so a script can read it. If the run wrote files before it stopped, chap-devops names them.
the model server at ... is not running (no answer on /api/v1/info), so chap was not started
chap-devops chap asked the --model-name URL for /api/v1/info before the run,
and nothing answered. chap was not started, so there is no traceback.
- A service name such as
http://my-model:8000that chap-devops does not know: give the model id instead (--model-name chapkit_simple_multistep_model). chap-devops then starts the model. In a deployment, the model must be enabled first. - A server elsewhere: check that it runs, and that this machine can reach it.
`ID` is not a model of the deployment at ...; `chap-devops models enable ID` adds it
In a deployment, chap-devops chap starts only the models that the deployment has.
It does not add a model for an evaluation, because that changes the
deployment. Run chap-devops models enable ID, then the same command again. Or run
chap-devops chap outside the deployment, where it starts the model in a
chap-devops run group.
... did not answer on http://...:8000 within 300s
chap-devops started the model for the run, and the model did not answer in time. A
first start pulls the model’s image, and an R model can take some minutes.
Run the command again with a longer --timeout. The message names the
chap-devops logs command that shows why the model does not start.
`http://localhost:...` is this machine, and in the container `localhost` is the container itself
chap-devops chap got a model URL on localhost (or 127.0.0.1). That is the URL
chap-devops ps shows, and it works from your machine. chap runs in a container,
where localhost is the container, so the URL reaches nothing. When chap-devops
knows which model has that port, the message gives its service URL, for
example http://auto-arima-chapkit:8000. Else it gives the same port at
host.docker.internal, the name of this machine in the container. That is
the URL for a service that chap-devops chapkit mlproject run started outside a
deployment.
stopped by Ctrl-C; ... is taken back out of ...
You pressed Ctrl-C while chap-devops run started the model or waited for it to
answer. chap-devops took back out what the start put in: the model that this run
enabled is out of its group again, as after a failed start, and its data stays.
The command exits with 130. Run the same chap-devops run again to start it. A model
that ran before the command is left running, and the message says so.
Slides
Seven slide decks, one for each audience. Each one is a web page that you can present in a browser, and a PDF in 16:9. All the decks on one page.
| Deck | For | Web | |
|---|---|---|---|
| An overview | anyone who is new to chap-devops | open | |
| Running models with chap-devops run | anyone who needs a model and its URL | open | |
| The chap CLI, without Python | people who use uvx --from chap-core chap | open | |
| Chap behind the Modeling App | people who use DHIS2 and the Modeling App | open | |
| A workshop | students, in a hands-on session of about one hour | open | |
| Developing a model | people who write forecasting models | open | |
| MLproject models with chap-devops | people who have a model in the MLproject format | open |
In the web page, the arrow keys move between the slides, and f shows the
deck on the full screen.
The decks are Markdown files in slides/decks/, with one theme in
slides/theme/chap-devops.css. To change one, edit its file and run make slides.
See Development.
Roadmap
What is planned for chap-devops, and what is not. The order is not a promise.
Planned
Resource limits for each service
Today the compose files set no memory or CPU limits, except the heap of
DHIS2 (DHIS2_JAVA_TOOL_OPTIONS in .env). A large DHIS2 database, such as a
national dump, needs more memory for PostgreSQL and for DHIS2 than the demo
dumps do. The plan:
- Settings in
.chap-devops/components.yamland.chap-devops/models.yamlfor the memory and the CPUs of each service, rendered asmem_limit,cpusand, for PostgreSQL,shared_buffersandwork_mem. - A preset for a large DHIS2 database.
- A
chap-devops doctorcheck that compares the limits with the memory that docker can give.
A longer wait for a large seed
The health check of dhis2-db waits at most about 15 minutes. A large
dump takes longer, so chap-devops up stops with “unhealthy” while the restore
continues. The wait must grow with the size of the dump.
Levels for the output of every command
Most commands print their closing lines with a level (info, warning, hint),
as Status and output describes. chap-devops api and
chap-devops chap still print their closing lines on stderr with no level. They
change to the levels next.
Templates for the compose files
The large compose files are templates in src/compose/templates/, but the
optional parts are built in Rust: the seed one-shot, the port lines, the
mounts. The plan is to use minijinja (Jinja2 for Rust) so that the conditions
and the loops are in the template files, and the Rust code only gives the
values. The golden files in tests/fixtures/ make sure that the output does
not change.
The checks of chap-admin
chap-devops installs, updates and removes models as chap-admin does, but
it checks less. The plan:
- Check that the
image_tagof a version issha-<prefix>of itscommit. - Check that the
idof a marketplace entry matches its file name, and that itsservice_idis the id with-for_. - After a model registers, compare its service id and its
git_revisionwith the pin, and warn when they differ. - Before
chap-devops models addregisters an image, refuse a template name that is already live in chap-core and that this deployment does not own.
A confirmation for an image outside the marketplace
chap-admin install --image needs --accept-risk each time, because nobody
reviewed the image. chap-devops models add asks for no confirmation. Whether it
should, and how (a flag, or a question at a terminal), is still open.
Undo of a failed update, for the container too
When a configure step fails, chap-devops retires the template version it
stored, so chap-core is as it was. chap-admin also stops the new container
and starts the previous image again. chap-devops leaves the new container
running.
The development overlays in a takeover
chap-devops init --adopt reads compose.marketplace.yml only. A deployment that
also ran chap-core’s compose.ewars.yml or compose.chapkit.yml loses those
services in the takeover. They are for development, but the takeover must
at least name them.
chap-devops for chap-admin users
A page that maps each chap-admin command and flag to the chap-devops
command that does the same, and a scenario in scripts/e2e.sh that runs
each of them against real images: install, update, uninstall, a custom image
and a takeover.
Chap Hub
The marketplace now calls itself Chap Hub in its registry.yaml. The help,
the messages and the docs change to that name when the Chap team confirms it.
A stable CLI test
The CLI test
models_test_runs_chapkit_test_in_the_container_and_cleans_up_after_it
failed once in a full run and passed again alone, so it is not stable.
Not planned
Native Windows
chap-devops runs on Windows in WSL 2, with the Linux binary. The native Windows builds are published, but they are not supported, and there is no PowerShell installer. These parts are ready:
- the cache and data directories under
%LOCALAPPDATA%\chap-devops; chap-devops open, Ctrl-C andchap-devops self update.
Native support would also need a Windows job in CI, a test double for docker
that is not a shell script, a path map for the mounts of chap-devops chap, and a
signed binary.
Development
Make targets
make test # run the test suite
make check # formatting and clippy, fixing nothing
make lint # fix what check reports: clippy --fix, then format (writes files)
make build # fast host-arch compile check
make release # universal (arm64+x86_64) macOS binary at bin/chap-devops
make install # copy bin/chap-devops to $PREFIX/bin, with the link cdx (PREFIX defaults to ~/.local)
make run # build the release binary, then run it (ARGS="...")
make vendor # refresh the embedded marketplace snapshot
make docs # regenerate the reference, then build the book into site/
make docs-serve # serve the book locally with live reload
make docs-reference # regenerate docs/reference.md from the CLI help texts
make release-tag VERSION=x.y.z # bump, check, commit and tag a release
make clean # remove target/ and bin/
make help lists every target. The underlying commands are cargo test,
cargo clippy --all-targets -- -D warnings, cargo fmt --all --check and
scripts/vendor-marketplace.sh.
Tests
cargo test
Unit tests live next to the code they cover, always in <module>/tests.rs
(src/status.rs and src/status/tests.rs), declared at the bottom of the
module as #[cfg(test)] mod tests;; there are no inline mod tests { }
blocks. A code file stays under about 600 lines: past that it becomes a folder
module split by concern, foo.rs with the module doc, the mod lines and the
re-exports, and foo/<concern>.rs beside it. Modules are foo.rs + foo/,
never foo/mod.rs. tests/cli/ is the end-to-end suite: it runs the built binary
against temporary directories, and every run is --offline with its own cache
directory. That cache starts with the frozen marketplace data of
tests/fixtures/marketplace/ (tests/support/marketplace.rs), so no test
touches the network or the developer’s real cache. It is one test binary
with a module per concern (init.rs, models.rs, dhis2.rs, …);
common/ holds the sandbox every test runs in and the stand-in servers for
GitHub, ghcr, the marketplace and chap-core, and dhis2_stand_in.rs the one
for DHIS2. A new test goes in the module for its command, and a helper moves to
common/ once a second module needs it. Keep a module under about 1500 lines;
past that, split it by concern.
chap-devops models add and chap-devops update --chap-tag are the commands that cannot
be tested that way: they resolve a repository, a release or a compose file over
HTTP. Those tests stand a local server in for everything they would reach -
GitHub’s REST API, the raw file host, ghcr and the marketplace index - through
four hooks nothing on the command line can set:
| Variable | What it moves |
|---|---|
CHAP_DEVOPS_GITHUB_API | The GitHub REST base URL, normally https://api.github.com: the repository lookups, chap-core’s release feed, chap-devops’s own, and /rate_limit. |
CHAP_DEVOPS_GITHUB_RAW | The raw file base URL, normally https://raw.githubusercontent.com: chap-core’s compose.ghcr.yml at a ref. |
CHAP_DEVOPS_GHCR_URL | The registry base URL, normally https://ghcr.io. |
CHAP_DEVOPS_OCI_URL | The base URL of every other image registry, normally the host in the image name. |
CHAP_DEVOPS_NO_DOCKER_PROBE | 1 turns off the uid probe, which would otherwise docker pull an image the test has not got. |
The registry itself moves with the ordinary --registry-url flag. The hooks
exist for the tests: an operator who wants another registry wants another image
reference, and one who wants another catalogue has the flag.
GITHUB_TOKEN and GH_TOKEN are not hooks but the real thing: the first of
the two with a value in it is sent as Authorization: Bearer on every
api.github.com request, which is 5000 requests an hour instead of 60. Both
are cleared for every end-to-end run, so a token in the developer’s shell
cannot change what a test sees, and set deliberately in the one test where the
header itself is what is under test. Nothing on the command line sets a token
and nothing writes one down. See Doctor.
The tests that need Docker check for it first and skip with a note on stderr when it is not there, so the suite is green on a machine without a daemon.
One test is #[ignore]d as well: live_up_then_purge_removes_the_model_volume
starts a real deployment, which pulls every image. Run it by hand on a
machine with Docker when the volume handling changes:
cargo test --test cli -- --ignored live_up_then_purge
The end-to-end run
scripts/e2e.sh (or make e2e) runs everything for real against published
images, once per chap-core tag (latest and master by default) and DHIS2
version (2.41, 2.42, 2.43 and dev by default; dev is the unreleased next
one, dhis2/core-dev:master). Each pair gets one deployment with OCS, the
object store, DHIS2 and chapkit_simple_multistep_model running together, on
ports of its own. --models all runs every marketplace model instead, which
takes much longer and needs more disk.
DHIS2 2.42 is seeded from the Laos climate demo chap-devops uses by default, and the
other versions start empty: DHIS2’s Sierra Leone demo exists for some of them,
but its admin may not create the route dhis2 connect writes. Then:
chap-devops up, and a wait untilchap-devops statusexits 0: every service up and every model registered;chap-devops doctor;- OCS answering
/healthand listing/stac/collections, then ingesting three days of CHIRPS3 (public, no credentials) and publishing it; chap-devops dhis2 connect: the route, the apps, analytics;chap-devops models test --all: every model trains and predicts;chap-devops models test --all --backtest: a dataset, a backtest and its scores through chap-core, the path the Modeling App takes;chap-devops chap evalof the first model over the deployment’s network, on chap-core’s Laos example data, thenchap-devops chap plot-backtest. Both files must be on the host.
scripts/e2e.sh # latest, then master
scripts/e2e.sh --models all # every marketplace model
scripts/e2e.sh --dhis2 dev # only the unreleased DHIS2
scripts/e2e.sh --dhis2 2.42 # one DHIS2 version
scripts/e2e.sh --tags v2.3.1 --keep # one tag, left running afterwards
scripts/e2e.sh --no-dhis2 --work ./e2e # no DHIS2, results under ./e2e
make e2e TAGS="latest"
A failed step does not stop the run: its log is kept, the container logs are
collected, and the steps that do not depend on it still run. report.md in the
work directory has one row per tag and step with its result and duration, and
the script exits non-zero when any step failed. It needs Docker with room for
DHIS2 (about 8 GB) and the network, and it takes a while.
The e2e workflow (.github/workflows/e2e.yml) runs the script every Monday
at 03:00 UTC, with one job for each chap-core tag (latest and master) and
DHIS2 2.42. To run it at a different time, start it from the Actions tab. You
can then set the tags, the DHIS2 versions (none leaves DHIS2 out) and the
models. Each job puts report.md in the run summary and keeps the report and
the logs as an artifact for 14 days.
Slides
The slide decks are Marp Markdown: one file for each deck
in slides/decks/, and one theme, slides/theme/chap-devops.css. A --- line
starts a new slide.
make slides # every deck, as HTML and as a 16:9 PDF, into site/slides/
make slides also writes an image of each deck’s title slide, and copies
slides/index.html to site/slides/index.html. That page lists the decks, so
/slides/ on Pages shows them and not a 404. A new deck needs a card in
slides/index.html and a row in docs/slides.md.
make slides runs a pinned Marp CLI through bunx, so it needs bun. The PDF
also needs Chrome or Chromium on the machine. The docs workflow builds the
slides after the book, so Pages serves them at /slides/, and
docs/slides.md links each one.
Run make docs first and make slides after it: mdbook build empties
site/, and that removes the slides.
Each command and each output on a slide must be one that chap-devops really runs or prints. Copy it from a real run, or from the chapter that it comes from.
The build script
build.rs produces four things the compiler cannot work out on its own.
$OUT_DIR/embedded_files.rs, the(relative path, contents)table thatsrc/registry/embedded.rsincludes. It is generated fromvendor/marketplace/registry.yamland the model files that index lists, in index order, so there is no hand-written list to keep in step. A file the index names but the directory does not hold fails the build with a message naming it; a file on disk the index does not list is acargo:warningand is left out.TARGET, the triple this binary is built for, whichchap-devops self updatereads to pick its release asset andchap-devops self versionprints.GIT_REVISION, the short commit of the checkout, empty when there is no git or no repository, whichchap-devops self versionprints when it is there.CHAP_DEVOPS_BUILD_CHANNEL,stableordev, read from the environment variable of the same name. Only the exact valuedevselects the dev channel, so nothing an environment happens to hold can end up in the compiled string, and a local build is stable without anyone having to say so..github/workflows/release.ymlsets it for a branch build. See The dev channel.
The script names its own rerun-if-changed files, so it re-runs when the
vendor directory or .git/HEAD moves and not otherwise, plus a
rerun-if-env-changed for CHAP_DEVOPS_BUILD_CHANNEL, so flipping the channel
rebuilds rather than handing back a cached binary from the other one.
Vendoring the marketplace snapshot
The catalogue compiled into the binary is the last fallback when the registry
can be neither fetched nor read from cache, so chap-devops works on a machine that
has never had network access.
make vendor
runs scripts/vendor-marketplace.sh, which refreshes the files under
vendor/marketplace/ and records the upstream commit in SNAPSHOT. It
records no fetch date, so a run against an unchanged upstream leaves the
directory as it was. A model added or dropped upstream needs nothing else:
build.rs reads the index and embeds what it lists, and cargo test registry
is what checks the result. Refresh when the marketplace changes and commit
the result; a snapshot change is compiled in, so it is a code change.
The vendored layout is the same as the on-disk cache layout, so
vendor/marketplace/ and a cache entry are interchangeable.
The tests do not use this snapshot. The unit tests embed
tests/fixtures/marketplace/, a frozen copy from before the marketplace
removed CHAP-EWARS, and the CLI tests start with it in their registry cache.
So a refresh changes no test result. Change the frozen copy only on purpose,
with the reason in the commit.
The documentation
The book is mdbook. book.toml at the repository root points src at docs/
and build-dir at site/, which is ignored by git.
make docs-serve # http://localhost:3000, rebuilds as you write
make docs # regenerate the reference, then build site/
docs/SUMMARY.md is the table of contents; a chapter that is not listed there
is not built. Every other file in docs/ is a hand-written chapter, except
docs/reference.md.
How the command reference is regenerated
docs/reference.md is generated from the --help texts, so a flag cannot
change without the chapter changing with it.
make docs-reference # cargo run -q -- docs-markdown > docs/reference.md
chap-devops docs-markdown is a hidden subcommand (hide = true) implemented in
src/commands/docs.rs. It walks the static Cli::command() tree through
clap’s introspection API and prints one ## section per command path, with the
about text, a fenced usage line from clap’s render_usage, and a table of
arguments and options.
Two details matter. It renders from the static tree rather than the one
main.rs parsed, because main.rs hides the project-only commands when there
is no project and the reference has to list all of them. And it skips clap’s
own -h and -V by their action rather than their name, because
chap-devops models enable --version is a flag of ours that must stay.
The generated file is committed, and cargo test compares
chap-devops docs-markdown to it byte for byte. When that test fails, run
make docs-reference and commit the result. make docs depends on
docs-reference, so a local book build can never be stale.
To document a new flag: add it to its argument struct in src/cli/ (or to
src/cli.rs for a global option) with a doc comment, run
make docs-reference, and mention it in whichever hand-written chapter it
belongs to.
Continuous integration
.github/workflows/ci.yml runs the same three checks as make check plus
make test on Linux and macOS, on every push to main and every pull
request, and a shellcheck job over install.sh (as POSIX sh) and
scripts/*.sh (as bash). Windows is not in the matrix: the release still
builds the two Windows targets, but nothing tests them, and Windows users are
pointed at WSL with the Linux binary.
.github/workflows/release.yml builds release binaries for six targets
(Linux, macOS and Windows, on x86_64 and aarch64) when a vX.Y.Z tag is
pushed, fuses the two macOS builds into a seventh universal archive, and
attaches them all to a GitHub release with their SHA-256 sums. Every target is
built natively, so there is no cross-compilation toolchain to maintain, the
freshly built binary generates its own completion scripts into the archive,
and the Linux builds are static musl so one binary runs on any distribution. A
push to main runs the same seven builds and publishes them as the rolling
dev pre-release; a workflow_dispatch run exercises the build matrix
without publishing anything.
.github/workflows/docs.yml publishes the book to GitHub Pages on every push
to main. It regenerates docs/reference.md and fails if the result differs
from the committed file, which is the same check cargo test makes, then runs
mdbook build and deploys site/.
Releasing
Prerequisites
The macOS archives are signed with an Apple Developer ID certificate and
notarized, which needs six repository secrets. scripts/github-secrets.sh
sets them, reading each value from 1Password and piping it into gh secret set
on stdin, so no secret is written to disk or shown in a process list. All six
are read before any of them is uploaded, and a read that fails or comes back
empty aborts the run before it has set anything: a half-replaced set of signing
secrets fails the next tagged build with a mix of old and new values.
OP_VAULT="Private" OP_ITEM="Apple Developer ID" scripts/github-secrets.sh
The six are APPLE_CERTIFICATE (base64 of the exported .p12),
APPLE_CERTIFICATE_PASSWORD, APPLE_SIGNING_IDENTITY, APPLE_ID,
APPLE_PASSWORD (an app-specific password) and APPLE_TEAM_ID. They are the
same six the winterop-com/maneki desktop build uses. --scope org sets them
on the organisation with --visibility selected instead of on this repository
alone; --dry-run shows what would be set.
Without the secrets a branch push or a dispatch run still succeeds with
unsigned macOS binaries, so a fork can exercise the build. A tag run fails
instead: a release is never published unsigned under notes that say it is
signed. The publish job also refuses a tag that does not match the version in
Cargo.toml, and publishes a tag with a suffix (v1.0.0-rc.1) as a
pre-release that never becomes releases/latest.
Preparing the Apple certificate
APPLE_CERTIFICATE is the base64 of a .p12 archive holding the Developer ID
private key, the certificate downloaded from the developer portal and Apple’s
intermediate, so the chain in the archive is complete:
# the Developer ID certificate from the portal, DER, next to its private key
openssl x509 -inform DER -in developer_id.cer -out developer_id.pem
# Apple's Developer ID G2 intermediate, which the portal does not include
curl -fsSL -o DeveloperIDG2CA.cer https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer
openssl x509 -inform DER -in DeveloperIDG2CA.cer -out DeveloperIDG2CA.pem
openssl pkcs12 -export -legacy -inkey developer_id.key -in developer_id.pem -certfile DeveloperIDG2CA.pem -name "Developer ID Application: <name> (<TEAMID>)" -out developer_id.p12
-legacy is not optional. OpenSSL 3 exports with AES-256-CBC, PBKDF2 and an
SHA-256 MAC by default, and the security tool on the GitHub macOS runner
cannot read that: the import fails with
security: SecKeychainItemImport: MAC verification failed during PKCS12 import (wrong password?)
with a password that is perfectly correct. -legacy writes the older encoding
security accepts. Exporting from Keychain Access instead of OpenSSL is also
fine; it already writes the compatible encoding.
Verify the archive locally before uploading it, in a throwaway keychain rather than the login one:
security create-keychain -p "" /tmp/verify.keychain
security import developer_id.p12 -k /tmp/verify.keychain -P '<password>' -T /usr/bin/codesign
security find-identity -v -p codesigning /tmp/verify.keychain
security delete-keychain /tmp/verify.keychain
find-identity has to print exactly one valid identity; that line is what
APPLE_SIGNING_IDENTITY has to match. If it prints none, the runner will fail
in the same way, whatever the password is.
The base64 of that file is APPLE_CERTIFICATE and the password chosen on
export is APPLE_CERTIFICATE_PASSWORD:
base64 -i developer_id.p12 | pbcopy
Cutting a release
make release-tag VERSION=0.2.0
which runs scripts/release.sh. It refuses to do anything unless the tree is
clean and the branch is main, bumps version in Cargo.toml and
Cargo.lock, runs make check test, commits chore: release v0.2.0, creates
an annotated v0.2.0 tag, and then prints the push command rather than
pushing. Nothing reaches GitHub until you run it:
git push --atomic origin main v0.2.0
--atomic lands the branch and the tag together or not at all, so a rejected
branch never leaves a tag building a release. With --push the script also
fetches origin/main first and stops if it holds commits the local branch
does not.
make release-tag VERSION=0.2.0 PUSH=1, or scripts/release.sh 0.2.0 --push,
pushes straight away.
What the workflow produces
The tag starts release.yml, which attaches eight files to the release:
chap-devops-universal-apple-darwin.tar.gz, both macOS slices in one binary, signed and notarized;chap-devops-aarch64-apple-darwin.tar.gzandchap-devops-x86_64-apple-darwin.tar.gz, likewise signed and notarized;chap-devops-x86_64-unknown-linux-musl.tar.gzandchap-devops-aarch64-unknown-linux-musl.tar.gz, static;chap-devops-x86_64-pc-windows-msvc.zipandchap-devops-aarch64-pc-windows-msvc.zip, unsigned and untested;SHA256SUMS, covering all seven archives.
Each archive holds one directory, chap-devops-<version>-<target>/, with the binary,
README.md, LICENSE and completions/ holding chap-devops.bash, _chap-devops,
chap-devops.fish and _chap-devops.ps1, generated by running chap-devops completions on the
binary that was just built.
No archive name carries the version: the tag is already in the download URL,
one name per platform is what releases/latest/download/<name> needs to keep
the tables in the README and in Install from going stale, and
SHA256SUMS makes a per-archive checksum file redundant. The directory inside
the archive does keep the version, because that is what someone sees after
unpacking it.
The release notes
scripts/release-notes.sh writes the body of the release, and the workflow
does nothing but redirect it into notes.md. It takes the tag, and optionally
the tag to compare against when the default is wrong:
scripts/release-notes.sh v0.2.3 # what that release said
scripts/release-notes.sh v0.2.4 # what the next one will say
scripts/release-notes.sh dev # the rolling pre-release
Running it on a checkout is the way to read the notes before the tag is pushed, and it is why the text is a script rather than a heredoc in the workflow.
A tagged release leads with ## What's changed: the commit subjects between
the previous version tag and this one, grouped by their Conventional Commit
type, with a scope kept as a bold prefix and the chore: release commit left
out. The groups print in this order, and one with nothing in it is not printed
at all:
| Heading | Types |
|---|---|
| Features | feat |
| Fixes | fix |
| Performance | perf |
| Reverts | revert |
| Documentation | docs |
| Internal changes | refactor, style, test |
| CI and tooling | ci, build, chore |
What changed for a user comes first. A refactor is an internal change rather
than tooling: it is usually the groundwork a later feature sits on, and filed
under CI and tooling a reader scanning the notes takes it for build noise. A
type no group claims, and a subject that is not a Conventional Commit at all,
lands under Other changes rather than being dropped. Then a compare link, the
install one-liner and the checksum and signature checks. The dev shape keeps
the unstable header, lists the same grouped commits under
## Since <last tag>, and installs with --version dev.
The previous tag is git describe --tags --abbrev=0 --match 'v[0-9]*'. The
match is not optional: dev is a tag too, it sits on a main commit, and
without the glob it is what describe finds.
GitHub’s own generate_release_notes is off. It lists merged pull requests,
and everything here is pushed straight to main, so all it ever added was a
“Full changelog” link. Both publish jobs check out with fetch-depth: 0, since
the notes need the history and the tags.
The dev channel
Every push to main publishes the same seven archives as a GitHub
pre-release under the single moving tag dev, named dev (rolling build of main). It is the same build the tag path produces, signed and notarized the
same way; what differs is which release it lands on and what the binary says
about itself.
CHAP_DEVOPS_BUILD_CHANNELis set at the workflow level tostablefor arefs/tags/v*run anddevfor everything else, so a dispatch build is never mistaken for a release build either.build.rscompiles it in andchap-devops self versionprints it aschannel.- The version does not change: a dev build reports the Cargo version, which
is the last tag. The commit is what moves, so
chap-devops self updateon a dev build compares the commit the release notes record against the build’s ownGIT_REVISIONrather than comparing versions, and the once-a-day notice does the same. The line the commit is read back from is written byscripts/release-notes.sh;commitand the sha have to stay together in it, and a unit test insrc/selfupdate/tests.rschecks that they do. - A stable build never drifts onto a dev one: it follows
releases/latest, which GitHub documents as “the most recent non-prerelease, non-draft release”, and the notice checksprereleaseitself on top of that. Crossing over ischap-devops self update --version dev, andinstall.sh --version devinstalls one from scratch. - The tag is moved by the workflow itself, with
git tag -f devand a force push, before the release is written. GitHub’s create-a-release and update-a-release APIs both documenttarget_commitishas unused once the tag exists, so leaving it to the release action would place the tag on the first rolling build and never move it again. softprops/action-gh-releaseoverwrites an asset of the same name (overwrite_filesdefaults to true) and reuses the release the tag already has, so the eight files are replaced in place. It does not delete assets it was not handed, so a final step removes anything attached to the release that this build did not produce - a target that was renamed or dropped - after the upload rather than before it, which keeps the release from briefly missing an archive.concurrency: release-<ref>keeps one run going and one waiting per ref, and a newer push replaces the waiting one, so a burst of pushes tomainbuilds the first and the newest commit. A run is never cancelled once it has started: one cut off whilepublish-devuploads would leave thedevrelease with archives andSHA256SUMSfrom different commits. Every third-party action is pinned to a commit SHA, with its version in a comment.
The two publish jobs are guarded against each other: publish runs only for
refs/tags/v* and publish-dev only for a push to refs/heads/main.
The install script
install.sh at the repository root is what
curl -fsSL .../install.sh | sh runs. POSIX sh rather than bash, so it works
on a minimal container image, and shellcheck-clean under -s sh in CI.
It resolves the platform from uname -s and uname -m, resolves the version
from CHAP_DEVOPS_VERSION, then the GitHub API, then the redirect of the
releases/latest page, downloads the archive and SHA256SUMS into a mktemp
directory, verifies with whichever of sha256sum and shasum exists, and
installs with install -m 0755. Completion scripts are copied into the
standard user directories that already exist, and a failure there never fails
the install.
--version dev needs nothing special: the tag goes into the download URL like
any other, and the binary inside the archive is found by matching
chap-devops-*-<target>/chap-devops rather than by spelling the directory out, because a
rolling archive carries dev-<short commit> where a release carries the tag.
--here, which CHAP_DEVOPS_INSTALL_DIR=. and --dir . also select, stops after
install -m 0755 ./chap-devops: it prints the path and the version and returns
without touching completion directories or saying anything about PATH. It is
the shape a CI job or a one-off trial wants, and the one mode that writes
nothing outside the current directory.
CHAP_DEVOPS_DOWNLOAD_BASE overrides where the archive and SHA256SUMS come from.
It is there to test the script against a locally built release and an install
never needs it. curl reads file://, so the whole path can be exercised
without a release:
target="$(uname -m | sed 's/arm64/universal/')-apple-darwin"
name="chap-devops-v0.0.0-test-$target"
mkdir -p /tmp/rel/"$name"
cp target/debug/chap-devops README.md LICENSE /tmp/rel/"$name"/
tar -czf /tmp/rel/chap-devops-"$target".tar.gz -C /tmp/rel "$name"
(cd /tmp/rel && shasum -a 256 chap-devops-"$target".tar.gz > SHA256SUMS)
CHAP_DEVOPS_DOWNLOAD_BASE=file:///tmp/rel \
sh install.sh --version v0.0.0-test --dir /tmp/bin
--dry-run prints the platform, the archive URL and the target path and stops
there, which is the quickest check that the detection is right on a machine.
The signing steps live in .github/actions/sign-macos, a composite action
used by both the per-architecture build and the universal one, so the two
cannot drift apart. It imports the certificate into a throwaway keychain,
signs with --options runtime --timestamp, submits to notarytool --wait and
fails the job on any status other than Accepted, printing notarytool log
first. The universal binary is signed again after lipo, because fusing the
two binaries invalidates the per-slice signatures. Nothing is stapled: a ticket
can only be stapled to a bundle, a disk image or an installer package, so
Gatekeeper fetches it from Apple instead. The keychain is deleted in an
if: always() step in the calling job.
Verifying a download
shasum -a 256 -c SHA256SUMS --ignore-missing
codesign -dv --verbose=4 chap-devops
The second prints Authority=Developer ID Application: ... and a secure
timestamp for a macOS binary. spctl --assess --type execute is not a useful
check here: it assesses app bundles and reports a plain command-line
executable as rejected even when the signature and the notarization are both
good. The same codesign output is written to the workflow’s job summary, so
a release can be audited without downloading anything.
The docs deploy
Pushing the release commit to main also runs docs.yml, which rebuilds the
book and deploys it to https://winterop-com.github.io/chap-devops/. There is
nothing to do by hand; the version in the install commands is written out in
docs/install.md and is updated there when it matters.
Repository setup
scripts/bootstrap-repo.sh is the one-time setup for a fresh GitHub
repository. It prints every command and executes nothing until you pass
--yes:
scripts/bootstrap-repo.sh # print the commands
scripts/bootstrap-repo.sh --yes # run them
It creates winterop-com/chap-devops as a public repository from this checkout,
pushes main, switches GitHub Pages to the Actions build source (which has to
happen before docs.yml can deploy), sets the About link to the documentation
site, and runs scripts/github-secrets.sh.