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/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/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. --- 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}}" +