Skip to content

docker: give the image a healthcheck it can actually run - #941

Open
m4bard wants to merge 1 commit into
Listenarrs:canaryfrom
m4bard:fix/72-container-healthcheck
Open

docker: give the image a healthcheck it can actually run#941
m4bard wants to merge 1 commit into
Listenarrs:canaryfrom
m4bard:fix/72-container-healthcheck

Conversation

@m4bard

@m4bard m4bard commented Sep 4, 2026

Copy link
Copy Markdown

Closes #940.

Adds a HEALTHCHECK to both Dockerfiles. 14 lines added, nothing removed, no application code touched.

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))"]

#940 carries the reasoning. In brief, on each choice:

The command runs node rather than pulling in a package. The runtime ships no HTTP client, so a curl or wget check would mean putting one back. Dockerfile.runtime:36 records that node is kept deliberately for the Discord bot ("the bot only needs node"), so the request goes through that binary instead. It adds no package and no layer.

It asks /api/v1/system/ready rather than /. That endpoint is [AllowAnonymous], so it answers with authentication enabled, and it returns 200 or 503 off real database, migration and filesystem state. A request to / would only prove the static file middleware is mounted.

Both Dockerfiles get it. The publish workflow builds listenarr.api/Dockerfile.runtime; the root Dockerfile is the local build. Patching one and not the other would let the two disagree about what a healthy container means.

On the intervals: 30s apart with a 5s timeout is cheap for one local HTTP request. The 90s start period is the one worth explaining. Failures inside that window do not count against the retry budget, and a first boot with migrations to run can take a while, so a generous window keeps a slow start from being reported as unhealthy.

If you build this with podman

Podman's default OCI build format drops HEALTHCHECK. It does warn, but the warning is easy to miss, so a locally built image looks as though this change did nothing. Build with --format docker and the instruction survives.

The published image is unaffected. CI builds through buildx, and the config blob of another buildx-built image using the same OCI media types from the same registry carries a populated Healthcheck.

What was checked

By running, against stock ghcr.io/listenarrs/listenarr:canary:

  • curl, wget, nc and busybox are all absent, and the naive curl healthcheck exits 127.
  • Both exit branches of the new command: 200 gives 0, 503 gives 1, connection refused gives 1.
  • The command succeeding against the real running application.
  • The instruction landing in image metadata after a build.
  • A container transitioning to Status=healthy with FailingStreak=0.

I have not run this in production. The evidence above comes from a test container.

Worked through with Claude Code at my direction. The image contents and both exit branches were checked by running them against the published image, and I watched a container reach healthy before opening this. I reviewed this before posting.

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) <noreply@anthropic.com>
@m4bard
m4bard requested a review from a team September 4, 2026 06:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The image declares no HEALTHCHECK, and the obvious one cannot work because the runtime has no HTTP client

1 participant