High Availability Cluster 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 first; this page
covers only what is specific to containers.
Prerequisites
- Docker Engine with the Compose plugin (
docker compose). psqlon the host.
Step 1: Create the replication token
mkdir -p ~/nano-ha-docker && cd ~/nano-ha-docker
head -c 32 /dev/urandom > replication.tokenchmod 600 replication.token# The image runs as uid 999; the token must be owned by that user.sudo chown 999:999 replication.tokenThe 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:
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(fromHELIOSDB_PASSWORD), a loopback-only HTTP listener and a self-signed TLS certificate in/data/tls. The replication listener binds0.0.0.0:5433inside the container network. - Plaintext replication. Because the replication listener is not on loopback,
the primary refuses a token unless
--allow-insecure-replication-remoteis set: the token and the WAL cross the Docker network unencrypted. Keep port 5433 unpublished (there is noports:entry for it), use this only on a private network or a protected tunnel, and store the password in a Docker secret withHELIOSDB_PASSWORD_FILEoutside 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-dirat/data/standby, which the standby creates on its first start. Without the entrypoint there is no generated TLS certificate; add--tls-certand--tls-keyto 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_healthyon a standby means it is serving reads. The shorter interval here replaces the image default of 30 seconds.
Step 3: Start the cluster
docker compose up -ddocker compose psThe 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
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
docker compose logs -f primarydocker compose logs standby1 | grep -i -E "history|caught up|resync|reconnect"
# Restart one standby; it resumes from its stored positiondocker compose restart standby2A restarted standby resumes only while the primary still holds the WAL it is missing; see 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
docker compose down # keep the volumesdocker compose down -v # remove the volumes as wellAfter down -v the next up -d creates a new primary history and new standby
directories.
Related pages
- High Availability Hands-On Tutorial: flags, configuration file, sync modes, troubleshooting.
- High Availability Checkpoints: exercises to run against a running setup.
- Production Deployment: secrets, TLS and resource limits for the container image.