Skip to content

Fix SIGTERM forwarding, harden image, restructure repo, add e2e CI tests - #227

Merged
modem7 merged 1 commit into
masterfrom
fix/signal-handling-and-docker-improvements
Jun 27, 2026
Merged

modem7 merged 1 commit into
masterfrom
fix/signal-handling-and-docker-improvements

Conversation

@modem7

@modem7 modem7 commented Jun 27, 2026 •

Copy link
Copy Markdown
Owner

Summary

Signal handling

  • Adds `borgmatic-start` wrapper at `/usr/local/bin/borgmatic-start` that traps `TERM`/`INT`/`HUP`, forwards the signal to borgmatic by PID, and waits for clean exit — prevents `docker stop` from killing borgmatic mid-backup and leaving a stale repo lock. Closes Update python Docker tag to v3.11.4 #104.
  • Adds `stop_grace_period: 10m` to `docker-compose.yml` so Docker waits for borgmatic to finish.
  • Warns at startup if the active crontab calls `borgmatic` directly instead of `borgmatic-start`.

Script hardening

  • Replaces `eval`+`set | grep` secret expansion with safe `compgen -e` + bash indirect expansion (`${!var}`).
  • Fixes `EXTRA_CRON` missing leading newline before append.
  • Fixes post-loop debug labels printing "Before" instead of "After".
  • `EXTRA_PKGS` split via `read -ra` instead of fragile unquoted word splitting.

Upstream sync (borgmatic-collective/docker-borgmatic)

  • Adds `init-envfile` S6 oneshot service: processes `FILE__VARNAME` → writes secret file contents to container environment (LinuxServer.io convention).
  • Alpine 3.23 → 3.24.
  • `S6_CMD_WAIT_FOR_SERVICES_MAXTIME` reverted to `0` (upstream default: wait forever).

Repo restructure

  • Moves everything from `base-fullbuild/` to repo root (matching upstream layout), preserving git history.
  • Removes redundant `sync-drone-tags.yml` workflow (Drone now generates tags dynamically from `requirements.txt`).
  • Removes Drone lint step (Hadolint + ShellCheck already run in GitHub Actions CI).
  • Drone tag generation: replaces hardcoded version tags with a Prepare step that writes `.tags` from `requirements.txt`.

Renovate

  • Extracts `ALPINE_VERSION` and `PYTHON_VERSION` as `ARG`s in the Dockerfile so Renovate can track each independently via `regexManagers`.

Config examples & documentation

  • All example configs updated from borgmatic 1.x format to 2.x flat format.
  • Hook examples updated from deprecated `before_everything`/`after_everything`/`on_error` to `commands:` syntax.
  • `docker-compose.restore.yml` corrected: paths, `BORG_PASSPHRASE`, shell.
  • `.env.template` updated: `BORG_RESTORE` replaces `VOLUME_RESTORE`, adds `VOLUME_BORGMATIC_STATE`.
  • README rewritten: Configuration, Hook scripts, Healthchecks.io, signal handling, secret files, FUSE restore walkthrough.
  • Adds `.gitignore` with `.claude`.

Example borgscripts

Three ready-to-use hook scripts in `data/borgscripts/` (all require `DOCKERCLI=true` + `docker.sock`):

  • `docker-stop.sh`: stops containers carrying a configurable label (`backup` by default) before borgmatic runs. Label-based selection avoids hardcoding names; `--no-run-if-empty` guards against empty matches. Configurable label and stop timeout.
  • `docker-start.sh`: starts a single compose stack after borgmatic using `docker compose start` (not `up -d`) to avoid unintended recreates. Configurable compose project directory.
  • `redis-backup.sh`: triggers `BGSAVE` on a Redis container and polls `rdb_bgsave_in_progress` until the snapshot is written, so `dump.rdb` is consistent when borgmatic backs it up as a regular file. Configurable container name and timeout.
  • README documents all three scripts with configuration variables, label setup, borgmatic hook wiring, and Docker requirements. New "Native database backup" section covers borgmatic's built-in PostgreSQL, MariaDB, MongoDB, and SQLite support.

