From 45eb85fae89f09dc790b2bcca3eaba5487436cf5 Mon Sep 17 00:00:00 2001 From: bitdeals Date: Fri, 7 Aug 2026 13:39:43 +0000 Subject: [PATCH] refactor: move the renewal loop into a script, reset the base entrypoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The loop lived in a shell-form ENTRYPOINT one-liner: unlintable, uncommentable, and expanded by docker into two nested shells. It is a file now, and the loop is CMD with the base image's `certbot` entrypoint reset — CMD is appended to ENTRYPOINT rather than replacing it, so without the reset the loop would have arrived as arguments to certbot. In exchange a one-off run replaces the loop outright and needs no --entrypoint: docker run --rm -v letsencrypt:/etc/letsencrypt certbot certificates `wait $(jobs -p)` became a bare `wait`: the command substitution runs in a subshell that reports the parent's jobs in bash but not in dash, and bare `wait` waits for every background job in either. A failed pass is now reported rather than passing silently, and the trap covers INT as well so Ctrl-C in an interactive run works. What this does not fix: a signal arriving while certbot is talking to Let's Encrypt is held until that call returns, because a POSIX shell runs a trap only after the foreground command finishes. That can outlast docker's ten-second stop grace. Set stop_grace_period on the service if a clean stop matters. --- docker/Dockerfile | 9 ++++++++- docker/scripts/entrypoint.sh | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 docker/scripts/entrypoint.sh diff --git a/docker/Dockerfile b/docker/Dockerfile index 648ca16..28ec0cd 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -12,5 +12,12 @@ RUN chmod 755 /opt/scripts/*.sh # Expose port 380 EXPOSE 380 -ENTRYPOINT /bin/sh -c 'trap exit TERM; while :; do /opt/scripts/1-renew-cert.sh ; sleep 12h & wait $(jobs -p); done;' +# The base image runs certbot itself (ENTRYPOINT ["certbot"]). Reset it, or the +# CMD below is appended as arguments to certbot instead of replacing it. Both +# directives are in exec form: a shell-form ENTRYPOINT makes docker drop CMD +# altogether, and a shell-form CMD would expand into a nested /bin/sh -c. +# Keeping the loop in CMD is what lets a one-off run replace it outright: +# docker run --rm -v letsencrypt:/etc/letsencrypt certbot certificates +ENTRYPOINT [] +CMD ["/opt/scripts/entrypoint.sh"] diff --git a/docker/scripts/entrypoint.sh b/docker/scripts/entrypoint.sh new file mode 100644 index 0000000..27bc113 --- /dev/null +++ b/docker/scripts/entrypoint.sh @@ -0,0 +1,32 @@ +#!/bin/sh + +# The renewal loop: run a pass, sleep 12 hours, repeat. PID 1 of the container. +# +# Deliberately not `set -e`: a pass that fails — Let's Encrypt unreachable, +# HAProxy refusing the certificate — must not end the loop, it must be retried +# on the next one. +# +# The trap is not decoration. PID 1 has no default signal dispositions, so a +# signal with no handler installed is ignored outright and `docker stop` would +# always have to fall through to SIGKILL. +# +# `sleep &` followed by `wait`, rather than a plain `sleep`: a foreground +# command keeps the shell from running a trap until it finishes, so a plain +# sleep would leave the container deaf to SIGTERM for up to 12 hours. `wait` is +# interruptible. Bare `wait` rather than `wait $(jobs -p)` — the command +# substitution runs in a subshell that reports the parent's jobs in bash but +# not in dash, and bare `wait` waits for every background job in either. +# +# One thing this cannot fix: a signal arriving while 1-renew-cert.sh is in the +# foreground is deferred until that script returns, and an issuance talking to +# Let's Encrypt can outlast docker's 10-second stop grace. Give the service a +# longer `stop_grace_period` if that matters. + +trap 'exit 0' TERM INT + +while :; do + /opt/scripts/1-renew-cert.sh \ + || echo "entrypoint: renewal pass failed, retrying in 12h" >&2 + sleep 12h & + wait +done