From 39d1d94bcff9f85849cc36f603233a12b4e8c5d4 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 03:33:49 +0000 Subject: [PATCH 1/2] Backups: name logs by service, not a container that does not exist `docker logs backup` fails: Compose names the container gam-backup-1. `docker compose logs backup` takes the service name and works however Compose names things. Same slip for the archive's backend container. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XWoa1a1upTria7ntq56x3B --- ops/archive/README.md | 4 ++-- ops/backup/README.md | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/ops/archive/README.md b/ops/archive/README.md index 053d8c4..1f15f83 100644 --- a/ops/archive/README.md +++ b/ops/archive/README.md @@ -149,7 +149,7 @@ with no restart needed. `GAM/mirror/MIRROR_INFO` says when the last run was. **After updating production**, update the archive the same way it was installed. Download the new commit's ZIP, extract it, and replace `docker/gam` with it, keeping your `ops/archive/.env`. Then **Container Manager → Project → gam-archive → Action → Stop**, -then **Build**, then **Start**. If the archive's logs (**Container → gam-archive-backend +then **Build**, then **Start**. If the archive's logs (**Container → gam-archive-backend-1 → Log**) say the database "is at migration …, which this checkout does not contain", this is the step you missed. @@ -165,4 +165,4 @@ old environment. | "Could not reach the asset library" | Backend container not running or unhealthy. Look at its log; a wrong `GAM_MIRROR_DIR` shows as `unable to open database file` | | `unable to open database file` | `GAM_MIRROR_DIR` points at the wrong place, or the mirror has not run yet. `ls` the path in File Station: it must hold `db/gam.db` | | Images show, videos never play | Usually a browser blocking plain-HTTP media on a LAN address. Try another browser, or reach it over Tailscale's MagicDNS name | -| Library hours or days old | The mirror has stopped. Check `docker logs backup` on the gam host for `[backup] FAILED` | +| Library hours or days old | The mirror has stopped. Check `docker compose logs backup` on the gam host for `[backup] FAILED` | diff --git a/ops/backup/README.md b/ops/backup/README.md index c4cba80..3434720 100644 --- a/ops/backup/README.md +++ b/ops/backup/README.md @@ -406,12 +406,12 @@ docker compose build backup docker compose run --rm backup restic init docker compose up -d backup docker compose exec -d backup sh -c '/usr/local/bin/backup.sh >>/proc/1/fd/1 2>>/proc/1/fd/2' -docker logs -f backup +docker compose logs -f backup ``` `restic init` is one-time and creates the repository on the NAS. The `exec` line starts the first backup now instead of waiting for the first cron tick, inside the -scheduled container and detached. Ctrl-C on `docker logs -f` stops only the watching, +scheduled container and detached. Ctrl-C on `docker compose logs -f` stops only the watching, not the backup. Inside that container rather than a `docker compose run --rm` one, because the run @@ -450,14 +450,14 @@ docker compose up -d --force-recreate backup ```bash docker compose run --rm backup restic snapshots -docker logs backup -docker inspect --format '{{.HostConfig.RestartPolicy.Name}}' +docker compose logs backup +docker inspect gam-backup-1 --format '{{.HostConfig.RestartPolicy.Name}}' docker compose exec backup crontab -l ``` One snapshot listed means the chain works. Confirm the restart policy is `unless-stopped` or `always`, or the container won't return after a host reboot. -Check `docker logs backup` again after the next scheduled tick — an unattended +Check `docker compose logs backup` again after the next scheduled tick — an unattended run is the only real proof. ## 13. Turn on the plain-file mirror @@ -480,7 +480,7 @@ large one run it detached and follow the log: ```bash docker compose up -d --build --force-recreate backup docker compose exec -d backup sh -c '/usr/local/bin/backup.sh >>/proc/1/fd/1 2>>/proc/1/fd/2' -docker logs -f backup +docker compose logs -f backup ``` Every run from then on sends only what changed: new media, the database, and a small @@ -512,7 +512,7 @@ What to know about the mirror: database are encrypted with production's `JWT_SECRET_KEY`, which never goes to the NAS. - **A brief window:** an asset deleted in the seconds between the database snapshot and the media sync shows as missing in the archive until the next run. -- **A failed mirror fails the run** (`[backup] FAILED` in `docker logs backup`), but only +- **A failed mirror fails the run** (`[backup] FAILED` in `docker compose logs backup`), but only after restic has finished, so a NAS problem with the mirror never costs you a snapshot. --- From 6879bd070dac58f80516d010f976c6bc8833e3b5 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 03:36:10 +0000 Subject: [PATCH 2/2] Backups: let a run started with `docker compose exec` find its repository The README's way to start a first backup by hand, `docker compose exec backup backup.sh`, failed with "Please specify repository location": exec starts a fresh process with only the container's configured environment, so the RESTIC_REPOSITORY and RCLONE_CONFIG_NAS_* exports made in the entrypoint never reached it. Cron runs inherit them and were unaffected. Those exports move to env.sh, sourced by the entrypoint and by backup.sh itself, so every way of starting a run gets them. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XWoa1a1upTria7ntq56x3B --- ops/backup/Dockerfile | 1 + ops/backup/backup.sh | 4 ++++ ops/backup/entrypoint.sh | 24 +++--------------------- ops/backup/env.sh | 30 ++++++++++++++++++++++++++++++ 4 files changed, 38 insertions(+), 21 deletions(-) create mode 100644 ops/backup/env.sh diff --git a/ops/backup/Dockerfile b/ops/backup/Dockerfile index fcb6d4e..a22fe00 100644 --- a/ops/backup/Dockerfile +++ b/ops/backup/Dockerfile @@ -8,6 +8,7 @@ RUN echo "https://dl-cdn.alpinelinux.org/alpine/v3.20/community" >> /etc/apk/rep COPY entrypoint.sh /entrypoint.sh COPY backup.sh /usr/local/bin/backup.sh +COPY env.sh /usr/local/lib/backup-env.sh RUN chmod +x /entrypoint.sh /usr/local/bin/backup.sh WORKDIR /backup diff --git a/ops/backup/backup.sh b/ops/backup/backup.sh index 56a3cce..220c180 100644 --- a/ops/backup/backup.sh +++ b/ops/backup/backup.sh @@ -7,6 +7,10 @@ set -eu trap 'echo "[backup] FAILED (exit $?)" >&2' ERR +# The repository and the mirror's remote, whoever started this run — cron, `docker +# compose run`, or `docker compose exec`, which skips the entrypoint (see env.sh). +. /usr/local/lib/backup-env.sh + mkdir -p /backup/tmp # One run at a time. A first upload of a large library takes most of a day, and the diff --git a/ops/backup/entrypoint.sh b/ops/backup/entrypoint.sh index f872599..18abef0 100644 --- a/ops/backup/entrypoint.sh +++ b/ops/backup/entrypoint.sh @@ -29,27 +29,9 @@ EOF chmod 700 /root/.ssh chmod 600 /root/.ssh/config -# The plain-file mirror's remote, configured through rclone's environment variables so -# there is no config file to mount or keep in step. Same account, key and pinned host key -# as restic — no NAS change is needed to turn the mirror on. -# -# SHELL_TYPE=none and DISABLE_HASHCHECK: DSM gives the backup account SFTP and no shell, -# so rclone cannot run md5sum remotely, and without these it tries and logs a failure -# per file. Size + modtime is a sound comparison here because media names are -# write-once UUIDs: a file with a given name never changes. -# /dev/null: everything is in the environment, and without it rclone logs that it found -# no config file on every single invocation. -export RCLONE_CONFIG=/dev/null -export RCLONE_CONFIG_NAS_TYPE=sftp -export RCLONE_CONFIG_NAS_HOST="${BACKUP_SFTP_HOST}" -export RCLONE_CONFIG_NAS_PORT="${BACKUP_SFTP_PORT}" -export RCLONE_CONFIG_NAS_USER="${BACKUP_SFTP_USER}" -export RCLONE_CONFIG_NAS_KEY_FILE=/root/.ssh/id_backup -export RCLONE_CONFIG_NAS_KNOWN_HOSTS_FILE=/root/.ssh/known_hosts -export RCLONE_CONFIG_NAS_SHELL_TYPE=none -export RCLONE_CONFIG_NAS_DISABLE_HASHCHECK=true - -export RESTIC_REPOSITORY="${RESTIC_REPOSITORY:-sftp:synology-backup:${BACKUP_SFTP_REMOTE_PATH}}" +# The repository and the mirror's rclone remote. In a file of their own because +# backup.sh needs them too, and is not always started from here (see env.sh). +. /usr/local/lib/backup-env.sh SCHEDULE="${BACKUP_CRON_SCHEDULE:-0 * * * *}" echo "${SCHEDULE} /usr/local/bin/backup.sh >>/proc/1/fd/1 2>>/proc/1/fd/2" > /etc/crontabs/root diff --git a/ops/backup/env.sh b/ops/backup/env.sh new file mode 100644 index 0000000..0d5cb1b --- /dev/null +++ b/ops/backup/env.sh @@ -0,0 +1,30 @@ +# Settings derived from the container's environment, for every way backup.sh runs. +# +# Sourced, not executed: by entrypoint.sh at start, and by backup.sh itself. The second +# is the load-bearing one. Exports made in the entrypoint reach the cron jobs crond +# forks and `docker compose run` commands, but not `docker compose exec`, which starts a +# fresh process with only the container's configured environment — so a backup started +# that way had no repository and failed with "Please specify repository location". + +# The plain-file mirror's remote, configured through rclone's environment variables so +# there is no config file to mount or keep in step. Same account, key and pinned host key +# as restic — no NAS change is needed to turn the mirror on. +# +# SHELL_TYPE=none and DISABLE_HASHCHECK: DSM gives the backup account SFTP and no shell, +# so rclone cannot run md5sum remotely, and without these it tries and logs a failure +# per file. Size + modtime is a sound comparison here because media names are +# write-once UUIDs: a file with a given name never changes. +# /dev/null: everything is in the environment, and without it rclone logs that it found +# no config file on every single invocation. +export RCLONE_CONFIG=/dev/null +export RCLONE_CONFIG_NAS_TYPE=sftp +export RCLONE_CONFIG_NAS_HOST="${BACKUP_SFTP_HOST}" +export RCLONE_CONFIG_NAS_PORT="${BACKUP_SFTP_PORT}" +export RCLONE_CONFIG_NAS_USER="${BACKUP_SFTP_USER}" +export RCLONE_CONFIG_NAS_KEY_FILE=/root/.ssh/id_backup +export RCLONE_CONFIG_NAS_KNOWN_HOSTS_FILE=/root/.ssh/known_hosts +export RCLONE_CONFIG_NAS_SHELL_TYPE=none +export RCLONE_CONFIG_NAS_DISABLE_HASHCHECK=true + +export RESTIC_REPOSITORY="${RESTIC_REPOSITORY:-sftp:synology-backup:${BACKUP_SFTP_REMOTE_PATH}}" +