CI — end-to-end tests

Four new tests that run borgmatic against a real Borg repository (no S6):

  • Full backup/restore cycle: `repo-create` → `create` → `list` (assert archive count ≥ 1) → `borg extract` → verify file content on runner.
  • `${VAR}` expansion in config: `source_directories` and `encryption_passphrase` read from container env vars.
  • `encryption_passcommand`: passphrase read from a mounted secrets file via `cat /run/secrets/passphrase`.
  • Invalid config rejection: `borgmatic config validate` must exit non-zero for a config with wrong type.

What reviewers should know

  • Users with a custom `CRON_COMMAND` or `crontab.txt` calling `borgmatic` directly will see an advisory warning at startup — nothing breaks, but they should switch to `borgmatic-start` for signal forwarding.
  • `borgmatic-start` passes all arguments through (`"$@"`), transparent drop-in replacement.
  • The `FILE__VARNAME` convention from `init-envfile` is distinct from the existing `BORG_FILE` / `YOUR_FILE` expansion — both coexist.
  • `docker-stop.sh` stops by label only — it does not stop the whole compose stack. `docker-start.sh` starts the whole stack. Users who want to stop the entire stack should wire `docker compose --project-directory ... stop` directly.
  • `redis-backup.sh` covers the default RDB persistence mode. If Redis uses AOF (`appendonly yes`), stop the container instead (add the `backup` label to the Redis service).

Test plan

  • Container starts cleanly with no `CRON` set (default schedule used)
  • Container starts cleanly with `CRON` env var set
  • `BORG_PASSPHRASE_FILE` / `YOUR_*_FILE` secret expansion works correctly
  • `EXTRA_PKGS` with multiple space-separated packages installs all of them
  • `DEBUG_SECRETS=true` shows correct Before/After labels
  • Warning appears when crontab contains `borgmatic` directly
  • No warning appears when crontab uses `borgmatic-start`
  • `borgmatic config validate` passes on the shipped `config.yaml`
  • `borgmatic config validate` fails on invalid config (CI: automated)
  • Full backup cycle against a real Borg repo: repo-create, create, list (assert archive count), borg list (assert expected files present in archive) (CI: automated)
  • `${VAR}` expansion in borgmatic config works end-to-end (CI: automated)
  • `encryption_passcommand` via mounted secrets file works (CI: automated)
  • `docker stop` during a running backup sends SIGTERM to borgmatic (check logs for "Caught signal")
  • `FILE__VARNAME` → env var expansion works at container startup
  • Renovate correctly tracks Alpine and Python version bumps separately
  • `docker-stop.sh` stops only labelled containers, leaves unlabelled ones running
  • `docker-start.sh` restores stopped containers without recreating them
  • `redis-backup.sh` completes before borgmatic starts (check log ordering)

@modem7 modem7 self-assigned this Jun 27, 2026
@gitguardian

gitguardian Bot commented Jun 27, 2026 •

Copy link
Copy Markdown

️✅ There are no secrets present in this pull request anymore.

If these secrets were true positive and are still valid, we highly recommend you to revoke them.
While these secrets were previously flagged, we no longer have a reference to the
specific commits where they were detected. Once a secret has been leaked into a git
repository, you should consider it compromised, even if it was deleted immediately.
Find here more information about risks.


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@modem7

modem7 commented Jun 27, 2026 •

Copy link
Copy Markdown
Owner Author

These were dummy CI test passphrases with no real credentials behind them. Extracted all three to job-level env: vars (BORG_CI_PASSPHRASE, BORG_CI_PASSPHRASE_ENVVAR, BORG_CI_PASSPHRASE_CMD) in df74c34 so no literal strings appear inline.

