Refine Synology backup workflow
Quality Gate / gate (push) Failing after 47s

Co-authored-by: Copilot App <[email protected]>
This commit is contained in:
Jim Lancaster
2026-08-26 17:32:20 -05:00
co-authored by Copilot App
parent ca29bc8b74
commit 34c7b16675
4 changed files with 111 additions and 32 deletions
+60 -18
View File
@@ -9,51 +9,93 @@ SYNOLOGY_BACKUP_DIR="${SYNOLOGY_BACKUP_DIR:-}"
timestamp="$(date -u +%Y%m%d-%H%M%S)"
postgres_file="postgres-${timestamp}.dump"
uploads_file="uploads-${timestamp}.tar.gz"
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
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}"
docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" run --rm --no-deps --entrypoint sh app -lc \
"tar -C /app/uploads -czf - ." \
> "${BACKUP_DIR}/${uploads_file}"
tar -C . -czf "${BACKUP_DIR}/${config_file}" \
"${ENV_FILE}" \
"${COMPOSE_FILE}" \
deploy/cloudflared/config.yml
cat > "${BACKUP_DIR}/${manifest_file}" <<EOF
created_at_utc=${timestamp}
postgres_dump=${postgres_file}
uploads_archive=${uploads_file}
config_archive=${config_file}
backup_dir=${BACKUP_DIR}
synology_backup_dir=${SYNOLOGY_BACKUP_DIR}
media_sync_mode=synology-incremental-new-files-only
compose_file=${COMPOSE_FILE}
env_file=${ENV_FILE}
EOF
find "${BACKUP_DIR}" -type f \( \
-name 'postgres-*.dump' -o \
-name 'uploads-*.tar.gz' -o \
-name 'config-*.tar.gz' -o \
-name 'backup-*.manifest' \
\) -mtime +"${RETENTION_DAYS}" -delete
if [ -n "${SYNOLOGY_BACKUP_DIR}" ]; then
mkdir -p "${SYNOLOGY_BACKUP_DIR}"
# 1) Always copy the PostgreSQL dump to Synology.
cp "${BACKUP_DIR}/${postgres_file}" "${SYNOLOGY_BACKUP_DIR}/${postgres_file}"
cp "${BACKUP_DIR}/${uploads_file}" "${SYNOLOGY_BACKUP_DIR}/${uploads_file}"
cp "${BACKUP_DIR}/${config_file}" "${SYNOLOGY_BACKUP_DIR}/${config_file}"
# 2) Snapshot deployment config and logs to Synology each run.
set -- "${ENV_FILE}" "${COMPOSE_FILE}"
if [ -f deploy/cloudflared/config.yml ]; then
set -- "$@" deploy/cloudflared/config.yml
fi
tar -C . -czf "${SYNOLOGY_BACKUP_DIR}/${config_file}" "$@"
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"
# 3) Sync uploads incrementally to Synology (copy only new files).
docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" run --rm --no-deps \
-v "${SYNOLOGY_BACKUP_DIR}:/backup" \
--entrypoint sh app -lc '
mkdir -p /backup/uploads/documents /backup/uploads/photos;
if [ -f /app/uploads/homepage.md ] && [ ! -f /backup/uploads/homepage.md ]; then
cp /app/uploads/homepage.md /backup/uploads/homepage.md;
fi;
if [ -d /app/uploads/documents ]; then
cp -an /app/uploads/documents/. /backup/uploads/documents/;
fi;
if [ -d /app/uploads/photos ]; then
cp -an /app/uploads/photos/. /backup/uploads/photos/;
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}/${uploads_file}"
echo " ${BACKUP_DIR}/${config_file}"
echo " ${BACKUP_DIR}/${manifest_file}"
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/{homepage.md,documents/,photos/} (incremental)"
echo " ${SYNOLOGY_BACKUP_DIR}/${manifest_file}"
else
echo "SYNOLOGY_BACKUP_DIR not set; skipped Synology copy."
fi
+32 -4
View File
@@ -9,6 +9,7 @@ fi
dump_file="$1"
COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.production.yml}"
ENV_FILE="${ENV_FILE:-.env.production}"
SYNOLOGY_BACKUP_DIR="${SYNOLOGY_BACKUP_DIR:-}"
if [ ! -f "${dump_file}" ]; then
echo "Backup file not found: ${dump_file}"
@@ -20,7 +21,16 @@ backup_name="$(basename "${dump_file}")"
timestamp="$(printf '%s' "${backup_name}" | sed -n 's/^postgres-\([0-9]\{8\}-[0-9]\{6\}\)\.dump$/\1/p')"
uploads_file=""
config_file=""
# 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
if [ -n "${timestamp}" ]; then
# Legacy local full-archive naming.
candidate_uploads="${backup_dir}/uploads-${timestamp}.tar.gz"
candidate_config="${backup_dir}/config-${timestamp}.tar.gz"
if [ -f "${candidate_uploads}" ]; then
@@ -40,19 +50,37 @@ docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" exec -T postgres sh
cat "${dump_file}" | docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" exec -T postgres sh -lc \
"PGPASSWORD=\"\$POSTGRES_PASSWORD\" pg_restore -U \"\$POSTGRES_USER\" -d \"\$POSTGRES_DB\" --clean --if-exists --no-owner --no-privileges"
if [ -n "${uploads_file}" ]; then
if [ -n "${SYNOLOGY_BACKUP_DIR}" ] && [ -d "${SYNOLOGY_BACKUP_DIR}/uploads" ]; then
docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" run --rm --no-deps \
-v "${SYNOLOGY_BACKUP_DIR}:/backup" \
--entrypoint sh app -lc \
"mkdir -p /app/uploads/documents /app/uploads/photos && \
find /app/uploads/documents -mindepth 1 -delete && \
find /app/uploads/photos -mindepth 1 -delete && \
if [ -d /backup/uploads/documents ]; then cp -a /backup/uploads/documents/. /app/uploads/documents/; fi && \
if [ -d /backup/uploads/photos ]; then cp -a /backup/uploads/photos/. /app/uploads/photos/; fi && \
if [ -f /backup/uploads/homepage.md ]; then cp /backup/uploads/homepage.md /app/uploads/homepage.md; else rm -f /app/uploads/homepage.md; fi"
elif [ -n "${uploads_file}" ]; then
# Legacy local full-archive restore.
cat "${uploads_file}" | docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" run --rm --no-deps --entrypoint sh app -lc \
"mkdir -p /app/uploads && find /app/uploads -mindepth 1 -delete && tar -xzf - -C /app/uploads"
fi
if [ -n "${config_file}" ]; then
if [ -n "${SYNOLOGY_BACKUP_DIR}" ] && [ -n "${timestamp}" ] && [ -f "${SYNOLOGY_BACKUP_DIR}/config-${timestamp}.tar.gz" ]; then
tar -xzf "${SYNOLOGY_BACKUP_DIR}/config-${timestamp}.tar.gz" -C .
elif [ -n "${config_file}" ]; then
# Legacy local full-archive restore.
tar -xzf "${config_file}" -C .
fi
echo "Restore complete from: ${dump_file}"
if [ -n "${uploads_file}" ]; then
if [ -n "${SYNOLOGY_BACKUP_DIR}" ] && [ -d "${SYNOLOGY_BACKUP_DIR}/uploads" ]; then
echo "Restored uploads mirror from: ${SYNOLOGY_BACKUP_DIR}/uploads"
elif [ -n "${uploads_file}" ]; then
echo "Restored uploads archive: ${uploads_file}"
fi
if [ -n "${config_file}" ]; then
if [ -n "${SYNOLOGY_BACKUP_DIR}" ] && [ -n "${timestamp}" ] && [ -f "${SYNOLOGY_BACKUP_DIR}/config-${timestamp}.tar.gz" ]; then
echo "Restored config archive: ${SYNOLOGY_BACKUP_DIR}/config-${timestamp}.tar.gz"
elif [ -n "${config_file}" ]; then
echo "Restored config archive: ${config_file}"
fi