From 2e7e6cca9bdbaf1a802bb7e89b5e3216fd9937cb Mon Sep 17 00:00:00 2001 From: xCyanGrizzly Date: Fri, 24 Jul 2026 11:36:19 +0200 Subject: [PATCH] Backup: switch NAS transport to SMB/CIFS, fix crond/OOM/live-tar defects Wire the backup service against the Synology share over SMB/CIFS (the NAS authenticates with a user/password; NFS is IP-allowlist only). Also fixes three defects found bringing the service up live: - entrypoint crash-loop: dcron's crond fails "setpgid: Operation not permitted" in this runtime -> use busybox crond; make repo-init idempotent (check via `restic cat config`, tolerate init-on-existing) so a transient CIFS/lock hiccup can't kill PID 1. - OOM: pg_dump of a ~276MB DB + tar + restic exceeded the 256M cap -> 1G. - live tar abort: GNU tar exits 1 when TDLib files change mid-read (worker is live); per design this is best-effort, so tolerate exit 1, fatal only >=2. Kuma push is now optional (empty URL disables alerting) since it's deferred. Co-Authored-By: Claude Opus 4.8 (1M context) --- .env.example | 8 +++++--- backup/Dockerfile | 4 +++- backup/backup.sh | 15 +++++++++++---- backup/entrypoint.sh | 9 ++++++--- docker-compose.yml | 10 +++++----- 5 files changed, 30 insertions(+), 16 deletions(-) diff --git a/.env.example b/.env.example index 081d0db..97e509a 100644 --- a/.env.example +++ b/.env.example @@ -37,9 +37,11 @@ WORKER_MAX_ZIP_SIZE_MB=4096 MULTIPART_TIMEOUT_HOURS=0 LOG_LEVEL="info" -# Backup (NAS via NFS + restic) +# Backup (NAS via SMB/CIFS + restic) NAS_HOST="" # Synology NAS IP or hostname reachable from this host -NAS_EXPORT_PATH="" # NFS export path, e.g. /volume1/dragonsstash-backups +NAS_SHARE="" # SMB share name, e.g. dragonsstash_backups +NAS_USERNAME="" # SMB user with read/write on the share +NAS_PASSWORD="" # SMB user password (avoid commas — they delimit cifs mount opts) RESTIC_PASSWORD="" # generate with: openssl rand -base64 32 -KUMA_PUSH_URL="" # Uptime Kuma Push monitor URL (create the monitor first) +KUMA_PUSH_URL="" # optional: Uptime Kuma Push monitor URL; leave empty to disable alerting TZ="Etc/UTC" diff --git a/backup/Dockerfile b/backup/Dockerfile index 8e1d651..104ac6e 100644 --- a/backup/Dockerfile +++ b/backup/Dockerfile @@ -1,6 +1,8 @@ FROM alpine:3.20 -RUN apk add --no-cache restic postgresql16-client curl tzdata dcron tar bash +# Note: use busybox's built-in crond (Alpine base), NOT the dcron package — +# dcron's crond fails with "setpgid: Operation not permitted" in this runtime. +RUN apk add --no-cache restic postgresql16-client curl tzdata tar bash COPY backup/backup.sh /backup.sh COPY backup/entrypoint.sh /entrypoint.sh diff --git a/backup/backup.sh b/backup/backup.sh index 2e596bd..cc415f7 100644 --- a/backup/backup.sh +++ b/backup/backup.sh @@ -2,6 +2,7 @@ set -euo pipefail report_failure() { + [ -n "${KUMA_PUSH_URL:-}" ] || return 0 curl -fsS "$KUMA_PUSH_URL" --get \ --data-urlencode "status=down" \ --data-urlencode "msg=$BASH_COMMAND failed" || true @@ -15,11 +16,17 @@ trap 'rm -f "$DUMP_FILE" "$TAR_FILE"' EXIT pg_dump -h dragonsstash-db -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc -f "$DUMP_FILE" -tar czf "$TAR_FILE" -C /data tdlib-worker tdlib-bot +# TDLib volumes are tarred live (best-effort, per design). A file changing +# mid-read makes GNU tar exit 1 (warning) — that is expected here and must not +# abort the backup. Only a genuine error (exit >= 2) is fatal. +tar --warning=no-file-changed -czf "$TAR_FILE" -C /data tdlib-worker tdlib-bot \ + || { rc=$?; [ "$rc" -le 1 ] || exit "$rc"; } restic backup "$DUMP_FILE" "$TAR_FILE" restic forget --keep-daily 14 --prune -curl -fsS "$KUMA_PUSH_URL" --get \ - --data-urlencode "status=up" \ - --data-urlencode "msg=OK" +if [ -n "${KUMA_PUSH_URL:-}" ]; then + curl -fsS "$KUMA_PUSH_URL" --get \ + --data-urlencode "status=up" \ + --data-urlencode "msg=OK" +fi diff --git a/backup/entrypoint.sh b/backup/entrypoint.sh index b063181..238abae 100644 --- a/backup/entrypoint.sh +++ b/backup/entrypoint.sh @@ -1,8 +1,11 @@ #!/bin/bash -set -euo pipefail +set -uo pipefail -if ! restic snapshots >/dev/null 2>&1; then - restic init +# Ensure the repo exists, but never crash-loop on it: a transient error reading +# the repo (CIFS hiccup, stale lock) must not kill PID 1. `restic init` failing +# because the repo already exists is expected and harmless here. +if ! restic cat config >/dev/null 2>&1; then + restic init || echo "restic init skipped (repo already exists or temporarily unreachable)" fi exec crond -f -l 2 diff --git a/docker-compose.yml b/docker-compose.yml index 44d47c3..cc447eb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -109,7 +109,7 @@ services: - POSTGRES_DB=${POSTGRES_DB:-dragonsstash} - RESTIC_REPOSITORY=/backups/restic-repo - RESTIC_PASSWORD=${RESTIC_PASSWORD:?Set RESTIC_PASSWORD in .env} - - KUMA_PUSH_URL=${KUMA_PUSH_URL:?Set KUMA_PUSH_URL in .env} + - KUMA_PUSH_URL=${KUMA_PUSH_URL:-} - TZ=${TZ:-Etc/UTC} volumes: - tdlib_state:/data/tdlib-worker:ro @@ -122,7 +122,7 @@ services: deploy: resources: limits: - memory: 256M + memory: 1G networks: - backend @@ -158,9 +158,9 @@ volumes: manual_uploads: nas_backups: driver_opts: - type: nfs - o: "addr=${NAS_HOST},rw,nfsvers=4,soft,timeo=100" - device: ":${NAS_EXPORT_PATH}" + type: cifs + o: "username=${NAS_USERNAME},password=${NAS_PASSWORD},vers=3.0,uid=0,gid=0,file_mode=0660,dir_mode=0770" + device: "//${NAS_HOST}/${NAS_SHARE}" networks: frontend: