PostgreSQL maintenance image for scheduled backups and restores. https://git.openso.de/openso/postgres-maintenance
  • Shell 69%
  • Python 26.8%
  • Dockerfile 4.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Marcel Henneke 36903b8a78
All checks were successful
ci / compose (push) Successful in 13s
ci / shellcheck (push) Successful in 3s
ci / conventions (push) Successful in 2s
ci / image (push) Successful in 37s
docs(readme): use short volume syntax in compose example (#2)
2026-11-01 14:34:21 +01:00
.forgejo Build Supercronic from source on golang 1.26.10 and rewrite README (#1) 2026-11-01 13:44:16 +01:00
.githooks Build the PostgreSQL maintenance image with pinned backup tools. 2026-11-01 12:49:11 +01:00
.openso/scripts Build the PostgreSQL maintenance image with pinned backup tools. 2026-11-01 12:49:11 +01:00
.dockerignore Build the PostgreSQL maintenance image with pinned backup tools. 2026-11-01 12:49:11 +01:00
AGENTS.md Build Supercronic from source on golang 1.26.10 and rewrite README (#1) 2026-11-01 13:44:16 +01:00
backup.postgresql_maintenance.sh Build Supercronic from source on golang 1.26.10 and rewrite README (#1) 2026-11-01 13:44:16 +01:00
Dockerfile Build Supercronic from source on golang 1.26.10 and rewrite README (#1) 2026-11-01 13:44:16 +01:00
entrypoint.postgresql_maintenance.sh Build Supercronic from source on golang 1.26.10 and rewrite README (#1) 2026-11-01 13:44:16 +01:00
LICENSE Build the PostgreSQL maintenance image with pinned backup tools. 2026-11-01 12:49:11 +01:00
README.md docs(readme): use short volume syntax in compose example (#2) 2026-11-01 14:34:21 +01:00

postgres-maintenance

Scheduled PostgreSQL backups and one-shot restores. The image talks to the database over TCP. It is not the database server. The server image is git.openso.de/openso/postgres.

The customer stack lives in docker-templates. This repository is the image only.

Image

git.openso.de/openso/postgres-maintenance:main

docker-templates uses git.openso.de/openso/postgres-maintenance with no tag. Docker resolves that reference to :latest. :latest tracks the same build as :main.

The process runs as uid/gid 999 (postgres), the same user as the server image. The root filesystem is expected to be read-only. Writable paths are /backup, /restore, and the tmpfs mounts from the stack. A physical restore also needs the data volume mounted read-write at /var/lib/postgresql for that one run.

PGDATA for this major is /var/lib/postgresql/18/docker. A new PostgreSQL major changes that path and the FROM pin together with git.openso.de/openso/postgres.

Runtime

On start the entrypoint reads POSTGRES_PASSWORD_FILE and then:

  1. If /restore contains full_*.tar.zst, runs a physical restore and exits. The server must be stopped. pg_isready succeeding aborts the run. The chain is extracted under /restore/.tmp/restore_chain, combined with pg_combinebackup when incrementals are present, and copied into PGDATA. Archives in /restore are removed after a successful copy.
  2. Otherwise, if /restore contains a logical archive, restores it and exits. The server must be up. Other TCP clients on that database abort the run. POSTGRES_RESTORE_STRICT=true allows one archive. SQL files go through psql. Custom dumps go through pg_restore --clean --if-exists.
  3. Otherwise starts Supercronic with /usr/local/bin/backup.cron. That file is not in the image. The stack bind-mounts it. Supercronic runs /usr/local/bin/backup.sh with full, incremental, dump, or globals.

POSTGRES_RESTORE_DRY_RUN=true logs the restore and does not write data or delete archives.

Physical backups land in /backup/<YYYYMMDD>/ as full_<date>_<nn>.tar.zst and incremental_<date>_<nn>_<nn>.tar.zst, plus a .manifest used as the next incremental base. Logical files are raw zstd SQL: dump_<date>_<HHMMSS>.sql.zst and globals_<date>_<HHMMSS>.sql.zst. The workspace is /backup/.tmp/postgresql_backup.

Incremental backup needs summarize_wal=on on the server. The postgres image sets that. A missing manifest for the day falls back to a full backup.

Environment

POSTGRES_PASSWORD_FILE is required. The file is a Docker secret, one line, the password. Do not put the password in the Compose file.

Unset names use the default.

Variable Default Purpose
POSTGRES_PASSWORD_FILE (required) Path to the password file.
POSTGRES_USER postgres Role used for backup and restore.
POSTGRES_DB postgres Database for logical backup, logical restore, and pg_isready.
POSTGRES_DB_HOST postgresql Server hostname. DB_HOST is the fallback when this is unset.
POSTGRES_PORT 5432 Server port.
POSTGRES_BACKUP_RETENTION_DAYS 14 Delete /backup/YYYYMMDD directories older than this many days.
POSTGRES_BACKUP_COMPRESS_LEVEL 3 zstd level passed to the compressor.
POSTGRES_BACKUP_DEBUG false true keeps debug lines. Any other value hides them.
POSTGRES_BACKUP_FULL_ARGS empty Extra arguments appended to pg_basebackup for a full backup.
POSTGRES_BACKUP_INCREMENTAL_ARGS empty Extra arguments appended to pg_basebackup for an incremental backup.
POSTGRES_BACKUP_DUMP_ARGS empty Extra arguments appended to pg_dump.
POSTGRES_BACKUP_GLOBAL_ARGS empty Extra arguments appended to pg_dumpall --globals-only.
POSTGRES_RESTORE_STRICT true true aborts when more than one logical archive is in /restore.
POSTGRES_RESTORE_DEBUG false true prints restore debug lines.
POSTGRES_RESTORE_DRY_RUN false true skips writes and archive deletion.
POSTGRES_RESTORE_PSQL_ARGS empty Extra arguments appended to psql.
POSTGRES_RESTORE_PGRESTORE_ARGS empty Extra arguments appended to pg_restore.
POSTGRES_RESTORE_COMBINE_ARGS empty Extra arguments appended to pg_combinebackup.

TZ is the container timezone. Backup directory names use the local date.

Retention, compression, and the extra argument variables do not install packages and do not select another image.

Logical restore accepts .sql, .sql.gz, .sql.zst, .dump, .dump.gz, and .dump.zst.

Compose

Use the template in docker-templates when this container sits next to postgres. The shape below is that service. The schedule file is a bind mount. Create ./backup and ./restore on the host before start, owned by uid/gid 999.

services:
  postgresql_maintenance:
    image: git.openso.de/openso/postgres-maintenance
    user: "999:999"
    read_only: true
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
    init: true
    tmpfs:
      - /tmp:rw,noexec,nosuid,nodev,size=64m
    volumes:
      - postgresql-data:/var/lib/postgresql:ro
      - ./backup.cron:/usr/local/bin/backup.cron:ro
      - ./backup:/backup:rw
      - ./restore:/restore:rw
    environment:
      TZ: Europe/Berlin
      POSTGRES_USER: app
      POSTGRES_DB: app
      POSTGRES_DB_HOST: postgresql
      POSTGRES_PASSWORD_FILE: /run/secrets/POSTGRES_PASSWORD
      POSTGRES_BACKUP_RETENTION_DAYS: "14"
    secrets:
      - POSTGRES_PASSWORD
    depends_on:
      postgresql:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "pgrep supercronic >/dev/null 2>&1"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s

secrets:
  POSTGRES_PASSWORD:
    file: ./secrets/POSTGRES_PASSWORD

Example backup.cron. The incremental line skips hour 0 so it does not overlap the daily full backup.

0 0 * * * /usr/local/bin/backup.sh full
0 1-23 * * * /usr/local/bin/backup.sh incremental

The data volume stays read-only (:ro) for backups and for a logical restore. A physical restore drops :ro on that same line, keeps read_only: true on the container, stops postgresql, and starts this service with --force-recreate --no-deps. After the container exits, put :ro back and start the server again.

Contents

Piece Pin
PostgreSQL client tools postgres:18.6 (pg_dump, pg_dumpall, pg_basebackup, pg_combinebackup, psql)
Supercronic tag v0.2.43, rebuilt with Go 1.26.10 and golang.org/x/sys v0.49.0
Compression zstd=1.5.7+dfsg-1
Process probe procps=2:4.0.4-9 (pgrep for the healthcheck)

The upstream Supercronic release binary is Go 1.25.7. The image scan fails on fixed HIGH findings in that standard library. This image compiles the same v0.2.43 tag. The build checks that supercronic -version reports v0.2.43.

postgres:18.6 ships gosu built with Go 1.24.6. This image deletes /usr/local/bin/gosu. The entrypoint does not drop privileges. The Compose user is already 999:999.

Build

A push to main builds and pushes :main and :latest. A tag v* builds and pushes :<tag> and :latest. The first commit on main is that push, so the first image is produced without a manual build.

A version bump is a change of the FROM tag, an apt pin, SUPERCRONIC_VERSION, SUPERCRONIC_SHA256, or GOLANG_X_SYS_VERSION. Do not resolve a floating latest release during the build.