fix: the image brings its own PID 1 instead of asking for init: true (4295)
CI / lint (push) Successful in 3s
CI / extension-version (push) Successful in 3s
Build images / sign-extension (push) Successful in 4s
Build images / build-agent (push) Successful in 6s
extension / lint (push) Successful in 19s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 33s
CI / integration (push) Successful in 2m15s
Build images / build-web (push) Successful in 2m52s
Build images / smoke-web (push) Successful in 1m16s
Build images / promote (push) Skipped
CI / lint (push) Successful in 3s
CI / extension-version (push) Successful in 3s
Build images / sign-extension (push) Successful in 4s
Build images / build-agent (push) Successful in 6s
extension / lint (push) Successful in 19s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 33s
CI / integration (push) Successful in 2m15s
Build images / build-web (push) Successful in 2m52s
Build images / smoke-web (push) Successful in 1m16s
Build images / promote (push) Skipped
Operator, 2026-09-23: *"it is out of the norm to require this init call we need to fix this."* Correct, and it is the same mistake as declaring the healthcheck per service — the image needing a deployment to remember a flag before it behaves correctly. PID 1 carries a duty no other process has: every orphaned process in the container reparents to it and must be reaped or it stays a zombie holding a PID slot. This app makes orphans in normal operation — six service modules shell out (gallery_dl, thumbnailer, backup_service, external_fetch, download_service, download_backends) and celery's prefork pool forks children that spawn them. Whatever the role, something that is not an init ends up as PID 1: supervisord for `all`, hypercorn for `web`, celery for a worker. `init: true` covered that, and cost correctness the moment it was forgotten or silently dropped — an older Swarm, a plain `docker run`, a compose file someone copied. No signal either way. So tini goes in the image and is the ENTRYPOINT. `docker run <image>` is correct on its own now, `init: true` comes out of docker-compose.single.yml, and nothing downstream has to know. The smoke asserts /proc/1/comm is tini, read from /proc because the runtime stage installs no `ps`. ## A correction to what I told the operator I justified `init: true` by saying supervisord "has no idea about orphans it never started". That is very likely wrong: supervisord's reaper calls waitpid(-1) and logs "reaped unknown pid" for children it did not spawn, so it does reap orphans. I asserted the mechanism without checking it, and could not check it here — `supervisor` is not installed in this environment. It does not change this commit. tini is correct whichever way that lands, and it covers the single-role containers too, where celery or hypercorn is PID 1 and the subprocess-spawning is heaviest. But the reason I gave was not a verified one and should not have been stated as fact. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
This commit is contained in:
@@ -1553,6 +1553,19 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "smoke: every lane answered"
|
echo "smoke: every lane answered"
|
||||||
|
|
||||||
|
# PID 1 is an init, and the IMAGE provides it — no `init: true` in
|
||||||
|
# whatever runs this. Read from /proc rather than `ps`, which the
|
||||||
|
# runtime stage does not install.
|
||||||
|
PID1=$(docker exec "$CID_ALL" cat /proc/1/comm)
|
||||||
|
echo "smoke: pid 1 is $PID1"
|
||||||
|
if [ "$PID1" != "tini" ]; then
|
||||||
|
echo "smoke: FAILED — pid 1 is '$PID1', not an init." >&2
|
||||||
|
echo "smoke: orphaned gallery-dl/ffmpeg/pg_dump processes would" >&2
|
||||||
|
echo "smoke: accumulate as zombies, and the image would be back" >&2
|
||||||
|
echo "smoke: to needing init:true from every deployment." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
# Say WHICH processes supervisord is running, so a lane that is
|
# Say WHICH processes supervisord is running, so a lane that is
|
||||||
# merely restart-looping is visible rather than inferred.
|
# merely restart-looping is visible rather than inferred.
|
||||||
#
|
#
|
||||||
|
|||||||
+24
-1
@@ -28,6 +28,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
postgresql-client \
|
postgresql-client \
|
||||||
zstd \
|
zstd \
|
||||||
megatools \
|
megatools \
|
||||||
|
# PID 1 for every role. See the ENTRYPOINT note at the foot of this file:
|
||||||
|
# without it the image needs `init: true` in whatever runs it, which is a
|
||||||
|
# deployment remembering a flag for the image to behave correctly.
|
||||||
|
tini \
|
||||||
libjpeg62-turbo \
|
libjpeg62-turbo \
|
||||||
libwebp7 \
|
libwebp7 \
|
||||||
libpng16-16 \
|
libpng16-16 \
|
||||||
@@ -142,7 +146,26 @@ EXPOSE 8080
|
|||||||
HEALTHCHECK --interval=30s --timeout=15s --start-period=90s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=15s --start-period=90s --retries=3 \
|
||||||
CMD ["python", "-m", "backend.app.scripts.healthcheck"]
|
CMD ["python", "-m", "backend.app.scripts.healthcheck"]
|
||||||
|
|
||||||
ENTRYPOINT ["./entrypoint.sh"]
|
# tini is PID 1, and the image brings its own rather than asking the
|
||||||
|
# deployment for one.
|
||||||
|
#
|
||||||
|
# PID 1 carries a duty no other process has: every orphaned process in the
|
||||||
|
# container reparents to it and must be reaped, or it stays a zombie holding
|
||||||
|
# a PID slot. This app makes orphans in normal operation — six service
|
||||||
|
# modules shell out (gallery-dl, ffmpeg, pg_dump, the external fetchers) and
|
||||||
|
# celery's prefork pool forks children that spawn them.
|
||||||
|
#
|
||||||
|
# Whatever the role, something that is not an init ends up as PID 1:
|
||||||
|
# supervisord for `all`, hypercorn for `web`, celery for a worker. The fix
|
||||||
|
# was `init: true` in the compose/stack file, which is out of the norm and
|
||||||
|
# put correct process handling in the hands of whoever deploys the image —
|
||||||
|
# the same mistake as declaring the healthcheck per service. A flag that is
|
||||||
|
# silently dropped (an older Swarm, a `docker run` without it) costs reaping
|
||||||
|
# with no signal at all.
|
||||||
|
#
|
||||||
|
# So the image owns it. `docker run <image>` is correct on its own, and
|
||||||
|
# nothing downstream has to know. The smoke asserts /proc/1/comm is tini.
|
||||||
|
ENTRYPOINT ["/usr/bin/tini", "--", "./entrypoint.sh"]
|
||||||
# The DEFAULT is the whole application, not one lane of it.
|
# The DEFAULT is the whole application, not one lane of it.
|
||||||
#
|
#
|
||||||
# `docker run fabledcurator` with no command starts hypercorn plus every
|
# `docker run fabledcurator` with no command starts hypercorn plus every
|
||||||
|
|||||||
@@ -63,12 +63,6 @@ services:
|
|||||||
# under supervisord — is what the image does by default, and supervisord's
|
# under supervisord — is what the image does by default, and supervisord's
|
||||||
# config is generated from the application's own lane table so the two
|
# config is generated from the application's own lane table so the two
|
||||||
# cannot disagree. `command: ["all"]` still works and means the same thing.
|
# cannot disagree. `command: ["all"]` still works and means the same thing.
|
||||||
# tini as PID 1, in front of supervisord. supervisord reaps its own
|
|
||||||
# children, but a container's PID 1 also inherits orphans from anywhere
|
|
||||||
# below — celery's prefork pool and gallery-dl's subprocesses both make
|
|
||||||
# them. Without this they accumulate as zombies for the life of the
|
|
||||||
# container.
|
|
||||||
init: true
|
|
||||||
# Sized to the SLOWEST lane, not the average. maintenance_long runs DB
|
# Sized to the SLOWEST lane, not the average. maintenance_long runs DB
|
||||||
# backups, library audits and translation backfill, and gets 180s to
|
# backups, library audits and translation backfill, and gets 180s to
|
||||||
# finish a chunk; the lanes stop in parallel, so this covers the max
|
# finish a chunk; the lanes stop in parallel, so this covers the max
|
||||||
|
|||||||
Reference in New Issue
Block a user