From f4d9c41466b2f6f47de2a9c1acc86c6fb8e28774 Mon Sep 17 00:00:00 2001 From: m4bard <304653687+m4bard@users.noreply.github.com> Date: Thu, 3 Sep 2026 23:40:34 -0500 Subject: [PATCH] fix(docker): give the image a healthcheck it can actually run The runtime image has never declared a HEALTHCHECK, so a Listenarr container reports no health at all and anything that gates on container health has nothing to read. An operator who adds one reaches for the obvious curl -f http://localhost:4545/, and that cannot work here. The runtime stage pulls curl in only as a build dependency of the Discord bot's Node install and then purges it again, and wget was never present. The check exits 127 on every run, so the container sits permanently unhealthy while the application serves normally. An orchestrator that restarts on unhealthy will act on that, and a real outage becomes indistinguishable from the standing false one. Node is in the final image deliberately, because the Discord bot runtime needs it and the install script keeps the node binary after stripping npm. So the request can be issued without adding anything to the image: node -e with http.get, exiting 0 only on a 200. The check asks the application's own readiness probe, GET /api/v1/system/ready, rather than /. That endpoint is AllowAnonymous, so it answers without a session, and it already returns 503 until the database is connected, migrations are current and the filesystem is ready. That is the condition worth reporting as health. A request to / would only show that the static file middleware is mounted. Both Dockerfiles get the instruction. listenarr.api/Dockerfile.runtime is the one CI publishes; the root Dockerfile is the local build, and leaving it out would let the two disagree about what a healthy container means. Co-Authored-By: Claude Opus 5 (1M context) --- Dockerfile | 7 +++++++ listenarr.api/Dockerfile.runtime | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/Dockerfile b/Dockerfile index 395f174f5..4c5d114fe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -62,4 +62,11 @@ COPY docker-entrypoint.sh /docker-entrypoint.sh RUN sh /tmp/listenarr-runtime/prepare-entrypoint.sh \ && rm -rf /tmp/listenarr-runtime +# Report container health from the application's own readiness probe. The runtime +# image ships no HTTP client - curl and gnupg are purged after the Discord bot +# install and wget was never present - so the request is issued with the node +# binary that the bot runtime already puts in the image. +HEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=3 \ + CMD ["node", "-e", "require('http').get('http://127.0.0.1:4545/api/v1/system/ready', res => process.exit(res.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"] + ENTRYPOINT ["/docker-entrypoint.sh"] diff --git a/listenarr.api/Dockerfile.runtime b/listenarr.api/Dockerfile.runtime index 6dbccc1c0..e5fc8f9e6 100644 --- a/listenarr.api/Dockerfile.runtime +++ b/listenarr.api/Dockerfile.runtime @@ -44,4 +44,11 @@ COPY docker-entrypoint.sh /docker-entrypoint.sh RUN sh /tmp/listenarr-runtime/prepare-entrypoint.sh \ && rm -rf /tmp/listenarr-runtime +# Report container health from the application's own readiness probe. The runtime +# image ships no HTTP client - curl and gnupg are purged after the Discord bot +# install and wget was never present - so the request is issued with the node +# binary that the bot runtime already puts in the image. +HEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=3 \ + CMD ["node", "-e", "require('http').get('http://127.0.0.1:4545/api/v1/system/ready', res => process.exit(res.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"] + ENTRYPOINT ["/docker-entrypoint.sh"]