> For the complete documentation index, see [llms.txt](https://docs.digit.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.digit.org/complaints-management/deploy/setup/single-machine-setup/ansible.md).

# Ansible

### Option C: Ansible — the whole stack, one command

This is the path to a **real deployment**: every service, a web server in front of them, a secret store, monitoring, and a browser wizard for creating your city and loading its data. One command does all of it.

It works two ways, from the same files:

* **on this machine** — nothing to SSH into, good for a demo, a pilot, or learning how the system fits together;
* **on a server** — a fresh Ubuntu box you have `root` SSH access to.

The instructions below are written for the first case. Where the second differs, it says so.

> **New to this?** Read the six steps in order and do not skip ahead. Each one takes a few minutes except the deploy itself, which takes 30–60 minutes the first time. Every step ends with something you can check, so you find out immediately if it did not work.

<details>

<summary>Under the hood — what "one command" actually runs</summary>

`./deploy.sh <name>` is a thin wrapper. In order it:

1. refuses to start if `ansible-playbook` is not on `PATH`;
2. runs `ansible-lint` and `yamllint` over the playbook and your config (skipped with a warning if they are not installed, or with `SKIP_LINT=1`);
3. regenerates `inventory/hosts.yml` from every `host_vars/*.yml` on disk — which is why there is no inventory file for you to edit;
4. runs `scripts/preflight.py` against your config, a set of rules each of which encodes a real incident (`SKIP_PREFLIGHT=1` bypasses it);
5. hands everything else to `ansible-playbook playbook-deploy.yml`, forwarding any extra flags you passed.

The playbook itself installs Docker, creates `/opt/digit/`, syncs the compose files and configs, initialises and unseals OpenBao and seeds your secrets, pulls or builds images, starts the stack, waits on health gates, creates your tenants, and configures nginx. Re-running it is safe: only changed config causes a restart.

The authoritative reference for all of it is [`ansible/README.md`](https://github.com/egovernments/Citizen-Complaint-Resolution-System/blob/master/local-setup/ansible/README.md).

</details>

#### What you need

**The machine you deploy to:**

<table><thead><tr><th width="151.33984375"></th><th width="336.21875">What the deploy expects</th><th>Comfortable</th></tr></thead><tbody><tr><td>CPU</td><td>8 vCPU</td><td>8+</td></tr><tr><td>RAM</td><td>16 GB</td><td>32 GB</td></tr><tr><td>Free disk</td><td>60 GB</td><td>100 GB</td></tr><tr><td>OS</td><td>Ubuntu 22.04 or 24.04</td><td>same</td></tr></tbody></table>

Fewer cores work — it is just slower, since most of a first deploy is downloading and building. 16 GB is the figure to take seriously: below it the JVM services start competing, and containers get killed.

Fedora, Rocky, Alma and Debian also work. Ubuntu is what gets tested on every change, so it is the one to pick if you have a choice.

**If you are deploying to a separate server**, you also need `root` SSH access to it using a key (not a password), and the machine you run the command from needs the tools in Step 2.

{% hint style="info" %}
**On Windows?** Use WSL2 and follow [WINDOWS-QUICKSTART.md](https://github.com/egovernments/Citizen-Complaint-Resolution-System/blob/master/WINDOWS-QUICKSTART.md) — validated end to end on a 16 GB machine. On a Mac, [MAC-QUICKSTART.md](https://github.com/egovernments/Citizen-Complaint-Resolution-System/blob/master/MAC-QUICKSTART.md).
{% endhint %}

{% stepper %}
{% step %}

### Get the code

**For a real deployment, use a release.** `master` is where development lands; a release tag is a set of versions that were tested together.

```
# Newest release — check https://github.com/egovernments/Citizen-Complaint-Resolution-System/releases
git clone --branch v2.12-beta --depth 1 \
  https://github.com/egovernments/Citizen-Complaint-Resolution-System.git
cd Citizen-Complaint-Resolution-System
```

`--depth 1` skips the history and downloads a lot less.

To find the newest tag without opening a browser:

```
git ls-remote --tags --refs --sort=-version:refname \
  https://github.com/egovernments/Citizen-Complaint-Resolution-System.git | head -5
```

If you are working *on* DIGIT rather than deploying it, clone the default branch instead — just be aware you are getting whatever landed this morning:

```
git clone https://github.com/egovernments/Citizen-Complaint-Resolution-System.git
```

**Check:** `ls local-setup/ansible/deploy.sh` prints the path.
{% endstep %}

{% step %}

### Install the prerequisites

You need Ansible, Python, Node.js 20 and a few command-line tools on the machine you run the deploy **from**. There is a script for it:

```
cd local-setup/scripts
./install-prereqs.sh
```

It works on Debian/Ubuntu, RHEL/Fedora/Rocky/Alma/CentOS, Arch and openSUSE, asks for `sudo` only if something is actually missing, and is safe to run again. Add `--check` to see what it would do without changing anything.

Docker is **not** in the list — the playbook installs Docker on the target itself, including when that is this same machine.

<details>

<summary>Under the hood — what it installs and why</summary>

| What                                                    | Why                                                                                                                         |
| ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `git`, `curl`, `rsync`, `unzip`                         | `rsync` is not optional: the playbook uses `ansible.posix.synchronize` to copy configs, which shells out to it on both ends |
| `python3` + `venv` + `pip`                              | Ansible is Python, and `scripts/preflight.py` runs on every deploy                                                          |
| Node.js 20 + npm                                        | the browser apps are **built on the controller**, not on the target                                                         |
| `ansible`, `ansible-lint`, `yamllint`                   | `deploy.sh` runs the two linters before touching anything                                                                   |
| the `ansible.posix` and `community.general` collections | `synchronize` and `ini_file` live there                                                                                     |

Two details that bite people:

* Ansible goes into a private virtualenv at `~/.local/share/digit-ansible`, symlinked into `~/.local/bin`. Ubuntu 24.04, Debian 12+, Fedora 38+ and Homebrew Python all refuse a plain `pip install` with `error: externally-managed-environment` (PEP 668). A virtualenv sidesteps that identically everywhere. If `~/.local/bin` is not on your `PATH`, the script says so and tells you what to add.
* Node comes from NodeSource on Debian and RHEL family, not from the distro. Ubuntu's own `nodejs` package is version 18 **and ships no npm**, which surfaces much later as a confusing `Cannot find module 'esbuild'`.

On a distro the script does not recognise it prints the exact package list and stops rather than guessing.

</details>

**Check:**

```
ansible-playbook --version && node --version && python3 --version
```

If `ansible-playbook` is missing right after a successful run, your shell has not picked up `~/.local/bin` yet — open a new terminal.
{% endstep %}

{% step %}

### Write your deployment config

Everything specific to your deployment lives in one YAML file. Copy the quickstart template and name it after your deployment:

```
cd ../ansible                 # local-setup/ansible
cp inventory/host_vars/quickstart.yml.example inventory/host_vars/mycity.yml
```

**The file name is the deployment name.** `mycity.yml` means you will run `./deploy.sh mycity`. Real config files are gitignored — only the `.example` ones are tracked — so your passwords stay out of git.

Now open `inventory/host_vars/mycity.yml` and change these. Everything else in the file is already correct for a first deployment.

<table><thead><tr><th width="183.9453125">Setting</th><th width="359.65625">What it is</th><th>Example</th></tr></thead><tbody><tr><td><code>state_root</code></td><td>Your top-level tenant: the country or state. Lowercase, no dots. Creating it is what the deploy does.</td><td><code>kenya</code></td></tr><tr><td><code>state_tenant_id</code></td><td>The tenant the browser apps authenticate against. Keep it the same as <code>state_root</code>.</td><td><code>kenya</code></td></tr><tr><td><code>tenant_id</code></td><td>Your <strong>city</strong> tenant, where complaints actually live. Must start with <code>&#x3C;state_root>.</code></td><td><code>kenya.nairobi</code></td></tr><tr><td><code>boot_tenant</code></td><td>Default tenant for the citizen app. Same as <code>tenant_id</code> is right.</td><td><code>kenya.nairobi</code></td></tr><tr><td><code>ui_state_tenant_id</code></td><td>The tenant the app lands on after login. Point at the <strong>city</strong>.</td><td><code>kenya.nairobi</code></td></tr><tr><td><code>login_tenant_allowlist</code></td><td>Which tenants appear in the login screen's City dropdown. List both.</td><td><code>[kenya, kenya.nairobi]</code></td></tr><tr><td><code>map_center</code></td><td>Where the complaint map opens. <strong>Required — the deploy fails without it.</strong></td><td><code>{lat: -1.2864, lng: 36.8172}</code></td></tr><tr><td><code>pgr_boundary_highest_level</code><br><code>pgr_boundary_lowest_level</code><br><code>boundary_type</code></td><td>What your administrative areas are called, largest first. These are labels on the complaint form, so use the words your staff use.</td><td><code>County</code>, <code>Ward</code>, <code>Ward</code></td></tr><tr><td><code>core_mobile_configs</code></td><td>Your country's phone-number rule. Get this wrong and every citizen signup is rejected.</td><td><code>+254</code> / <code>^0?[17][0-9]{8}$</code></td></tr><tr><td><code>core_postal_configs</code></td><td>Your country's postcode rule.</td><td><code>^[0-9]{5}$</code></td></tr><tr><td><code>secrets_path</code></td><td>Where this deployment's secrets are filed inside the secret store. Just a path.</td><td><code>kv/digit/mycity</code></td></tr><tr><td><code>bootstrap_secrets</code></td><td>Your passwords. Change all of them <strong>except</strong> <code>elasticsearch_master_password</code>, which must stay as written.</td><td>—</td></tr></tbody></table>

**Deploying to a separate server instead of this machine?** Change two more lines: put the server's address in `ansible_host`, and delete the `ansible_connection: local` line. If it has a real domain name and certificates, set `domain:` to that name and `tls_enabled: true`.

<details>

<summary>Under the hood — why there are five tenant settings and not one</summary>

Tenants are two levels: a **root** (`kenya`) and a **city** under it (`kenya.nairobi`). Five variables name them because different parts of the system need a different one, and they are genuinely not interchangeable:

| Variable             | Read by                                                                                                                      | Consequence of getting it wrong                                                                               |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `state_root`         | the JVM services, as `STATE_LEVEL_TENANT_ID` — it decides which tenant's encryption keys and MDMS defaults they load at boot | pointing it at a tenant that does not exist yet crash-loops `egov-workflow-v2` and `egov-enc-service`         |
| `state_tenant_id`    | the browser apps; the configurator takes its login-tenant default from the first segment of this                             | if it names a root the deploy never creates, there is no `ADMIN` there to log in as                           |
| `tenant_id`          | templates that need a single city value; also the city tenant the deploy bootstraps                                          | data lands under a tenant nothing queries                                                                     |
| `boot_tenant`        | only the opt-in CI suite, as `BOOT_TENANT`/`DIGIT_TENANT`                                                                    | inert unless `run_ci_tests: true` — but preflight still checks it sits under `state_root`                     |
| `ui_state_tenant_id` | the SPA, as the tenant it lands on                                                                                           | the wizard writes boundaries to the city; if the app reads the root instead, every location dropdown is empty |

The tenant-creation step only runs when `state_root` is something other than `pg`, and it creates `state_root` and `tenant_id` — **not** `state_tenant_id`. (`pg` is the demo tenant that ships inside the database dump; leaving `state_root: pg` means "use the demo data as-is".) If `state_tenant_id` names a root that was never created, the playbook notices there is no `ADMIN` there, points the app at `pg` so you can still log in, and prints exactly that.

The deploy does this in two phases on purpose: the stack boots against `pg`, your tenants are created through the API, and only then are the `STATE_LEVEL_TENANT_ID` values rewritten and the services restarted. That is why a first deploy restarts things partway through and why it takes as long as it does.

</details>

<details>

<summary>Under the hood — the rest of the settings</summary>

`quickstart.yml.example` is deliberately short. [`_example.yml`](https://github.com/egovernments/Citizen-Complaint-Resolution-System/blob/master/local-setup/ansible/inventory/host_vars/_example.yml) in the same directory is the full catalogue: every flag the playbook understands, what it does, what values it takes and what it pairs with. Highlights you are likely to want soon:

* `enable_search_stack` — Elasticsearch, the indexer and the employee inbox. Costs about 3 GB of RAM; without it the inbox screen returns 503, which is why `employee_module_denylist: [IM]` hides it by default.
* `enable_novu` — SMS, email and WhatsApp notifications. Eight more containers. There is a turn-key installer, `scripts/enable-notifications.sh`, rather than just the flag.
* `enable_keycloak` — single sign-on. DIGIT's own OTP login works without it.
* `enable_otp_services` — real SMS one-time passwords. Off means the citizen login OTP is always `123456`, which is what you want while testing.
* `observability_level` — `metrics`, `logs` or `traces` (the default, meaning everything). Lowering it deploys fewer monitoring containers.
* `run_ci_tests` — runs the Postman and Playwright suites at the end of every deploy. Adds 5–10 minutes.

Where a service has an `nginx_features.*` twin, you need **both**: the service flag runs it, the nginx flag makes it reachable from a browser. Keep every key in the `nginx_features` block — six of them are read without a fallback, so deleting a line fails the vhost render instead of turning that path off.

</details>

**Check:** the config is validated for you at the start of the next step, so there is nothing to run here.
{% endstep %}

{% step %}

### Deploy

```
./deploy.sh mycity
```

That is the whole command. Expect **30–60 minutes** on a first run and a few minutes after that. Pulling the stack alone is around ten minutes on Linux, and the quickstart config additionally builds the onboarding wizard, the UI bundle and the tenant-creation service from source on top of that.

Ansible prints nothing while a long task is running, which looks like a hang. It is not. Watch progress in a second terminal:

```
tail -f /opt/digit/digit-stack-up.mycity.progress
watch -n5 "docker ps --format '{{.Names}}\t{{.Status}}' | grep -E 'healthy|Exited|Restart'"
```

**Two things to expect on the way through:**

* **It asks for your sudo password, or fails saying it needs one.** The playbook installs packages and writes nginx config, which needs root. `deploy.sh` does not ask for the password itself, so on a local deployment, pass it through:

  ```
  ./deploy.sh mycity -K            # prompts for the sudo password
  ```

  Deploying to a remote server as `root` does not need this.
* **A preflight warning about `configurator_build`.** Expected and harmless: the value is produced by the build task later in the same run, so it cannot be set in your config beforehand.

<details>

<summary>Under the hood — passing extra flags, and re-running</summary>

`deploy.sh` forwards anything after the deployment name straight to `ansible-playbook`:

```
./deploy.sh mycity --tags=nginx        # only the nginx tasks
./deploy.sh mycity --start-at-task="..."   # resume from a named task
./deploy.sh mycity -vvv                # verbose, for debugging a failure
```

Two environment escape hatches, both for when you know what you are doing: `SKIP_LINT=1` skips the linters, `SKIP_PREFLIGHT=1` skips the config gate.

Re-running the whole thing is the normal way to recover from a failure part way through. The playbook is idempotent: work already done is detected and skipped, and only changed config causes a restart. It is not a fresh start, though — set `force_clean: true` for that (it removes containers and networks but keeps the database).

</details>
{% endstep %}

{% step %}

### Check that it worked

The last thing the deploy prints is a summary. Then, in a browser:

<table><thead><tr><th width="197.08203125">What</th><th>URL (local deployment)</th><th>URL (server with a domain)</th></tr></thead><tbody><tr><td>Employee app</td><td><a href="http://localhost/digit-ui/employee">http://localhost/digit-ui/employee</a></td><td><code>https://&#x3C;domain>/digit-ui/employee</code></td></tr><tr><td>Citizen app</td><td><a href="http://localhost/digit-ui/citizen">http://localhost/digit-ui/citizen</a></td><td><code>https://&#x3C;domain>/digit-ui/citizen</code></td></tr><tr><td>Onboarding wizard</td><td><a href="http://localhost/configurator/">http://localhost/configurator/</a></td><td><code>https://&#x3C;domain>/configurator/</code></td></tr><tr><td>Health dashboard</td><td><a href="http://localhost/status/">http://localhost/status/</a></td><td><code>https://&#x3C;domain>/status/</code></td></tr><tr><td>Dashboards (Grafana)</td><td><a href="http://localhost/grafana/">http://localhost/grafana/</a></td><td><code>https://&#x3C;domain>/grafana/</code></td></tr></tbody></table>

Unlike Options A and B, everything here is on the **normal web ports** (80, and 443 with TLS) because a real web server is in front of the stack — there is no `:18000` in these URLs.

From a terminal on the deployment machine:

```
# every container and its health
docker ps --format 'table {{.Names}}\t{{.Status}}'

# the health dashboard's own view, as JSON
curl -s http://localhost/status/api/v1/endpoints/statuses | head -40
```

`digit-gatus` never reports `healthy` — it declares no health check at all, so it sits at a plain `Up`. That is normal and not a failure.
{% endstep %}

{% step %}

### Log in

The deploy creates one administrator account per tenant. **On a stock config, those credentials are `ADMIN` / `eGov@123`, which are published in this repository — change them before anyone else can reach the machine.**

| Where                                | Username                | Password              | Which tenant                                                                 |
| ------------------------------------ | ----------------------- | --------------------- | ---------------------------------------------------------------------------- |
| Onboarding wizard (`/configurator/`) | `ADMIN`                 | `eGov@123`            | your **root** — the field is pre-filled from `state_tenant_id`               |
| Employee app (`/digit-ui/employee`)  | `ADMIN`                 | `eGov@123`            | pick from the City dropdown; only tenants in `login_tenant_allowlist` appear |
| Employees you onboard later          | their **employee code** | `eGov@123`            | their city tenant                                                            |
| Citizen app                          | a mobile number         | OTP `123456`          | —                                                                            |
| Grafana (`/grafana/`)                | `admin`                 | generated — see below | —                                                                            |

To change the administrator credentials, set `bootstrap_user` and `bootstrap_password` in your config and redeploy. To change the default password every new employee gets, set `bootstrap_secrets.egov_hrms_default_password` — but only before the first deploy, because those secrets are written to the secret store once and then owned by it.

Grafana's password is generated on the first deploy and stored in OpenBao. Read it back on the deployment machine:

```
sudo docker exec \
  -e BAO_TOKEN="$(sudo jq -r .root_token /opt/digit/.openbao/init.json)" \
  openbao bao kv get -field=grafana_admin_password kv/digit/mycity
```

<details>

<summary>Under the hood — where these accounts come from</summary>

The employee code overriding the username is not a typo: HRMS replaces the `userName` you supply with the employee code when it creates the record, and the employee code is what actually authenticates.

The citizen OTP is fixed at `123456` because `enable_otp_services` is off and Kong answers `/user-otp/*` with a canned response — no SMS provider needed. Turning real OTP on takes more than the flag; see the notes in `kong/kong.yml`.

Every other secret lives in OpenBao under your `secrets_path`, seeded from `bootstrap_secrets` on the **first** deploy only. Editing those values in your config afterwards does nothing; change them in OpenBao instead, and redeploy so services pick them up. The root token is at `/opt/digit/.openbao/init.json` (mode 0600) — **back that file up somewhere else**, because losing it means losing every secret in this deployment with no way to recover them. [`ansible/runbooks/01-openbao.md`](https://github.com/egovernments/Citizen-Complaint-Resolution-System/blob/master/local-setup/ansible/runbooks/01-openbao.md) covers reading, rotating and unsealing.

</details>

#### When it does not work

| What you see                                                                     | What it means                                                                                                                        | What to do                                                                                                          |
| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- |
| `ERROR: 'ansible-playbook' not found on PATH`                                    | Step 2 has not run, or your shell has not picked up `~/.local/bin`                                                                   | run `install-prereqs.sh`, then open a new terminal                                                                  |
| `preflight failed: db_fast_path: true requires db_fast_path_ack_data_wipe: true` | The database loader recreates the Postgres container, which destroys data held in an anonymous volume, so it wants that acknowledged | on a first deploy, set `db_fast_path_ack_data_wipe: true`. On a machine with data you care about, back it up first  |
| `preflight failed: enable_mcp: true requires docker_registry`                    | The tenant-creation service needs a registry name even when it is built locally                                                      | keep the `docker_registry` line from the template                                                                   |
| Ansible fails on an apt/dnf or nginx task with a permissions error               | `deploy.sh` never prompts for `sudo`                                                                                                 | re-run as `./deploy.sh mycity -K`                                                                                   |
| A container keeps restarting                                                     | usually memory                                                                                                                       | `docker stats --no-stream`, then either free memory or turn off `enable_search_stack` / lower `observability_level` |
| The login screen has no City dropdown entry for your tenant                      | `login_tenant_allowlist`                                                                                                             | add the tenant and redeploy                                                                                         |
| Login fails for `ADMIN` on your root tenant                                      | that root was never created — see the tenant note in Step 3                                                                          | check the deploy output for the line naming the fallback to `pg`                                                    |

For deeper diagnosis, including reading logs and metrics, see the [operations handbook](https://github.com/egovernments/Citizen-Complaint-Resolution-System/blob/master/docs/operations/README.md).
{% endstep %}

{% step %}

### Onboard a tenant

The stack is running but has no city data in it yet — no wards, no departments, no complaint types, no staff. That comes next, in the browser:

**→** [**Onboarding & Add-ons guide**](https://github.com/egovernments/Citizen-Complaint-Resolution-System/blob/master/local-setup/docs/ONBOARDING-AND-ADDONS.md)
{% endstep %}
{% endstepper %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.digit.org/complaints-management/deploy/setup/single-machine-setup/ansible.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
