Sign inSign up

miran2369/postgres-cloud-backup

By miran2369

•Updated about 1 month ago

Automated PostgreSQL backups with encrypted R2/Drive storage and Slack alerts.

Image
Developer tools
Databases & storage
0

28

miran2369/postgres-cloud-backup repository overview

⁠PostgreSQL Cloud Backup

A reusable Docker container for automated PostgreSQL database backups.

This image creates validated PostgreSQL custom-format backups using pg_dump -Fc, keeps local copies with configurable retention, encrypts cloud backups using rclone crypt, uploads them to Cloudflare R2 and/or Google Drive, and optionally sends Slack success/failure notifications.

The image is designed to be reusable across multiple projects and environments.

All project-specific configuration is supplied at runtime using environment variables. No project credentials, database passwords, cloud keys, Slack webhooks, or encryption passwords are baked into the Docker image.


⁠What This Container Does

The backup flow is:

PostgreSQL
    |
    | pg_dump -Fc
    v
Validated .backup file
    |
    +---------------------+
    |                     |
    v                     v
Local Storage        Encrypted Cloud Backup
                        |
              +---------+---------+
              |                   |
              v                   v
       Cloudflare R2        Google Drive
         optional             optional
              |
              v
      Slack notification
          optional

Each backup run performs the following:

1. Check PostgreSQL connectivity
2. Create a custom-format PostgreSQL backup
3. Verify the backup is not empty
4. Validate the archive with pg_restore
5. Upload to enabled cloud destinations
6. Verify the uploaded files exist
7. Apply local retention policy
8. Calculate backup statistics
9. Send success/failure notification

⁠Main Features

  • PostgreSQL custom-format backup using pg_dump -Fc
  • Backup validation using pg_restore -l
  • Automatic scheduled backups
  • Timezone-aware cron scheduling
  • Configurable local retention
  • Cloudflare R2 support
  • Optional Google Drive support
  • Cloud backup encryption using rclone crypt
  • Slack success/failure notifications
  • Automatic rclone.conf generation
  • No host-level rclone config required
  • No host-level cron configuration required
  • Reusable across DEV, QA, Production, or other environments
  • All project-specific values supplied at runtime
  • No credentials baked into the Docker image
  • Supports disaster-recovery workflows with independent restore clients

⁠Backup Format

Backups are created using:

pg_dump -Fc

This produces PostgreSQL custom-format archives.

Example logical backup file:

mydatabase_2026-09-27_08-00-00.backup

The backup is validated using:

pg_restore -l

A valid PostgreSQL custom-format archive begins with:

PGDMP

⁠Cloud Encryption

Cloud backups are encrypted using rclone crypt.

The default recommended configuration keeps filenames readable while encrypting file content.

Example logical file:

mydatabase_2026-09-27_08-00-00.backup

The raw object stored in Cloudflare R2 or Google Drive may appear as:

mydatabase_2026-09-27_08-00-00.backup.bin

This .bin file is encrypted.

Do not rename:

file.backup.bin

to:

file.backup

and try to restore it directly.

Renaming does not decrypt the content.

The backup must be downloaded through the matching rclone crypt configuration.


⁠Supported Destinations

⁠Local Backup

Every backup is first written to local persistent storage.

Local retention can be configured using:

LOCAL_RETENTION_HOURS=48

For example:

48 hours
72 hours
168 hours
⁠Cloudflare R2

Cloudflare R2 can be enabled with:

ENABLE_R2_BACKUP=true

Example configuration:

R2_ACCESS_KEY_ID=...
R2_SECRET_ACCESS_KEY=...
R2_BUCKET=my-database-backups
R2_ROOT_PATH=Production
R2_REGION=auto
R2_ENDPOINT=https://ACCOUNT_ID.r2.cloudflarestorage.com

The container automatically creates the required rclone S3-compatible configuration.

⁠Google Drive

Google Drive is optional.

Enable it with:

ENABLE_GDRIVE_BACKUP=true

Disable it with:

ENABLE_GDRIVE_BACKUP=false

When enabled, a previously authorized rclone-compatible Google OAuth token must be supplied.

Example:

GDRIVE_ROOT_PATH=Database Backups/Production
GDRIVE_SCOPE=drive
GDRIVE_CLIENT_ID=...
GDRIVE_CLIENT_SECRET=...
GDRIVE_TOKEN=...

The container does not perform interactive browser authorization.


⁠Slack Notifications

Slack notifications are optional.

Enable:

ENABLE_SLACK_NOTIFICATION=true

Disable:

ENABLE_SLACK_NOTIFICATION=false

Provide:

SLACK_WEBHOOK_URL=https://hooks.slack.com/...

A successful backup notification can contain:

Environment
Database
Server
Backup file
Backup size
Local copy count
Cloudflare R2 backup count
Cloudflare R2 storage usage
Google Drive backup count
Google Drive storage usage
Completion time

