From c05349f7c450c3d57fa4b830822b4df5d3f707bd Mon Sep 17 00:00:00 2001 From: Maurice Date: Sun, 25 Oct 2026 16:47:22 +0100 Subject: [PATCH] fix(docker): let dumb-init forward the stop signal in the shipped compose file The compose file drops every capability and adds back CHOWN, SETUID and SETGID. dumb-init runs as root and node as the node user, and signalling a process of another user needs KILL, so docker stop never reached node and always ended in SIGKILL (exit 137). KILL only reaches processes inside the container's own PID namespace. CI starts the image with the same set and now checks that docker stop exits 0 after a logged SIGTERM. Closes #2790 --- .github/workflows/test.yml | 13 ++++++++++++- docker-compose.yml | 4 ++++ wiki/Install-Docker-Compose.md | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ef0389629..cf48d1b6d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1161,7 +1161,7 @@ jobs: docker run -d --name trek-ci -p 127.0.0.1:3000:3000 \ --read-only --tmpfs /tmp:noexec,nosuid,size=128m \ --security-opt no-new-privileges:true \ - --cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \ + --cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add KILL \ -v "$RUNNER_TEMP/trek/data:/app/data" \ -v "$RUNNER_TEMP/trek/uploads:/app/uploads" \ -e ENCRYPTION_KEY="$(openssl rand -hex 32)" \ @@ -1194,6 +1194,17 @@ jobs: curl -sf http://127.0.0.1:3000/ | grep -q '
' \ || { echo "::error::/ does not serve the client"; exit 1; } + # Same capabilities as the shipped compose file: dumb-init has to be able + # to pass the stop signal on to node, or docker stop ends in SIGKILL (137). + - name: Stop gracefully + run: | + set -euo pipefail + docker stop -t 30 trek-ci >/dev/null + code="$(docker inspect -f '{{.State.ExitCode}}' trek-ci)" + [ "$code" = "0" ] || { echo "::error::docker stop ended with exit code $code, not a graceful shutdown"; exit 1; } + docker logs trek-ci 2>&1 | grep -q 'SIGTERM received' \ + || { echo "::error::the server never saw SIGTERM"; exit 1; } + - name: Container log if: always() run: docker logs trek-ci 2>&1 || true diff --git a/docker-compose.yml b/docker-compose.yml index ab879c62a..5c0c97f76 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,10 @@ services: - CHOWN - SETUID - SETGID + # dumb-init runs as root and forwards SIGTERM to node, which runs as the + # node user; signalling another user's process needs KILL. Without it + # docker stop ends in SIGKILL and the graceful shutdown never runs. + - KILL tmpfs: - /tmp:noexec,nosuid,size=128m ports: diff --git a/wiki/Install-Docker-Compose.md b/wiki/Install-Docker-Compose.md index 1027650cc..1379a9a9b 100644 --- a/wiki/Install-Docker-Compose.md +++ b/wiki/Install-Docker-Compose.md @@ -15,7 +15,7 @@ The compose file ships with several hardening options enabled by default: | `read_only: true` | Mounts the container filesystem read-only; only the two mounted volumes (`/app/data`, `/app/uploads`) and `/tmp` are writable | | `security_opt: no-new-privileges:true` | Prevents the process from gaining additional Linux privileges via setuid/setgid executables | | `cap_drop: [ALL]` | Drops all Linux capabilities from the container | -| `cap_add: [CHOWN, SETUID, SETGID]` | Adds back only the capabilities needed for the entrypoint to drop privileges to the `node` user | +| `cap_add: [CHOWN, SETUID, SETGID, KILL]` | Adds back only what the container needs: the first three let the entrypoint drop privileges to the `node` user, `KILL` lets `dumb-init` pass the stop signal on to it, so `docker stop` shuts TREK down cleanly instead of killing it | | `tmpfs: /tmp:noexec,nosuid,size=128m` | Mounts a 128 MB in-memory `/tmp`; required because the container root is read-only | > **Note (Docker from snap):** If you installed Docker via `snap` (config under `/var/snap/docker/...`), `no-new-privileges:true` will prevent the container from starting with `exec /usr/bin/dumb-init: operation not permitted`. This is a [snap/AppArmor limitation](https://bugs.launchpad.net/snapd/+bug/1908448), not a TREK issue — install Docker from the [official apt repository](https://docs.docker.com/engine/install/ubuntu/) instead, or remove `no-new-privileges`. See [Troubleshooting](Troubleshooting#container-wont-start-exec-usrbindumb-init-operation-not-permitted).