#!/usr/bin/env sh set -eu COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.production.yml}" ENV_FILE="${ENV_FILE:-.env.production}" BACKUP_DIR="${BACKUP_DIR:-${DATABASE_BACKUP_DIR:-./data/backups}}" APP_DATA_BACKUP_DIR="${APP_DATA_BACKUP_DIR:-${BACKUP_DIR}/data}" UPLOADS_BACKUP_DIR="${UPLOADS_BACKUP_DIR:-${BACKUP_DIR}/uploads}" RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-14}" SYNOLOGY_BACKUP_DIR="${SYNOLOGY_BACKUP_DIR:-}" timestamp="$(date -u +%Y%m%d-%H%M%S)" postgres_file="postgres-${timestamp}.dump" config_file="config-${timestamp}.tar.gz" logs_file="logs-${timestamp}.tar.gz" manifest_file="backup-${timestamp}.manifest" mkdir -p "${BACKUP_DIR}" # If SYNOLOGY_BACKUP_DIR wasn't exported in the shell, read it from ENV_FILE. if [ -z "${SYNOLOGY_BACKUP_DIR}" ] && [ -f "${ENV_FILE}" ]; then SYNOLOGY_BACKUP_DIR="$( sed -n 's/^SYNOLOGY_BACKUP_DIR=//p' "${ENV_FILE}" | tail -n 1 )" fi use_compose_backup=0 if command -v docker >/dev/null 2>&1 && [ -S /var/run/docker.sock ] && [ -f "${COMPOSE_FILE}" ]; then use_compose_backup=1 fi if [ "${use_compose_backup}" -eq 1 ]; then docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" exec -T postgres sh -lc \ "PGPASSWORD=\"\$POSTGRES_PASSWORD\" pg_dump -U \"\$POSTGRES_USER\" -d \"\$POSTGRES_DB\" -Fc" \ > "${BACKUP_DIR}/${postgres_file}" else : "${POSTGRES_HOST:=postgres}" : "${POSTGRES_PORT:=5432}" if [ -z "${POSTGRES_DB:-}" ] || [ -z "${POSTGRES_USER:-}" ] || [ -z "${POSTGRES_PASSWORD:-}" ]; then echo "POSTGRES_DB, POSTGRES_USER, and POSTGRES_PASSWORD must be set for in-container backup mode." >&2 exit 1 fi if ! command -v pg_dump >/dev/null 2>&1; then echo "pg_dump is not installed in this environment." >&2 exit 1 fi PGPASSWORD="${POSTGRES_PASSWORD}" pg_dump \ -h "${POSTGRES_HOST}" \ -p "${POSTGRES_PORT}" \ -U "${POSTGRES_USER}" \ -d "${POSTGRES_DB}" \ -Fc \ > "${BACKUP_DIR}/${postgres_file}" fi cat > "${BACKUP_DIR}/${manifest_file}" < "${SYNOLOGY_BACKUP_DIR}/${config_file}" fi if [ "${use_compose_backup}" -eq 1 ]; then docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" run --rm --no-deps \ -v "${SYNOLOGY_BACKUP_DIR}:/backup" \ --entrypoint sh app -lc \ "if [ -d /app/data/logs ]; then tar -C /app/data/logs -czf /backup/${logs_file} .; else : > /backup/${logs_file}; fi" else if [ -d /app/data/logs ]; then tar -C /app/data/logs -czf "${SYNOLOGY_BACKUP_DIR}/${logs_file}" . else : > "${SYNOLOGY_BACKUP_DIR}/${logs_file}" fi mkdir -p "${SYNOLOGY_BACKUP_DIR}/uploads" cp -an "${UPLOADS_BACKUP_DIR}/." "${SYNOLOGY_BACKUP_DIR}/uploads/" fi cp "${BACKUP_DIR}/${manifest_file}" "${SYNOLOGY_BACKUP_DIR}/${manifest_file}" # Keep rolling dump/config/log/manifest snapshots on Synology. find "${SYNOLOGY_BACKUP_DIR}" -maxdepth 1 -type f \( \ -name 'postgres-*.dump' -o \ -name 'config-*.tar.gz' -o \ -name 'logs-*.tar.gz' -o \ -name 'backup-*.manifest' \ \) -mtime +"${RETENTION_DAYS}" -delete fi echo "Created backup set:" echo " ${BACKUP_DIR}/${postgres_file}" echo " ${BACKUP_DIR}/${manifest_file}" echo " ${APP_DATA_BACKUP_DIR}/ (incremental app data mirror)" echo " ${UPLOADS_BACKUP_DIR}/ (incremental uploads mirror)" if [ -n "${SYNOLOGY_BACKUP_DIR}" ]; then echo "Synology artifacts:" echo " ${SYNOLOGY_BACKUP_DIR}/${postgres_file}" echo " ${SYNOLOGY_BACKUP_DIR}/${config_file}" echo " ${SYNOLOGY_BACKUP_DIR}/${logs_file}" echo " ${SYNOLOGY_BACKUP_DIR}/uploads/ (copied from local incremental mirror)" echo " ${SYNOLOGY_BACKUP_DIR}/${manifest_file}" else echo "SYNOLOGY_BACKUP_DIR not set; skipped Synology copy." fi