Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ops/archive/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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` |
1 change: 1 addition & 0 deletions ops/backup/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 7 additions & 7 deletions ops/backup/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -450,14 +450,14 @@ docker compose up -d --force-recreate backup

```bash
docker compose run --rm backup restic snapshots
docker logs backup
docker inspect <container> --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
Expand All @@ -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
Expand Down Expand Up @@ -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.

---
Expand Down
4 changes: 4 additions & 0 deletions ops/backup/backup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 3 additions & 21 deletions ops/backup/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions ops/backup/env.sh
Original file line number Diff line number Diff line change
@@ -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}}"

Loading