fix(entrypoint): graceful docker stop + cac-fpm error_log owner on first start
Graceful stop (cac, cac-fpm, shared-httpd, and form-sender which runs cac): entrypoint.sh, entrypoint-fpm.sh and entrypoint-shared-httpd.sh end with bash as PID 1 tailing logs. PID 1 only receives signals it has a handler for, so docker stop's SIGTERM was dropped; docker waited the full 10s and SIGKILLed apache / php-fpm mid-request (exit 137). The entrypoints now trap TERM/INT and stop each daemon they started via the new scripts/stop-services.sh: httpd graceful-stop (WINCH), php-fpm QUIT, crond TERM, each with a bounded wait and a TERM/KILL fallback, budgets summing to < 10s, then exit 0. The log tail is backgrounded + `wait`ed (and shared-httpd's 60s rescan sleep likewise) so the trap runs immediately. Log streaming to docker logs is unchanged. cac-fpm error_log: entrypoint-fpm.sh `touch`ed logs/php-fpm/error.log as root AFTER its chown -R, so on a site's first start the pool (customer uid) could not open it and PHP errors went out as FastCGI stderr to shared-httpd's log until the next restart. The file is now created and chowned to the customer right after the logs dir is made, before php-fpm starts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
14761597bb
commit
eaf616eb30
5 files changed
+137
-8
No files matched your search
@@ -13,6 +13,7 @@ COPY ./scripts/detect-memory.sh /scripts/detect-memory.sh
|
||||
COPY ./scripts/create-apache-mpm-config.sh /scripts/create-apache-mpm-config.sh
|
||||
COPY ./scripts/log-rotate.sh /scripts/log-rotate.sh
|
||||
COPY ./scripts/entrypoint-shared-httpd.sh /scripts/entrypoint-shared-httpd.sh
|
||||
COPY ./scripts/stop-services.sh /scripts/stop-services.sh
|
||||
COPY ./scripts/tune-mpm.sh /scripts/tune-mpm.sh
|
||||
RUN chmod +x /scripts/*
|
||||
|
||||
|
||||
@@ -16,6 +16,14 @@ adduser -u $uid $user
|
||||
|
||||
mkdir -p /home/$user/public_html
|
||||
mkdir -p /home/$user/logs/php-fpm
|
||||
# Create the pool's error_log (php_admin_value[error_log] in
|
||||
# create-php-config.sh) BEFORE the chown -R below, and chown it explicitly.
|
||||
# It used to be `touch`ed as root at the very end, after the chown, so on a
|
||||
# site's first start the pool (customer uid) could not open it and PHP errors
|
||||
# fell back to FastCGI stderr -> shared-httpd's error log until the next
|
||||
# restart's chown -R fixed the owner.
|
||||
touch /home/$user/logs/php-fpm/error.log
|
||||
chown $user:$user /home/$user/logs/php-fpm/error.log
|
||||
|
||||
ln -sf /home/$user/logs/php-fpm /var/log/php-fpm
|
||||
|
||||
@@ -89,8 +97,26 @@ crontab -u $user /home/$user/crontab
|
||||
|
||||
/usr/sbin/crond
|
||||
|
||||
# Tail PHP-FPM logs (becomes PID 1 process)
|
||||
touch /home/$user/logs/php-fpm/error.log
|
||||
tail -f /home/$user/logs/php-fpm/*
|
||||
# Graceful shutdown. bash is PID 1 here, and PID 1 only receives signals it
|
||||
# has a handler for, so without this trap `docker stop`'s SIGTERM was dropped:
|
||||
# docker waited the full 10s and SIGKILLed php-fpm mid-request (exit 137).
|
||||
# php-fpm QUIT = graceful (workers finish their current request), then crond.
|
||||
source /scripts/stop-services.sh
|
||||
TAIL_PID=""
|
||||
shutdown() {
|
||||
trap - TERM INT
|
||||
echo "entrypoint: received stop signal, shutting down"
|
||||
stop_service php-fpm /run/php-fpm/php-fpm.pid QUIT 6 'php-fpm: master'
|
||||
stop_service crond /run/crond.pid TERM 1 '^/usr/sbin/crond'
|
||||
[ -n "$TAIL_PID" ] && kill "$TAIL_PID" 2>/dev/null
|
||||
exit 0
|
||||
}
|
||||
trap shutdown TERM INT
|
||||
|
||||
# Tail PHP-FPM logs to docker logs. Backgrounded + `wait` (instead of a
|
||||
# foreground tail) so bash can run the trap as soon as a signal arrives.
|
||||
tail -f /home/$user/logs/php-fpm/* &
|
||||
TAIL_PID=$!
|
||||
wait "$TAIL_PID"
|
||||
|
||||
exit 0
|
||||
@@ -59,11 +59,30 @@ fi
|
||||
# Start cron for log rotation
|
||||
/usr/sbin/crond
|
||||
|
||||
# Tail Apache logs (becomes PID 1 process)
|
||||
# Graceful shutdown. bash is PID 1 here, and PID 1 only receives signals it
|
||||
# has a handler for, so without this trap `docker stop`'s SIGTERM was dropped:
|
||||
# docker waited the full 10s and SIGKILLed apache mid-request (exit 137), for
|
||||
# every site on the host at once. graceful-stop (WINCH) lets in-flight requests
|
||||
# finish; after 7s it falls back to TERM. Timeouts sum to < 10s so this
|
||||
# completes inside docker's default stop timeout.
|
||||
source /scripts/stop-services.sh
|
||||
TAIL_PID=""
|
||||
SLEEP_PID=""
|
||||
shutdown() {
|
||||
trap - TERM INT
|
||||
echo "entrypoint-shared-httpd: received stop signal, shutting down"
|
||||
stop_service httpd /etc/httpd/run/httpd.pid WINCH 7 '^/usr/sbin/httpd'
|
||||
stop_service crond /run/crond.pid TERM 1 '^/usr/sbin/crond'
|
||||
[ -n "$SLEEP_PID" ] && kill "$SLEEP_PID" 2>/dev/null
|
||||
[ -n "$TAIL_PID" ] && kill "$TAIL_PID" 2>/dev/null
|
||||
exit 0
|
||||
}
|
||||
trap shutdown TERM INT
|
||||
|
||||
# Tail Apache logs to docker logs.
|
||||
# Use a loop to pick up new log files as vhosts are added.
|
||||
# tail -f only watches files that exist at start time.
|
||||
touch /var/log/httpd/error_log
|
||||
TAIL_PID=""
|
||||
while true; do
|
||||
LOG_FILES=$(find /var/log/httpd/ -name '*.log' -o -name '*_log' 2>/dev/null | sort)
|
||||
if [ -n "$TAIL_PID" ]; then
|
||||
@@ -72,6 +91,10 @@ while true; do
|
||||
fi
|
||||
tail -f $LOG_FILES &
|
||||
TAIL_PID=$!
|
||||
# Re-check for new log files every 60 seconds
|
||||
sleep 60
|
||||
# Re-check for new log files every 60 seconds. Background + `wait` (not a
|
||||
# foreground sleep) so a stop signal runs the trap immediately instead of
|
||||
# after up to 60s.
|
||||
sleep 60 &
|
||||
SLEEP_PID=$!
|
||||
wait "$SLEEP_PID"
|
||||
done
|
||||
+26
-1
@@ -100,7 +100,32 @@ fi
|
||||
crontab -u $user /home/$user/crontab
|
||||
|
||||
/usr/sbin/crond
|
||||
tail -f /var/log/httpd/*
|
||||
|
||||
# Graceful shutdown. bash is PID 1 here, and PID 1 only receives signals it
|
||||
# has a handler for, so without this trap `docker stop`'s SIGTERM was dropped:
|
||||
# docker waited the full 10s and SIGKILLed apache and php-fpm mid-request
|
||||
# (exit 137). Stop apache first (graceful-stop: finish in-flight requests,
|
||||
# accept no new ones), then php-fpm (QUIT: workers finish their current
|
||||
# request), then crond. Timeouts sum to < 10s so this completes inside
|
||||
# docker's default stop timeout.
|
||||
source /scripts/stop-services.sh
|
||||
TAIL_PID=""
|
||||
shutdown() {
|
||||
trap - TERM INT
|
||||
echo "entrypoint: received stop signal, shutting down"
|
||||
stop_service httpd /etc/httpd/run/httpd.pid WINCH 4 '^/usr/sbin/httpd'
|
||||
stop_service php-fpm /run/php-fpm/php-fpm.pid QUIT 3 'php-fpm: master'
|
||||
stop_service crond /run/crond.pid TERM 1 '^/usr/sbin/crond'
|
||||
[ -n "$TAIL_PID" ] && kill "$TAIL_PID" 2>/dev/null
|
||||
exit 0
|
||||
}
|
||||
trap shutdown TERM INT
|
||||
|
||||
# Stream apache logs to docker logs. Backgrounded + `wait` (instead of a
|
||||
# foreground tail) so bash can run the trap as soon as a signal arrives.
|
||||
tail -f /var/log/httpd/* &
|
||||
TAIL_PID=$!
|
||||
wait "$TAIL_PID"
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
## stop-services.sh — sourced by the bash PID-1 entrypoints (entrypoint.sh,
|
||||
## entrypoint-fpm.sh, entrypoint-shared-httpd.sh). Not executed directly.
|
||||
##
|
||||
## Why this exists: those entrypoints end with bash as PID 1 tailing logs.
|
||||
## The kernel never delivers a signal to PID 1 unless PID 1 installed a handler
|
||||
## for it, so `docker stop`'s SIGTERM was silently dropped. Docker waited the
|
||||
## full stop timeout (10s) and then SIGKILLed the whole container, killing
|
||||
## apache / php-fpm mid-request (exit 137). The entrypoints now trap TERM/INT
|
||||
## and call stop_service for each daemon they started, then exit 0.
|
||||
##
|
||||
## Budget: docker's default stop timeout is 10s. Callers pass per-service
|
||||
## timeouts that sum to < 10s so the graceful path always completes before
|
||||
## docker gives up and SIGKILLs.
|
||||
|
||||
# A pid counts as alive if it exists and is not a zombie. bash (PID 1) reaps
|
||||
# the daemons it adopted, but between exit and reap a zombie still answers
|
||||
# `kill -0`, so check /proc state too.
|
||||
_svc_alive() {
|
||||
[ -n "$1" ] || return 1
|
||||
kill -0 "$1" 2>/dev/null || return 1
|
||||
! grep -qs '^State:[[:space:]]*Z' "/proc/$1/status"
|
||||
}
|
||||
|
||||
# stop_service NAME PIDFILE GRACEFUL_SIGNAL GRACE_SECONDS [PGREP_PATTERN]
|
||||
# Sends GRACEFUL_SIGNAL, waits up to GRACE_SECONDS for the process to exit,
|
||||
# then sends TERM and waits 1s more, then KILL. Pid comes from PIDFILE, or
|
||||
# from `pgrep -o -f PGREP_PATTERN` when the pidfile is missing/stale.
|
||||
stop_service() {
|
||||
local name=$1 pidfile=$2 sig=$3 grace=$4 pat=${5:-} pid="" i
|
||||
[ -r "$pidfile" ] && pid=$(tr -dc '0-9' < "$pidfile")
|
||||
if ! _svc_alive "$pid" && [ -n "$pat" ]; then
|
||||
pid=$(pgrep -o -f "$pat" 2>/dev/null)
|
||||
fi
|
||||
if ! _svc_alive "$pid"; then
|
||||
echo "entrypoint: $name not running"
|
||||
return 0
|
||||
fi
|
||||
echo "entrypoint: stopping $name (pid $pid) with SIG$sig"
|
||||
kill -s "$sig" "$pid" 2>/dev/null
|
||||
for ((i = 0; i < grace * 10; i++)); do
|
||||
_svc_alive "$pid" || { echo "entrypoint: $name stopped"; return 0; }
|
||||
sleep 0.1
|
||||
done
|
||||
echo "entrypoint: $name still running after ${grace}s, sending SIGTERM"
|
||||
kill -TERM "$pid" 2>/dev/null
|
||||
for ((i = 0; i < 10; i++)); do
|
||||
_svc_alive "$pid" || { echo "entrypoint: $name stopped"; return 0; }
|
||||
sleep 0.1
|
||||
done
|
||||
echo "entrypoint: $name still running, sending SIGKILL"
|
||||
kill -KILL "$pid" 2>/dev/null
|
||||
return 0
|
||||
}
|
||||
Reference in new issue
Block a user