Failure notifications include:

Database
Server
Backup file
Failed line
Exit code
Failure time

⁠Timezone-Aware Scheduling

The container uses its own timezone configuration.

Example:

TZ=Asia/Dhaka
BACKUP_CRON=0 8 * * *

This means:

Daily at 08:00 Bangladesh time

even if the Docker host uses UTC or another timezone.

Example schedules:

Daily at 02:00:

BACKUP_CRON=0 2 * * *

Daily at 08:00:

BACKUP_CRON=0 8 * * *

Daily at 23:30:

BACKUP_CRON=30 23 * * *

⁠Environment Configuration

All project-specific settings are supplied using environment variables.

A typical .env file looks like:

# Docker
BACKUP_IMAGE=yourdockerhubusername/postgres-cloud-backup:1.0.0
BACKUP_CONTAINER_NAME=postgres-cloud-backup
BACKUP_RESTART_POLICY=unless-stopped
APP_NETWORK_NAME=your-project-network
BACKUP_HOST_PATH=/opt/postgres-backups

# Environment / Schedule
TZ=Asia/Dhaka
ENVIRONMENT=DEV
BACKUP_CRON=0 8 * * *
RUN_ON_STARTUP=false
SERVER_NAME_OVERRIDE=

# PostgreSQL
DB_HOST=postgres
DB_PORT=5432
DB_NAME=mydatabase
DB_USER=postgres
DB_PASSWORD=CHANGE_ME

# Local Backup
BACKUP_DIR=/backups/daily
LOCAL_RETENTION_HOURS=48

# Destinations
ENABLE_R2_BACKUP=true
ENABLE_GDRIVE_BACKUP=false
ENABLE_SLACK_NOTIFICATION=true
REMOTE_DAILY_DIR=daily

# Cloudflare R2
R2_REMOTE_NAME=r2
R2_CRYPT_REMOTE_NAME=r2-crypt
R2_ACCESS_KEY_ID=CHANGE_ME
R2_SECRET_ACCESS_KEY=CHANGE_ME
R2_BUCKET=my-database-backups
R2_ROOT_PATH=Dev
R2_REGION=auto
R2_ENDPOINT=https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com

# Google Drive - Optional
GDRIVE_REMOTE_NAME=gdrive
GDRIVE_CRYPT_REMOTE_NAME=gdrive-crypt
GDRIVE_ROOT_PATH=Database Backups/Dev
GDRIVE_SCOPE=drive
GDRIVE_CLIENT_ID=
GDRIVE_CLIENT_SECRET=
GDRIVE_TOKEN=

# Encryption
CRYPT_FILENAME_ENCRYPTION=off
CRYPT_DIRECTORY_NAME_ENCRYPTION=false
CRYPT_PASSWORD=CHANGE_ME
CRYPT_PASSWORD2=CHANGE_ME

# Slack
SLACK_WEBHOOK_URL=CHANGE_ME

# Internal
RCLONE_CONFIG_PATH=/config/rclone/rclone.conf

# Healthcheck
HEALTHCHECK_INTERVAL=30s
HEALTHCHECK_TIMEOUT=5s
HEALTHCHECK_RETRIES=3

⁠Docker Compose Example

