Files
sure/bin/db-backup.sh
T
TheRodzz 78502c707c feat(hosting): universal headless rclone backup sidecar (#2875)
- Replaces prodrigestivill/postgres-backup-local with an Alpine-based container
- Enables backing up to 70+ cloud providers natively
- Adds environment variable controls for file overwrite vs timestamp
- Resolves security audit flags and rclone backup scoping
- Resolves database credential exposure and addresses CodeRabbit review items
2026-09-02 05:41:35 +02:00

87 lines
2.9 KiB
Bash
Executable File

#!/bin/sh
set -e
# shellcheck disable=SC3040
if (set -o pipefail 2>/dev/null); then
set -o pipefail
fi
# Configuration
BACKUP_OVERWRITE=${BACKUP_OVERWRITE:-false}
BACKUP_KEEP_DAYS=${BACKUP_KEEP_DAYS:-7}
BACKUP_DESTINATION=${BACKUP_DESTINATION:-}
INSTANCE_ID=${INSTANCE_ID:-default}
POSTGRES_PORT=${POSTGRES_PORT:-5432}
if [ -z "$BACKUP_DESTINATION" ]; then
echo "Error: BACKUP_DESTINATION is not set."
exit 1
fi
if [ -z "$POSTGRES_DB" ] || [ -z "$POSTGRES_USER" ] || [ -z "$POSTGRES_PASSWORD" ] || [ -z "$POSTGRES_HOST" ]; then
echo "Error: Database connection variables are not fully set."
exit 1
fi
case "${POSTGRES_HOST}${POSTGRES_PORT}${POSTGRES_DB}${POSTGRES_USER}${POSTGRES_PASSWORD}" in
*"
"*)
echo "Error: Database connection parameters must not contain newlines."
exit 1
;;
esac
if [ "$BACKUP_OVERWRITE" != "true" ] && [ -n "$BACKUP_KEEP_DAYS" ]; then
if ! [ "$BACKUP_KEEP_DAYS" -gt 0 ] 2>/dev/null; then
echo "Error: BACKUP_KEEP_DAYS must be a positive integer (got: ${BACKUP_KEEP_DAYS})."
exit 1
fi
fi
TIMESTAMP=$(date +%Y-%m-%d_%H-%M-%S)
if [ "$BACKUP_OVERWRITE" = "true" ]; then
FILENAME="backup_latest.sql.gz"
else
FILENAME="backup_${TIMESTAMP}_$$.sql.gz"
fi
TMP_DIR=$(umask 077 && mktemp -d)
TEMP_FILE="${TMP_DIR}/${FILENAME}"
PGPASS_FILE="${TMP_DIR}/.pgpass"
# POSIX trap handler to guarantee cleanup of temporary workspace on exit or termination signal
trap 'rm -rf "${TMP_DIR}"' EXIT INT TERM
# Securely generate .pgpass file with strict permissions (0600) and escaped fields
PG_HOST_ESC=$(printf '%s' "${POSTGRES_HOST}" | sed -e 's/\\/\\\\/g' -e 's/:/\\:/g')
PG_PORT_ESC=$(printf '%s' "${POSTGRES_PORT}" | sed -e 's/\\/\\\\/g' -e 's/:/\\:/g')
PG_DB_ESC=$(printf '%s' "${POSTGRES_DB}" | sed -e 's/\\/\\\\/g' -e 's/:/\\:/g')
PG_USER_ESC=$(printf '%s' "${POSTGRES_USER}" | sed -e 's/\\/\\\\/g' -e 's/:/\\:/g')
PG_PASS_ESC=$(printf '%s' "${POSTGRES_PASSWORD}" | sed -e 's/\\/\\\\/g' -e 's/:/\\:/g')
(umask 077 && printf '%s:%s:%s:%s:%s\n' "${PG_HOST_ESC}" "${PG_PORT_ESC}" "${PG_DB_ESC}" "${PG_USER_ESC}" "${PG_PASS_ESC}" > "${PGPASS_FILE}")
chmod 0600 "${PGPASS_FILE}"
export PGPASSFILE="${PGPASS_FILE}"
# Create temp output file with restricted permissions before pg_dump execution
(umask 077 && touch "${TEMP_FILE}")
chmod 0600 "${TEMP_FILE}"
echo "Starting backup for ${POSTGRES_DB} to ${TEMP_FILE}..."
pg_dump -h "${POSTGRES_HOST}" -p "${POSTGRES_PORT}" -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" | gzip > "${TEMP_FILE}"
DEST_PATH="${BACKUP_DESTINATION%/}/${INSTANCE_ID}"
echo "Uploading ${TEMP_FILE} to ${DEST_PATH}/${FILENAME} via rclone..."
rclone copy "${TEMP_FILE}" "${DEST_PATH}"
if [ "$BACKUP_OVERWRITE" != "true" ] && [ -n "$BACKUP_KEEP_DAYS" ]; then
echo "Pruning remote backups older than ${BACKUP_KEEP_DAYS} days in ${DEST_PATH}..."
rclone delete "${DEST_PATH}" --min-age "${BACKUP_KEEP_DAYS}d" --include "backup_*.sql.gz"
fi
echo "Cleaning up..."
rm -rf "${TMP_DIR}"
echo "Backup complete!"