Skip to content

Latest commit

 

History

History
161 lines (124 loc) · 4.77 KB

File metadata and controls

161 lines (124 loc) · 4.77 KB

Scheduling render-backup.sh

Pick one. All three run the same script; they differ in what happens when it fails and who finds out.

Everything below assumes the repository is checked out at /opt/render-backup and that credentials live in /opt/render-backup/.env, which is chmod 600 and not committed.

.env:

DATABASE_URL='postgresql://user:password@host.oregon-postgres.render.com/dbname'
BACKUP_DIR='/var/backups/render'
KEEP=14
DB_CREATED_AT='2026-08-01'
ALERT_WEBHOOK='https://hooks.example.com/your-endpoint'

DB_CREATED_AT is the date the Render instance was created. Setting it turns on the countdown: the script warns at 7 days, escalates at 3, and exits 3 once either threshold is crossed so your scheduler can treat it as an alert rather than a success.


cron

# m  h  dom mon dow  command
  17 3   *   *   *   set -a; . /opt/render-backup/.env; set +a; /opt/render-backup/examples/render-backup.sh >> /var/log/render-backup.log 2>&1

# during the last week before expiry, twice a day
  17 3,15 *  *   *   set -a; . /opt/render-backup/.env; set +a; /opt/render-backup/examples/render-backup.sh >> /var/log/render-backup.log 2>&1

Use one or the other, not both. 17 rather than 0 because everybody's cron fires on the hour and your backup does not need to queue behind the rest of the internet.

cron mails output to the crontab owner if MAILTO is set and a mail transfer agent exists. On most containers neither is true, which is why the script also supports ALERT_WEBHOOK.


systemd timer

More setup, better failure reporting: systemctl list-timers shows you the next run, and a failed unit is visible to anything watching systemd state.

/etc/systemd/system/render-backup.service:

[Unit]
Description=Logical backup of the Render Postgres instance
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
User=backup
EnvironmentFile=/opt/render-backup/.env
ExecStart=/opt/render-backup/examples/render-backup.sh
# exit 3 means "backup succeeded, expiry is close" — surface it, do not hide it
SuccessExitStatus=0
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/var/backups/render

/etc/systemd/system/render-backup.timer:

[Unit]
Description=Daily Render Postgres backup

[Timer]
OnCalendar=*-*-* 03:17:00
RandomizedDelaySec=600
Persistent=true

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now render-backup.timer
sudo systemctl start render-backup.service   # run once now
journalctl -u render-backup.service -n 50

Persistent=true matters: if the machine was asleep at 03:17 the run happens at next boot instead of being skipped.


GitHub Actions

For when there is no always-on machine. The runner is ephemeral, so the dump has to go somewhere — object storage, or an artifact if it is small and you accept the retention window.

name: render-backup
on:
  schedule:
    - cron: '17 3 * * *'
  workflow_dispatch:

jobs:
  dump:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install a matching client
        run: |
          sudo sh -c 'echo "deb https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
          curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/pgdg.gpg
          sudo apt-get update && sudo apt-get install -y postgresql-client-16

      - name: Back up
        env:
          DATABASE_URL: ${{ secrets.RENDER_DATABASE_URL }}
          DB_CREATED_AT: ${{ vars.RENDER_DB_CREATED_AT }}
          BACKUP_DIR: ${{ runner.temp }}/backups
          KEEP: 1
        run: ./examples/render-backup.sh

      - uses: actions/upload-artifact@v4
        with:
          name: render-dump-${{ github.run_number }}
          path: ${{ runner.temp }}/backups
          retention-days: 30

Two things to be honest about. Scheduled workflows on GitHub are best-effort and can be delayed under load, so this is not a precise clock. And GitHub disables scheduled workflows in repositories with no activity for a period — if this is your only backup and the repository is otherwise idle, it will eventually stop running and not tell you. Use workflow_dispatch occasionally, or use one of the other two options.


Whichever you pick, test the restore

A backup you have never restored is a hypothesis.

docker run -d --name pgtest -e POSTGRES_PASSWORD=test -p 5433:5432 postgres:16
export TARGET_DSN='postgresql://postgres:test@localhost:5433/postgres'
export DUMP_PATH=/var/backups/render/20260818-031700.dump
./restore-into.sh
docker rm -f pgtest

Put a reminder in your calendar to do this monthly. It takes four minutes and it is the only thing that turns a directory of files into a backup.