services:
  postgres-backup:
    image: ${BACKUP_IMAGE}
    container_name: ${BACKUP_CONTAINER_NAME}
    restart: ${BACKUP_RESTART_POLICY}

    environment:
      TZ: ${TZ}
      ENVIRONMENT: ${ENVIRONMENT}

      DB_HOST: ${DB_HOST}
      DB_PORT: ${DB_PORT}
      DB_NAME: ${DB_NAME}
      DB_USER: ${DB_USER}
      DB_PASSWORD: ${DB_PASSWORD}

      BACKUP_DIR: ${BACKUP_DIR}
      BACKUP_CRON: ${BACKUP_CRON}
      LOCAL_RETENTION_HOURS: ${LOCAL_RETENTION_HOURS}
      RUN_ON_STARTUP: ${RUN_ON_STARTUP}

      ENABLE_R2_BACKUP: ${ENABLE_R2_BACKUP}
      ENABLE_GDRIVE_BACKUP: ${ENABLE_GDRIVE_BACKUP}
      ENABLE_SLACK_NOTIFICATION: ${ENABLE_SLACK_NOTIFICATION}

      REMOTE_DAILY_DIR: ${REMOTE_DAILY_DIR}

      R2_REMOTE_NAME: ${R2_REMOTE_NAME}
      R2_CRYPT_REMOTE_NAME: ${R2_CRYPT_REMOTE_NAME}
      R2_ACCESS_KEY_ID: ${R2_ACCESS_KEY_ID}
      R2_SECRET_ACCESS_KEY: ${R2_SECRET_ACCESS_KEY}
      R2_BUCKET: ${R2_BUCKET}
      R2_ROOT_PATH: ${R2_ROOT_PATH}
      R2_REGION: ${R2_REGION}
      R2_ENDPOINT: ${R2_ENDPOINT}

      GDRIVE_REMOTE_NAME: ${GDRIVE_REMOTE_NAME}
      GDRIVE_CRYPT_REMOTE_NAME: ${GDRIVE_CRYPT_REMOTE_NAME}
      GDRIVE_ROOT_PATH: ${GDRIVE_ROOT_PATH}
      GDRIVE_SCOPE: ${GDRIVE_SCOPE}
      GDRIVE_CLIENT_ID: ${GDRIVE_CLIENT_ID}
      GDRIVE_CLIENT_SECRET: ${GDRIVE_CLIENT_SECRET}
      GDRIVE_TOKEN: ${GDRIVE_TOKEN}

      CRYPT_FILENAME_ENCRYPTION: ${CRYPT_FILENAME_ENCRYPTION}
      CRYPT_DIRECTORY_NAME_ENCRYPTION: ${CRYPT_DIRECTORY_NAME_ENCRYPTION}
      CRYPT_PASSWORD: ${CRYPT_PASSWORD}
      CRYPT_PASSWORD2: ${CRYPT_PASSWORD2}

      SLACK_WEBHOOK_URL: ${SLACK_WEBHOOK_URL}

      RCLONE_CONFIG_PATH: ${RCLONE_CONFIG_PATH}

    volumes:
      - ${BACKUP_HOST_PATH}:${BACKUP_DIR}

    networks:
      - backup-network

    healthcheck:
      test: ["CMD", "/app/healthcheck.sh"]
      interval: ${HEALTHCHECK_INTERVAL}
      timeout: ${HEALTHCHECK_TIMEOUT}
      retries: ${HEALTHCHECK_RETRIES}

networks:
  backup-network:
    name: ${APP_NETWORK_NAME}
    external: true

⁠PostgreSQL Networking

The backup container connects directly to PostgreSQL over the Docker network.

Example:

DB_HOST=postgres

The PostgreSQL container and backup container must share the same Docker network.

No Docker socket mount is required.

This image does not need access to:

/var/run/docker.sock

⁠Starting the Backup Container

Create:

docker-compose.yml
.env

Then:

docker compose config

Review the configuration.

Start:

docker compose up -d

Check:

docker compose ps

Logs:

docker logs -f postgres-cloud-backup

⁠Manual Backup Test

Run:

docker exec postgres-cloud-backup /app/backup.sh

A successful backup should show something similar to:

Starting PostgreSQL backup
Backup created
Backup validation successful
Uploading to Cloudflare R2
Cloudflare R2 verified
BACKUP COMPLETED SUCCESSFULLY

⁠Run Backup Immediately on Startup

For initial testing:

RUN_ON_STARTUP=true

Then:

docker compose up -d --force-recreate

The container will run one backup immediately and then continue with the normal cron schedule.

For normal operation:

RUN_ON_STARTUP=false

⁠Local Backup Storage

Example host path:

BACKUP_HOST_PATH=/opt/postgres-backups

The container uses:

BACKUP_DIR=/backups/daily

Docker maps:

Host:
/opt/postgres-backups

Container:
/backups/daily

This means local backup files survive container replacement.


⁠Checking Backups

⁠Local
ls -lh /opt/postgres-backups
⁠Cloudflare R2

From inside the running container:

docker exec postgres-cloud-backup \
  rclone ls r2-crypt:daily

If custom names are used, replace:

r2-crypt
daily

with your configured values.


⁠Disaster Recovery

The cloud object stored by rclone crypt is encrypted.

Example cloud file:

mydatabase_2026-09-27_08-00-00.backup.bin

To restore it, use another rclone crypt configuration containing the same:

CRYPT_PASSWORD
CRYPT_PASSWORD2
filename encryption setting
directory encryption setting
remote path

Then:

rclone copyto \
  "r2-restore-crypt:daily/mydatabase_2026-09-27_08-00-00.backup" \
  "./mydatabase_2026-09-27_08-00-00.backup"

Verify:

head -c 5 mydatabase_2026-09-27_08-00-00.backup

Expected:

PGDMP

Then restore using:

pg_restore

or pgAdmin.


⁠Important Encryption Warning

Preserve these values securely:

CRYPT_PASSWORD
CRYPT_PASSWORD2

If these values are lost, encrypted historical backups may become impossible to decrypt.

For important systems, keep an independent recovery copy outside the backup server.


⁠Security

The Docker image contains no project-specific secrets.

Secrets such as:

