---
kind: guide
zz: "0.4.0"
updated: 2026-10-09
state: draft
rules: https://zriz.io/llms.txt
---
# Deploy a zriz runner with Docker

Build the runner image from its source, give it a token and a config file, and start it with Docker. The work is done when a run has status `pass` on this runner.

A [runner](https://zriz.io/docs/words.md#runner) is a program in your network. It makes the calls of a run and sends back only what the checks need.

The runner is open source. The source is in the [ZrizTech/runner](https://github.com/ZrizTech/runner) repository.

It holds each secret. [Security](https://zriz.io/docs/security.md#never-leaves) tells what stays in your network.

## Do you need one? {#need-one}

Yes. Each run needs a runner. For a first test, start one on this machine: [A runner on this machine](https://zriz.io/docs/runner.md#on-this-machine).

Deploy a runner when:

- the app or the database under test is not reachable from your machine,
- you want runs from CI or on a schedule against a shared environment,
- you use `browser` or `cli` resources.

## How it connects {#connects}

The runner makes outbound connections only: to `https://zriz.io`, and to the resources in its config. Nothing listens. You open no port and change no firewall rule.

## Before you start {#before-you-start}

- Docker is installed on the host of the runner: `docker --version`
- `git` is installed on that host: `git --version`
- `zz` is logged in: `zz runners`
- The project has the sample pipeline: `zz validate hello`

## 1. Make a token {#make-a-token}

### Do

- `<runner>` is a name that you give to the runner. Example: `ci`.
- `<env>` is the name of the [environment](https://zriz.io/docs/words.md#environment) that the runner serves. Example: `staging`.

```sh
zz runners token create <runner> --env <env>
```

### Why

The token fixes the environment of the runner. The token is `data.token` of the reply: store it as a secret.

### You should see

```text
{"ok":true,"command":"runners",
"token":"zrt_
```

### If not

- `not logged in`: Do `zz login`. Then do the command again.

## 2. Write the config {#write-the-config}

### Do

Make the file `config.json` on the host of the runner. The sample pipeline `hello` calls the [resource](https://zriz.io/docs/words.md#resource) `target`, so the config names `target`.

- `<base url>` is the URL of your app, as the runner reaches it. Example: `https://staging.shop.example`.

> Common mistake: `localhost` in `<base url>`. In a container, `localhost` is the container itself.

`config.json`:

```json
{
  "cloud": { "url": "https://zriz.io", "token-env": "ZRIZ_RUNNER_TOKEN" },
  "resources": {
    "target": { "type": "http", "base-url": "<base url>" }
  }
}
```

```sh
grep '"token-env"' config.json
```

### Why

The `resources` map is the allowlist: the runner calls no other target. The id of a resource is the name of its file in `.zriz/resources`, without `.json`.

### You should see

```text
"token-env": "ZRIZ_RUNNER_TOKEN"
```

### If not

- `No such file or directory`: Make the file in the current folder. Then do the command again.

## 3. Start the runner {#start-it}

### Do

Do the commands in the folder that has `config.json`. The build compiles the runner, so it takes some minutes.

- `<token>` is `data.token` from step 1.

Make the file `runner.env` with an editor, not with a command. A command puts the token in the shell history. The file has one line:

```text
ZRIZ_RUNNER_TOKEN=<token>
```

```sh
chmod 600 runner.env
git clone https://github.com/ZrizTech/runner
docker build -t zriz-runner runner
docker run -d --name zriz-runner \
  --env-file runner.env \
  -e ZRIZ_RUNNER_CONFIG=/config/config.json \
  -v "$PWD/config.json:/config/config.json:ro" \
  zriz-runner
sleep 5
docker logs zriz-runner
```

### Why

The runner is open source, so you build the image from code that you can read. The file `runner.env` keeps the token out of the shell history and out of `config.json`.

### You should see

```text
runner connected
```

### If not

- `started`: The runner is not connected yet. Wait 10 seconds. Then do `docker logs zriz-runner` again.
- `poll refused`: The cloud refused the token. Do step 1 again, and put the new token in `runner.env`. Then do `docker rm -f zriz-runner` and `docker run` again.
- `poll failed`: The runner cannot reach `https://zriz.io`. Permit outbound HTTPS from the host.
- `environment variable ZRIZ_RUNNER_TOKEN is not set`: `runner.env` must have the line of the token. `docker run` must have `--env-file runner.env`.
- `missing environment variable`: A `${NAME}` of the config has no value. Add its line to `runner.env`.
- `config:`: The line names the fault in `config.json`. Correct the file.
- `is already in use by container`: Do `docker rm -f zriz-runner`. Then do `docker run` again.

## 4. Check that a run uses it {#check-it}

### Do

Do the command in the folder of your service. The project must have the file `.zriz/environments/<env>.json`.

- `<env>` is the environment from step 1.

```sh
zz run hello --env <env>
```

### Why

`zz run` needs a runner that serves the environment of the [run](https://zriz.io/docs/words.md#run).

### You should see

```text
"status":"pass"
```

### If not

- `no environment file`: Make the file that the message names. [Pipelines](https://zriz.io/docs/pipelines.md) gives its format.
- `runner-unavailable`: No runner serves this environment. Do `zz runners`, and compare `env` of each runner.
- `no runner connected`: The runner is down. Do `docker logs zriz-runner`.
- `"status":"fail"`: Do the command in `hints` of the reply. It shows the failed [step](https://zriz.io/docs/words.md#step).

## A runner on this machine {#on-this-machine}

`zz runners init` writes the files of a runner for this machine. With `--start`, it also starts the runner. Docker must be installed.

```sh
zz runners init --start
```

The command has these options:

- `--env <env>` is the environment that the runner serves. The default is `default-env` of `.zriz/project.json`.
- `--name <name>` is the name of the runner. The default is the project name.
- `--start` starts the runner with Docker Compose and waits until it is connected. It waits 60 seconds at most.

The command writes three files in the folder `.zriz/runner/`:

| File | Content |
|---|---|
| `config.json` | The cloud URL, the name of the token variable, and one entry for each `http`, `sql`, and `browser` resource. |
| `runner.env` | The runner token and each value that the env file lists in `sensitive`. The file mode is 600. |
| `compose.yml` | The service of the runner. The service of the worker, when the project has a `browser` resource. |

The command adds the line `runner/` to `.zriz/.gitignore`. The folder belongs to one machine and is not committed.

The command follows these rules:

- The command makes the token with the call of `zz runners token create`. The token goes only into `runner.env`. The output never shows it.
- The command writes only a file that does not exist. It never changes a file that exists.
- The command makes a token only when it writes `runner.env`. A second call writes nothing and makes no token.
- The command never prompts.
- A `cli` resource is not in `config.json`, because the runner config needs the path of each command. A warning names the resource.
- A `browser` resource takes its URL from the env value `<RESOURCE>_URL`. The name is in capitals, with `_` for `-`. Example: `SHOP_WEB_URL` for the resource `shop-web`.
- A host `localhost` or `127.0.0.1` becomes `host.docker.internal` in `config.json`. A warning tells you that the service must listen on an address that a container can get to.

The image tag in `compose.yml` is fixed in the `zz` release. This release writes `ghcr.io/zriztech/runner:0.2.0`, and `ghcr.io/zriztech/worker:0.2.0` for a browser resource.

Check that the tag exists before you rely on it. The [runner README](https://github.com/ZrizTech/runner#quick-start-build-from-source) tells how to build the images from source.

To start the runner without `--start`, do this:

```sh
docker compose -f .zriz/runner/compose.yml up -d
```

To stop the runner, do this:

```sh
docker compose -f .zriz/runner/compose.yml down
```

If the start fails, the error is `runner-start-failed`. The files stay, and a second call does not write them again. The field `data.step` tells which part failed:

| `data.step` | Meaning | Fix |
|---|---|---|
| `docker-missing` | The system has no program `docker`. | Install Docker. Then do `zz runners init --start`. Or start the runner by hand with the commands above. |
| `compose-failed` | `docker compose` ended with an exit code that is not 0. | Do `docker compose -f .zriz/runner/compose.yml logs`. Correct the fault. Then do `zz runners init --start`. |
| `not-connected` | The runner did not connect in 60000 ms. | Do `docker compose -f .zriz/runner/compose.yml logs runner`. Check the token and the network. Then do `zz runners`. |

## Secrets and databases in the config {#config-secrets}

The config holds the names of secrets, not their values. This example has an HTTP resource with one secret, and a Postgres database.

`config.json`:

```json
{
  "cloud": { "url": "https://zriz.io", "token-env": "ZRIZ_RUNNER_TOKEN" },
  "resources": {
    "shop": {
      "type": "http",
      "base-url": "https://staging.shop.example",
      "secrets": ["SHOP_API_KEY"]
    },
    "shop-db": {
      "type": "sql",
      "connection": "postgres://<user>:${SHOP_DB_PASSWORD}@<host>:5432/<database>",
      "read-only": true
    }
  }
}
```

- The runner fills `${NAME}` in `cloud.url`, `base-url`, `connection`, and `origins` from its environment at the start. Put each value in `runner.env`, one line for each name.
- `secrets` lists names of environment variables. A step can use them as `${NAME}` in the headers, the body, and the query parameters of that HTTP resource.
- Do not put the token variable in `secrets`. The runner refuses that config.
- `cloud.url` must be `https`.
- All the projects of an org share its runner. Thus a resource id must be unique in the org: `ms-a-db`, not `db`.
- For a database, use a user that can only read: [Create a read-only Postgres user](https://zriz.io/docs/postgres-read-only-user.md).

| Database | Form of `connection` |
|---|---|
| Postgres | `postgres://user:pass@host:5432/db` |
| MySQL | `user:pass@tcp(host:3306)/db` |

[What can the runner do?](https://zriz.io/docs/security.md#runner-may) gives the rules that the runner obeys.

## Browser and command-line tests {#browser-cli}

A `browser` resource drives a web page. A `cli` resource runs a command that the config permits. The two types run in the worker, a second container that runs next to the runner.

The worker holds no secret of the runner. The file `ops/docker-compose.yml` of the runner repository starts the runner and the worker together.

The [runner README](https://github.com/ZrizTech/runner#config-reference) gives the config keys of the two types.

## More {#more}

- [Security](https://zriz.io/docs/security.md): what leaves your network and what stays.
- [Pipelines](https://zriz.io/docs/pipelines.md): how to write the tests.
