---
title: "High Availability Cluster with Docker Compose"
description: "Run a HeliosDB Nano primary and two streaming-replication standbys from the published container image with Docker Compose."
---

This page runs one primary and two standbys from the published image
`ghcr.io/heliosdatabase/heliosdb-nano` with Docker Compose on a Linux host. For
how replication behaves, the sync modes and the current limits, read the
[High Availability Hands-On Tutorial](https://www.heliosdb.com/docs/nano/guides/ha_hands_on_tutorial/) first; this page
covers only what is specific to containers.

## Prerequisites

- Docker Engine with the Compose plugin (`docker compose`).
- `psql` on the host.

## Step 1: Create the replication token

```bash
mkdir -p ~/nano-ha-docker && cd ~/nano-ha-docker

head -c 32 /dev/urandom > replication.token
chmod 600 replication.token
# The image runs as uid 999; the token must be owned by that user.
sudo chown 999:999 replication.token
```

The token authenticates standbys to the primary. It must be a regular file with
exactly 32 bytes, mode `0600` or stricter, owned by the user that runs the server.

## Step 2: Write the Compose file

Create `docker-compose.yml`:

```yaml
x-image: &image ghcr.io/heliosdatabase/heliosdb-nano:latest   # pin a release tag in production

x-health: &health
  test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8080/health"]
  interval: 5s
  timeout: 3s
  retries: 12

services:
  primary:
    image: *image
    command:
      - start
      - --replication-role
      - primary
      - --replication-port
      - "5433"
      - --sync-mode
      - semi-sync
      - --replication-token-file
      - /run/secrets/replication.token
      - --allow-insecure-replication-remote
    environment:
      HELIOSDB_PASSWORD: change-me
    volumes:
      - primary-data:/data
      - ./replication.token:/run/secrets/replication.token:ro
    ports:
      - "127.0.0.1:15432:5432"
    healthcheck: *health

  standby1:
    image: *image
    entrypoint: ["heliosdb-nano"]
    command:
      - start
      - --data-dir
      - /data/standby
      - --listen
      - 0.0.0.0
      - --auth
      - scram-sha-256
      - --replication-role
      - standby
      - --primary-host
      - primary:5433
      - --sync-mode
      - semi-sync
      - --replication-token-file
      - /run/secrets/replication.token
    environment:
      HELIOSDB_PASSWORD: change-me
    volumes:
      - standby1-data:/data
      - ./replication.token:/run/secrets/replication.token:ro
    ports:
      - "127.0.0.1:15442:5432"
    healthcheck: *health
    depends_on:
      primary:
        condition: service_healthy

  standby2:
    image: *image
    entrypoint: ["heliosdb-nano"]
    command:
      - start
      - --data-dir
      - /data/standby
      - --listen
      - 0.0.0.0
      - --auth
      - scram-sha-256
      - --replication-role
      - standby
      - --primary-host
      - primary:5433
      - --sync-mode
      - semi-sync
      - --replication-token-file
      - /run/secrets/replication.token
    environment:
      HELIOSDB_PASSWORD: change-me
    volumes:
      - standby2-data:/data
      - ./replication.token:/run/secrets/replication.token:ro
    ports:
      - "127.0.0.1:15452:5432"
    healthcheck: *health
    depends_on:
      primary:
        condition: service_healthy

volumes:
  primary-data:
  standby1-data:
  standby2-data:
```

Notes on this file:

- **Primary.** The image entrypoint adds `--data-dir /data`, `--listen 0.0.0.0`,
  `--auth scram-sha-256` (from `HELIOSDB_PASSWORD`), a loopback-only HTTP listener
  and a self-signed TLS certificate in `/data/tls`. The replication listener binds
  `0.0.0.0:5433` inside the container network.
- **Plaintext replication.** Because the replication listener is not on loopback,
  the primary refuses a token unless `--allow-insecure-replication-remote` is set:
  the token and the WAL cross the Docker network unencrypted. Keep port 5433
  unpublished (there is no `ports:` entry for it), use this only on a private
  network or a protected tunnel, and store the password in a Docker secret with
  `HELIOSDB_PASSWORD_FILE` outside of tests. Encryption of the replication channel
  is planned.
- **Standbys.** A standby refuses a data directory that already exists, and the
  image entrypoint creates the data directory before it starts the server. The
  standbys therefore run the binary directly (`entrypoint: ["heliosdb-nano"]`) and
  point `--data-dir` at `/data/standby`, which the standby creates on its first
  start. Without the entrypoint there is no generated TLS certificate; add
  `--tls-cert` and `--tls-key` to the standby command to offer TLS.
- **Health checks.** A standby starts its HTTP listener only after it has caught up
  with the primary, so `service_healthy` on a standby means it is serving reads.
  The shorter interval here replaces the image default of 30 seconds.

## Step 3: Start the cluster

```bash
docker compose up -d
docker compose ps
```

The standbys start once the primary is healthy. Start all three together, before
writing data: a standby added later to a primary that already holds data is refused
with `full resync required` (re-seeding is planned).

## Step 4: Verify replication

```bash
export PGPASSWORD=change-me

psql -h 127.0.0.1 -p 15432 -U postgres -d postgres <<'SQL'
CREATE TABLE orders (id INT PRIMARY KEY, item TEXT, qty INT);
INSERT INTO orders VALUES (1, 'widget', 3), (2, 'gadget', 5);
SELECT node_id, address, state, flush_lsn, apply_lsn, lag_bytes FROM pg_replication_standbys;
SQL

psql -h 127.0.0.1 -p 15442 -U postgres -d postgres -c "SELECT * FROM orders ORDER BY id;"
psql -h 127.0.0.1 -p 15452 -U postgres -d postgres -c "SELECT * FROM orders ORDER BY id;"

# A write on a standby is refused (SQLSTATE 25006)
psql -h 127.0.0.1 -p 15442 -U postgres -d postgres -c "INSERT INTO orders VALUES (3, 'gizmo', 1);"
```

## Step 5: Logs and restarts

```bash
docker compose logs -f primary
docker compose logs standby1 | grep -i -E "history|caught up|resync|reconnect"

# Restart one standby; it resumes from its stored position
docker compose restart standby2
```

A restarted standby resumes only while the primary still holds the WAL it is
missing; see [Restart a standby](https://www.heliosdb.com/docs/nano/guides/ha_hands_on_tutorial/#part-4-restart-a-standby).
When the primary stops, the standbys keep serving reads and keep retrying the
connection; writes wait for the primary to come back. Promotion and automatic
failover are planned.

## Step 6: Clean up

```bash
docker compose down        # keep the volumes
docker compose down -v     # remove the volumes as well
```

After `down -v` the next `up -d` creates a new primary history and new standby
directories.

## Related pages

- [High Availability Hands-On Tutorial](https://www.heliosdb.com/docs/nano/guides/ha_hands_on_tutorial/): flags,
  configuration file, sync modes, troubleshooting.
- [High Availability Checkpoints](https://www.heliosdb.com/docs/nano/guides/ha_interactive_tutorial/): exercises to run
  against a running setup.
- [Production Deployment](https://www.heliosdb.com/docs/nano/guides/production_deployment/): secrets, TLS and resource limits
  for the container image.
