- Shell 69%
- Python 26.8%
- Dockerfile 4.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .forgejo | ||
| .githooks | ||
| .openso/scripts | ||
| .dockerignore | ||
| AGENTS.md | ||
| backup.postgresql_maintenance.sh | ||
| Dockerfile | ||
| entrypoint.postgresql_maintenance.sh | ||
| LICENSE | ||
| README.md | ||
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:
- If
/restorecontainsfull_*.tar.zst, runs a physical restore and exits. The server must be stopped.pg_isreadysucceeding aborts the run. The chain is extracted under/restore/.tmp/restore_chain, combined withpg_combinebackupwhen incrementals are present, and copied intoPGDATA. Archives in/restoreare removed after a successful copy. - Otherwise, if
/restorecontains a logical archive, restores it and exits. The server must be up. Other TCP clients on that database abort the run.POSTGRES_RESTORE_STRICT=trueallows one archive. SQL files go throughpsql. Custom dumps go throughpg_restore --clean --if-exists. - 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.shwithfull,incremental,dump, orglobals.
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.