Add entrypoint fallback for missing bind-mounted scripts #2

Merged
m.mabrey merged 2 commits from scripts/entrypoint into main 2026-08-05 01:56:50 +00:00
4 changed files with 82 additions and 7 deletions

View File

@@ -82,7 +82,6 @@ services:
volumes:
- ./scripts:/scripts:ro
- ./backups:/backups
entrypoint: ["/bin/sh", "/scripts/backup-cron.sh"]
restart: unless-stopped
depends_on:
db:

View File

@@ -1,9 +1,27 @@
#!/bin/sh
# Entry point for the `backup` sidecar container. Runs db-backup.sh on a fixed
# interval (default: daily). Kept deliberately simple — a sleep loop instead of a
# cron daemon — so it works in a bare postgres:16-alpine image.
# Entry point for the `backup` sidecar container's periodic loop. Runs
# db-backup.sh on a fixed interval (default: daily) -- a sleep loop instead of
# a cron daemon, kept deliberately simple so it works in a bare
# postgres:16-alpine image.
#
# Resolves db-backup.sh the same way entrypoint.sh resolves this file: prefer
# the live bind-mounted copy at /scripts (so edits don't need a rebuild), fall
# back to the copy baked into the image at build time if the mount is
# missing, empty, or stale. Resolving fresh on every loop iteration also means
# that if the mount comes back healthy later (e.g. someone fixes the host
# directory) this container picks it up on the very next run, with no
# restart needed.
set -eu
resolve() {
# $1 = script filename, e.g. db-backup.sh
if [ -f "/scripts/$1" ]; then
echo "/scripts/$1"
else
echo "/app/scripts-default/$1"
fi
}
INTERVAL="${BACKUP_INTERVAL_SECONDS:-86400}" # 86400 = once a day
echo "[backup] sidecar started; interval=${INTERVAL}s, keep=${BACKUP_KEEP:-14}, dir=${BACKUP_DIR:-/backups}"
@@ -11,6 +29,7 @@ echo "[backup] sidecar started; interval=${INTERVAL}s, keep=${BACKUP_KEEP:-14},
# immediate restore point instead of waiting a whole interval.
sleep 20
while true; do
sh /scripts/db-backup.sh || echo "[backup] run failed; will retry next interval" >&2
DB_BACKUP="$(resolve db-backup.sh)"
sh "$DB_BACKUP" || echo "[backup] run failed; will retry next interval" >&2
sleep "$INTERVAL"
done

View File

@@ -1,5 +1,19 @@
# Backup sidecar image: Postgres client tools (pg_dump/psql) + openssl for
# at-rest encryption of dumps. The scripts themselves are bind-mounted at runtime
# (see the `backup` service in docker-compose.yml), so they're not COPYed here.
# at-rest encryption of dumps.
#
# The scripts are bind-mounted live at runtime (see the `backup` service in
# docker-compose.yml) so they can be edited without a rebuild -- but they're
# ALSO baked in here as a fallback default under /app/scripts-default/.
# entrypoint.sh prefers the live mount and only falls back to this baked-in
# copy if the mount is missing, empty, or stale. That fallback is what keeps
# a broken bind mount from crash-looping the container into an unreachable
# state (see entrypoint.sh for the full story).
FROM postgres:16-alpine
RUN apk add --no-cache openssl
COPY scripts/backup-cron.sh scripts/db-backup.sh scripts/db-restore.sh /app/scripts-default/
COPY scripts/entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh /app/scripts-default/*.sh
ENTRYPOINT ["/app/entrypoint.sh"]
CMD []

43
scripts/entrypoint.sh Normal file
View File

@@ -0,0 +1,43 @@
#!/bin/sh
# Entrypoint for the `backup` sidecar. Prefers the live, bind-mounted copy of
# backup-cron.sh at /scripts (so it can be edited without a rebuild), and
# falls back to the copy baked into this image at build time if that bind
# mount is missing, empty, or stale.
#
# Why this exists: the previous entrypoint ran `/bin/sh /scripts/backup-cron.sh`
# directly. If that file wasn't there -- e.g. because the host directory
# backing the ./scripts bind mount hadn't been populated by whatever deploy
# process manages this stack -- the container failed instantly, and
# `restart: unless-stopped` retried in a tight crash loop forever: fast enough
# that the container was never "running" long enough for `docker exec` or
# Portainer's console to attach. That made the failure itself undiagnosable
# from inside the container -- you could only ever see it in the logs, and
# only by getting lucky with timing. This wrapper guarantees something always
# runs, and that the container always stays reachable, even in the worst case.
set -u
LIVE="/scripts/backup-cron.sh"
FALLBACK="/app/scripts-default/backup-cron.sh"
if [ -f "$LIVE" ]; then
echo "[entrypoint] using live scripts from /scripts (bind mount present)"
exec /bin/sh "$LIVE"
fi
echo "[entrypoint] WARNING: $LIVE not found." >&2
echo "[entrypoint] The ./scripts bind mount is missing, empty, or stale on the host." >&2
echo "[entrypoint] Check the directory backing that mount (see docker-compose.yml)." >&2
if [ -f "$FALLBACK" ]; then
echo "[entrypoint] Falling back to the scripts baked into this image at build time." >&2
echo "[entrypoint] Backups will still run, on whatever version was current when this" >&2
echo "[entrypoint] image was last built -- not any newer live edits to ./scripts." >&2
exec /bin/sh "$FALLBACK"
fi
echo "[entrypoint] FATAL: no backup-cron.sh in the bind mount or the image." >&2
echo "[entrypoint] Staying up (idle) instead of crash-looping, so this container" >&2
echo "[entrypoint] can still be reached via 'docker exec' / the Portainer console." >&2
while true; do
sleep 3600
done