DB_PASSWORD
R2_ACCESS_KEY_ID
R2_SECRET_ACCESS_KEY
GDRIVE_CLIENT_SECRET
GDRIVE_TOKEN
CRYPT_PASSWORD
CRYPT_PASSWORD2
SLACK_WEBHOOK_URL

must be supplied at runtime.

Never commit the real .env file.

Recommended .gitignore:

.env
*.env
!.env.example

Recommended .dockerignore:

.env
*.env
.git
secrets/

Direct environment variables are convenient and supported.

However, users with sufficient Docker/root access can potentially inspect runtime environment variables.

For higher-security deployments, use external secret management or Docker/Kubernetes secrets.


⁠Updating Configuration

Edit:

.env

Then recreate:

docker compose up -d --force-recreate

The container automatically regenerates its rclone configuration on startup.

No manual:

rclone config

is required.


⁠Updating the Docker Image

Change:

BACKUP_IMAGE=yourdockerhubusername/postgres-cloud-backup:1.1.0

Then:

docker compose pull
docker compose up -d

For production systems prefer:

yourdockerhubusername/postgres-cloud-backup:1.0.0

instead of relying only on:

latest

This makes deployments reproducible.


⁠Typical R2-Only Setup

ENABLE_R2_BACKUP=true
ENABLE_GDRIVE_BACKUP=false
ENABLE_SLACK_NOTIFICATION=true

This is a simple recommended configuration for many projects.


⁠Typical R2 + Google Drive Setup

ENABLE_R2_BACKUP=true
ENABLE_GDRIVE_BACKUP=true
ENABLE_SLACK_NOTIFICATION=true

This stores encrypted backups in both cloud providers.


⁠Container Healthcheck

The container includes a basic healthcheck that confirms the internal cron daemon is running.

Check:

docker inspect \
  --format='{{json .State.Health}}' \
  postgres-cloud-backup

⁠Troubleshooting

⁠PostgreSQL is unreachable

Check:

docker network inspect YOUR_NETWORK

Confirm both containers are connected.

Then verify:

DB_HOST=postgres
DB_PORT=5432
⁠R2 upload fails

Check:

R2_ACCESS_KEY_ID
R2_SECRET_ACCESS_KEY
R2_BUCKET
R2_ENDPOINT

Also confirm the token has access to the configured bucket.

⁠Cloudflare root bucket listing returns 403

A restricted R2 token may not have permission to list all account buckets.

This does not necessarily indicate a backup failure.

Use the configured specific bucket/crypt remote instead.

⁠.backup.bin cannot be restored

Expected.

The .bin file is encrypted.

Do not rename it.

Decrypt it through rclone crypt.

⁠Slack notification does not arrive

Check:

ENABLE_SLACK_NOTIFICATION=true

and:

SLACK_WEBHOOK_URL=...

Then inspect:

docker logs postgres-cloud-backup
⁠Cron does not run at expected time

Check:

TZ=Asia/Dhaka
BACKUP_CRON=0 8 * * *

Then recreate the container:

docker compose up -d --force-recreate

After initial setup:

[ ] PostgreSQL connection succeeds
[ ] Manual backup succeeds
[ ] Local .backup exists
[ ] pg_restore validation succeeds
[ ] Cloudflare R2 upload succeeds
[ ] Cloud backup appears encrypted
[ ] Slack success message arrives
[ ] Cron timezone is correct
[ ] Scheduled backup runs successfully
[ ] Restore test succeeds
[ ] Crypt password is stored securely outside the server

⁠Example Architecture

                         Application Project
                                |
                                v
                         Docker Network
                                |
             +------------------+------------------+
             |                                     |
             v                                     v
       PostgreSQL                            Backup Container
                                                  |
                                             pg_dump -Fc
                                                  |
                                                  v
                                             .backup
                                                  |
                   +------------------------------+------------------+
                   |                                                 |
                   v                                                 v
              Local Storage                                  rclone crypt
                                                                |
                                                 +--------------+-------------+
                                                 |                            |
                                                 v                            v
                                           Cloudflare R2                Google Drive
                                             optional                    optional
                                                 |
                                                 v
                                             Slack Alert
                                              optional

⁠Image Philosophy

This image is designed to remain generic.

The Docker image should contain only:

backup logic
backup tools
rclone
scheduler
notification logic

The image should not contain:

database names
database passwords
cloud credentials
bucket names
project names
Slack webhooks
encryption passwords
environment-specific schedules

Those belong in runtime configuration.

This makes the same image reusable for:

DEV
QA
Production
multiple applications
multiple customers
multiple PostgreSQL databases
different Cloudflare R2 buckets
different Google Drive accounts

without rebuilding the Docker image.

Tag summary

Content type

Image

Digest

sha256:212122ce6…

Size

164.9 MB

Last updated

about 1 month ago

docker pull miran2369/postgres-cloud-backup:1.0.0