---
kind: guide
zz: "0.4.0"
updated: 2026-10-09
state: draft
rules: https://zriz.io/llms.txt
---
# Test a database in a private network

Put a zriz runner inside the network. It makes outbound connections only, so you open no port. The work is done when a run on the runner has status `pass`.

The [runner](https://zriz.io/docs/words.md#runner) reaches the database, and the internet does not. [Which connections does the runner make?](https://zriz.io/docs/security.md#outbound) gives the rule.

The connection string stays in the config of the runner. The example is for Postgres.

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

- `zz` is logged in: `zz runners`
- The project has the sample pipeline: `zz validate hello`

## 1. Deploy a runner in the network {#deploy-a-runner}

### Do

Do the guide [Deploy a zriz runner with Docker](https://zriz.io/docs/runner.md) on a host that can reach the database. Then come back.

The guide ends with a run of the sample on the runner. Keep its token, its file `config.json`, and the name of its [environment](https://zriz.io/docs/words.md#environment).

```sh
zz runners
```

### Why

The host needs outbound HTTPS to `https://zriz.io` and a route to the database. It needs no inbound rule.

### You should see

```text
{"ok":true,"command":"runners",
"connected":true
```

### If not

- `"connected":false`: The runner is down. Do `docker logs zriz-runner` on its host. The runner guide names each message.
- `not logged in`: Do `zz login`. Then do the command again.

## 2. Add the database to the runner {#add-the-database}

### Do

On the host of the runner, add one [resource](https://zriz.io/docs/words.md#resource) to `resources` of `config.json`. Keep the other keys as they are.

- `<base url>` is the value that the file has now.
- `<db>` is the id that you give to the database. It must be unique in your org. Example: `shop-db`.
- `<user>`, `<host>`, and `<database>` are the parts of the connection string. Use an account that can only read: [Create a read-only Postgres user](https://zriz.io/docs/postgres-read-only-user.md).
- `<password>` is the password of that account.
- `<token>` is the token of the runner guide.

`config.json`:

```json
{
  "cloud": { "url": "https://zriz.io", "token-env": "ZRIZ_RUNNER_TOKEN" },
  "resources": {
    "target": { "type": "http", "base-url": "<base url>" },
    "<db>": {
      "type": "sql",
      "connection": "postgres://<user>:${DB_PASSWORD}@<host>:5432/<database>",
      "read-only": true
    }
  }
}
```

The runner reads the config at its start. Start it again, with the password in its environment:

```sh
docker rm -f zriz-runner
docker run -d --name zriz-runner \
  -e ZRIZ_RUNNER_CONFIG=/config/config.json \
  -e ZRIZ_RUNNER_TOKEN=<token> \
  -e DB_PASSWORD=<password> \
  -v "$PWD/config.json:/config/config.json:ro" \
  zriz-runner
docker logs zriz-runner
```

### Why

The runner calls only the resources that its config names. It fills `${DB_PASSWORD}` from its own environment, so the password is in no file.

### 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.
- `missing environment variable`: Give `DB_PASSWORD` to `docker run` with `-e`.
- `config:`: The line names the fault in `config.json`. Correct the file.

## 3. Write the test {#write-the-test}

### Do

Do this step in the folder of your service. Make a resource file with the same id, and one [pipeline](https://zriz.io/docs/words.md#pipeline) that reads the database.

- `<db>` is the id from step 2.

A resource file must have the key `connection`. `zz` removes it before a run, and the runner uses the connection of its own config. Thus `DB_URL` needs no value.

`.zriz/resources/<db>.json`:

```json
{
  "type": "sql",
  "connection": "${env.DB_URL}",
  "read-only": true,
  "actions": {
    "ping": { "query": "select 1 as ok" }
  }
}
```

`.zriz/pipelines/db-ping.json`:

```json
{
  "description": "The runner reaches the database",
  "steps": [
    { "call": "<db>/ping", "expect": [["row-count", "==", 1]] }
  ]
}
```

```sh
zz validate db-ping
```

### Why

The files in the repository name the query and the [check](https://zriz.io/docs/words.md#check). They hold no host and no password.

### You should see

```text
{"ok":true,"command":"validate",
```

### If not

- `missing-action`: The `call` and the resource file do not agree. `error.valid` lists the actions. Correct the name.
- `"ok":false`: `error.file` and `error.message` name the fault. Correct it, then do the command again.

## 4. Run the test on the runner {#run-on-the-runner}

### Do

- `<env>` is the environment of the runner, from the runner guide.

```sh
zz run db-ping --env <env>
```

### Why

`zz run` needs a runner that serves the environment of the [run](https://zriz.io/docs/words.md#run). The runner is in your network, so the query comes from inside it.

### You should see

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

### If not

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

## Next {#next}

- Change the query to a `select` on a table of your own. [Pipelines](https://zriz.io/docs/pipelines.md) gives the checks on `rows`.
- [Test an API call and the database row](https://zriz.io/docs/api-and-database-test.md): one test for the API and the database.
- [What never leaves my network?](https://zriz.io/docs/security.md#never-leaves)
