Skip to content
Rowsafe
Docs

Run the agent in Docker

Protect PostgreSQL running in the official postgres image, with the Rowsafe agent as a sidecar container. No changes to your image.

If PostgreSQL runs in the official postgres:NN image (Debian or Alpine), you don't install anything on the Docker host. The agent runs as a sidecar container in the same compose project. You add a few volume mounts and one service. Adoption then works like on a server: plan, apply, restart the container yourself, verify.

Linux Docker hosts only for production

Docker Desktop on macOS or Windows runs containers in a VM whose disk and fsync behaviour Rowsafe doesn't guarantee. Use it to try Rowsafe, not for data you need to recover. Kubernetes isn't covered.

How it works

 compose project
 +-------------------------------+        +------------------------------------------+
 | postgres (postgres:17, stock) |        | rowsafe-agent (ghcr.io/rowsafe/agent:    |
 |                               |        |                <version>-pg17)           |
 |  archive_command:             |        |                                          |
 |   cp + fsync + rename --------+--+     |  spool pusher: pgbackrest archive-push   |
 |                               |  |     |    each file in order, delete after OK --+--> your bucket
 |  /var/lib/postgresql/data  <--+--+-----+-- same volume, same path, read-only      |    (encrypted)
 |  /var/run/postgresql       <--+--+-----+-- socket: SQL                            |
 |  /rowsafe-spool/<db>/      <--+--+-----+-- spool: WAL waiting to be pushed        |
 +-------------------------------+        +------------------------------------------+
  • The stock postgres image has no pgBackRest. So archive_command only copies each WAL file into a shared spool volume, with standard tools (cp, sync, mv), and never overwrites a different file.
  • The agent pushes spooled files to your bucket with pgBackRest, in order, and deletes each one only after it is stored.
  • Backups, checks, restore points and drills run in the agent container. It reads the data volume read-only, at the same path.
  • Nothing runs inside the PostgreSQL container except archive_command. Rowsafe never restarts it.

The spool must be on durable disk

PostgreSQL treats a WAL file as archived once it is in the spool. Until the agent has pushed it, the spool holds the only copy. Use a named volume on local disk, never tmpfs or a network filesystem that ignores fsync. Don't delete spooled files by hand.

Set up

You need a one-time enrollment token (rowsafe hosts enroll-token), a bucket and its keys, and an encryption passphrase stored in your secret manager first. See steps 2 and 3 of Adopt an existing database.

Put the agent's settings in rowsafe-agent.env next to your compose file. Make it readable only by you (chmod 600) and never commit it:

rowsafe-agent.env
ROWSAFE_ENROLL_TOKEN=rse_...        # first start only; delete it afterwards
ROWSAFE_REPO_S3_ENDPOINT=<account-id>.eu.r2.cloudflarestorage.com
ROWSAFE_REPO_S3_BUCKET=app-rowsafe
ROWSAFE_REPO_S3_KEY=...
ROWSAFE_REPO_S3_KEY_SECRET=...
ROWSAFE_REPO_CIPHER_PASS=...
# ROWSAFE_PG_USER=app               # if POSTGRES_USER is not postgres

Then add the agent to your compose file. Your postgres service keeps its image, environment, networks and ports.

compose.yml
services:
  postgres:
    image: postgres:17
    volumes:
      - pgdata:/var/lib/postgresql/data
      - pgsocket:/var/run/postgresql        # added
      - rowsafe-spool:/rowsafe-spool        # added

  rowsafe-agent:
    image: ghcr.io/rowsafe/agent:<version>-pg17
    hostname: db-1                          # the host name Rowsafe shows
    user: "999:999"                         # the postgres user of the Debian images
    restart: unless-stopped
    depends_on: [postgres]
    env_file: rowsafe-agent.env
    shm_size: 256m                          # drills start a scratch PostgreSQL here
    volumes:
      - pgdata:/var/lib/postgresql/data:ro  # same volume, same path, read-only
      - pgsocket:/var/run/postgresql
      - rowsafe-spool:/rowsafe-spool
      - rowsafe-state:/var/lib/rowsafe

volumes:
  pgdata:
  pgsocket:
  rowsafe-spool:
  rowsafe-state:

Pick the agent image's <version> from the releases. Its PostgreSQL major must match your server's.

Start it:

docker compose up -d
docker compose logs rowsafe-agent      # "enrolled"
rowsafe hosts list                     # db-1

Adding the two mounts recreates the postgres container: a few seconds of downtime, so do it in a maintenance window. With a bind mount instead of a named volume, mount the same host directory in both services at the same path.

Keep the rowsafe-state volume: it holds the host's identity and the generated pgBackRest configuration. If you lose it, enroll again with a new token.

Adopt

Adoption works like on a server. Only the restart differs:

rowsafe adopt app --host db-1        # read-only plan
rowsafe apply app                    # config, spool directory, ALTER SYSTEM + reload
docker compose restart postgres      # you, in a maintenance window
rowsafe verify app                   # proves WAL reaches the bucket, through the spool

The settings are written with ALTER SYSTEM into postgresql.auto.conf in the data volume, so they survive container restarts and re-creation.

Upgrade

The agent never updates itself in Docker. To upgrade, change the tag and recreate the container, ideally between backups:

docker compose up -d rowsafe-agent

If a backup or drill is interrupted, the new container reports it as failed and the schedule runs it again. WAL waiting in the spool is pushed by the new container.

Rowsafe doesn't do PostgreSQL major upgrades. After one, switch to the agent tag for the new major and adopt the database again, so its repository starts a new history.

Watch the spool

The agent reports the spool with every heartbeat. In Docker, the WAL status that rowsafe show app prints describes your bucket, not the spool: "last archived" is the last file pushed to the bucket, and failed pushes count as archiving failures. If the oldest spooled file waits longer than 5 minutes (ROWSAFE_SPOOL_STALL_AFTER), that counts as a failure too: you get the wal_archiving_failing alert and the database stops counting as protected. The API (GET /v1/databases/{ref}) also has the spool's file count, size and last push error.

The image's health check fails when a spool is stalled, so docker ps shows the container as unhealthy.

Troubleshooting

The agent checks its environment at startup and names the fix:

MessageFix
refusing to run as rootSet user: "999:999" (or "70:70" for Alpine images).
PostgreSQL's socket directory /var/run/postgresql is owned by uid 70, but the agent runs as uid 999Alpine image: use the agent's -alpine tag and user: "70:70".
the agent runs as uid N, which has no user in this imageUse the agent tag that matches your postgres image flavour.
the WAL spool /rowsafe-spool is missingMount the spool volume in both services.
the WAL spool ... is owned by uid 0Another container created the volume first. Recreate it while empty, or chown 999:999 it.
PostgreSQL's data directory ... is not visible in the agent containerMount the data volume at the same path in both services (for 18+, at /var/lib/postgresql).
... belongs to another cluster (system identifier ...)The agent sees a different volume at that path.
this agent image has no PostgreSQL N server binariesUse the -pgN tag for your server's major version.

If WAL piles up in the spool, docker compose logs rowsafe-agent shows the pgBackRest error. The spool drains by itself once the cause is fixed.

Edit on GitHub