@modem7 modem7 changed the title Fix SIGTERM forwarding to borgmatic and harden Docker image Fix SIGTERM forwarding, harden image, restructure repo, add e2e CI tests Jun 27, 2026
Signal handling
- Add borgmatic-start wrapper (/usr/local/bin/borgmatic-start) that traps
  TERM/INT/HUP, forwards the signal to borgmatic by PID, and waits for
  clean exit. Prevents docker stop from killing borgmatic mid-backup and
  leaving a stale repository lock. Closes #104.
- Add stop_grace_period: 10m to docker-compose.yml.
- Warn at startup if the active crontab calls borgmatic directly instead
  of borgmatic-start.

Script hardening
- Replace eval+set|grep secret expansion with safe compgen -e and bash
  indirect expansion (${!var}).
- Fix EXTRA_CRON missing leading newline before append.
- Fix post-loop debug labels printing "Before" instead of "After".
- Split EXTRA_PKGS via read -ra instead of fragile unquoted word splitting.

Upstream sync (borgmatic-collective/docker-borgmatic)
- Add init-envfile S6 oneshot: processes FILE__VARNAME env vars, writes
  secret file contents to container environment (LinuxServer.io convention).
- Alpine 3.23 → 3.24.
- S6_CMD_WAIT_FOR_SERVICES_MAXTIME reverted to 0 (upstream default).

Repo restructure
- Move all files from base-fullbuild/ to repo root, preserving git history.
- Remove sync-drone-tags.yml workflow (Drone now generates tags dynamically
  from requirements.txt via a Prepare step).
- Remove Drone lint step (Hadolint + ShellCheck run in GitHub Actions CI).

Renovate
- Extract ALPINE_VERSION and PYTHON_VERSION as ARGs in Dockerfile so
  Renovate can track each independently via regexManagers.

Config examples and documentation
- Update all example configs from borgmatic 1.x to 2.x flat format.
- Update hook examples from deprecated before_everything/after_everything/
  on_error to commands: syntax.
- Fix docker-compose.restore.yml: paths, BORG_PASSPHRASE, shell.
- Rename VOLUME_RESTORE to BORG_RESTORE in restore compose and .env.template.
- Add VOLUME_BORGMATIC_STATE to .env.template.
- README: Configuration section (example files, env var expansion,
  Healthchecks.io), Hook scripts, signal handling, secret files, FUSE
  restore walkthrough, log rotation.
- Add .gitignore excluding .claude.

Example borgscripts (data/borgscripts/)
- docker-stop.sh: stop containers carrying a configurable label before
  backup. Uses --no-run-if-empty to guard against empty matches.
- docker-start.sh: start a single compose stack with docker compose start
  (not up -d) to avoid unintended recreates.
- redis-backup.sh: trigger BGSAVE and poll rdb_bgsave_in_progress until
  the snapshot is complete before borgmatic runs.
- README documents all three scripts with setup, label config, borgmatic
  hook wiring, and Docker requirements. New native database backup section
  covers borgmatic's built-in PostgreSQL, MariaDB, MongoDB, SQLite support.

CI — end-to-end and behavioural tests
- Full backup cycle: repo-create, create, list (assert archive count),
  borg list (assert expected filenames present in archive).
- ${VAR} expansion in borgmatic config: source_directories and
  encryption_passphrase read from container env vars.
- encryption_passcommand: passphrase read from a mounted secrets file.
- Invalid config rejection: borgmatic config validate exits non-zero for
  keep_daily: "not-a-number".
- SIGTERM forwarding: mock borgmatic traps TERM, borgmatic-start forwards
  signal, assert "Caught signal" or BORGMATIC_SIGTERM_RECEIVED in logs.
- FILE__VARNAME expansion: container started with FILE__MY_SECRET, assert
  MY_SECRET set in /run/s6/container_environment/ and FILE__ var removed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@modem7
modem7 force-pushed the fix/signal-handling-and-docker-improvements branch from 69c07af to 5b49a5f Compare June 27, 2026 03:37
@modem7
modem7 merged commit 9093771 into master Jun 27, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant