Skip to content

chore(docker): adopt hadolint 2.15.1 and fix the four findings it reports - #1700

Merged
cristim merged 2 commits into
mainfrom
chore/1695-hadolint-digest-pin
Aug 3, 2026
Merged

cristim merged 2 commits into
mainfrom
chore/1695-hadolint-digest-pin

Conversation

@cristim

@cristim cristim commented Aug 3, 2026 •

Copy link
Copy Markdown
Member

Closes #1701

What this is now

This PR was opened against #1695 before I discovered that #1697 had already landed the fix for it. #1695 is closed and main's pre-commit is green as of 218f3858e (12:53). The queue is unblocked, and nothing here is needed for that.

The branch has been rebased onto current main, and the duplicate pin commit dropped out automatically (git rebase: "dropping 0010d70... -- patch contents already upstream"). What remains is the half main does not have.

What remains, and why it is worth landing

#1697 pinned the hadolint image to the v2.14.0 digest. That correctly fixes the root cause (the upstream hadolint-docker hook declares entry: ghcr.io/hadolint/hadolint hadolint with no image tag, so rev: pinned the hook definition while Docker resolved :latest every run).

But it leaves the four findings un-seen rather than fixed. 2.14.0 reports nothing only because it predates the rules. This PR moves the pin to 2.15.1 and fixes them, with no .hadolint.yaml ignore entries and no inline # hadolint ignore= directives.

DL3066, non-numeric user id (Dockerfile:149, Dockerfile.test:21, Dockerfile.dev:62). A name-form USER cannot be verified by Kubernetes runAsNonRoot and similar admission checks, which see only the numeric id.

  • Dockerfile and Dockerfile.test already pinned uids explicitly (1000, 10001), so those are substitutions with no behaviour change.
  • Dockerfile.dev used adduser -S, which takes the first free system id and so varies with the base image; uid/gid are now pinned to 1001. Its uid value does change. Nothing outside Dockerfile.dev refers to devuser, so it is contained.

DL3025, JSON notation (Dockerfile:165). This is the one that could have broken the running image. The flagged line is the HEALTHCHECK CMD, not the container ENTRYPOINT/CMD (already exec form):

CMD curl -f http://localhost:8080/health || exit 1

|| is a shell operator. Converting to ["curl","-f","...","||","exit","1"] would hand || and exit to curl as literal arguments and the healthcheck would stop reporting unhealthy correctly. Written instead as ["/bin/sh", "-c", "curl -f http://localhost:8080/health || exit 1"] — JSON notation, same process tree, shell dependency declared rather than implied.

Verification

Docker was available, so this is built and exercised rather than reasoned about:

  • hadolint 2.15.1 against all three Dockerfiles: exit 0. pre-commit run hadolint --all-files: exit 0.
  • All three images build: Dockerfile, Dockerfile.dev, Dockerfile.test, exit 0 each.
  • Runtime identity resolves as intended: uid=1000(cudly), uid=10001(e2e), uid=1001(devuser). The numeric form still resolves to the named user.
  • Healthcheck recorded as ["CMD","/bin/sh","-c","curl -f http://localhost:8080/health || exit 1"] and exercised both directions: exit 1 with no server listening (proving || is shell-evaluated, not passed to curl) and exit 0 on the short-circuit path.

For the record, on the root cause

My first hypothesis on #1695 — that a pinned tag had been rebuilt underneath us — was wrong. The image was never pinned at all. Registry confirms v2.14.0 (sha256:27086352…) and v2.15.1 (sha256:32dac941…, identical to latest) are different images, and reproducing both directions locally gives exit 0 and exit 1 respectively.

One unrelated failure, reported not fixed

pre-commit run --all-files exits 1 locally on Terraform validate: Failed to install provider ... Error while installing hashicorp/azurerm v4.74.0: mkdir ...: file exists. That is a shared ~/.terraform.d/plugin-cache race between concurrent worktrees, not a config error. This diff touches zero .tf files.

If you would rather not take this

Closing is a reasonable call. main is green, and this is hygiene rather than a fix for anything currently broken. The argument for landing it is that staying on 2.14.0 means never picking up new hadolint rules, which is the slow version of the drift #1695 was about.

Summary by CodeRabbit

  • Bug Fixes

    • Improved container health-check command handling for more reliable status reporting.
    • Standardized container user and group IDs across runtime, development, and test environments.
  • Chores

    • Updated the Hadolint pre-commit check to version 2.15.1 with an immutable image reference.

@cristim cristim added triaged Item has been triaged priority/p1 Next up; this sprint severity/medium Moderate harm urgency/now Drop other things impact/internal Team-internal only effort/s Hours type/chore Maintenance / non-user-visible labels Aug 3, 2026
@coderabbitai

coderabbitai Bot commented Aug 3, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 20 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fa23d13d-6575-4a34-833b-a37e047149fb

📥 Commits

Reviewing files that changed from the base of the PR and between d7f6a6a and 61a6274.

📒 Files selected for processing (1)
  • .github/workflows/pre-commit.yml
📝 Walkthrough

Walkthrough

The pull request updates the Hadolint pre-commit image, changes runtime and development containers to numeric user identities, and makes the runtime health check explicitly invoke /bin/sh -c.

Changes

Container and lint configuration

Layer / File(s) Summary
Hadolint image pin
.pre-commit-config.yaml
The local hook now uses Hadolint 2.15.1 with a new immutable digest. Its comments describe the image version and untagged image behavior.
Runtime identity and health check
Dockerfile
The runtime container now uses UID:GID 1000:1000. The health check now uses JSON-form /bin/sh -c execution.
Development and test identities
Dockerfile.dev, Dockerfile.test
The development image creates and uses UID:GID 1001:1001. The test image uses UID 10001 instead of the symbolic user name.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related issues

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: upgrading Hadolint to 2.15.1 and fixing its four reported findings.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/1695-hadolint-digest-pin

Comment @coderabbitai help to get the list of available commands.

Follow-up to the digest pin in the previous commit, which restored the gate by
freezing the linter at 2.14.0. Freezing is not the end state: 2.14.0 reports
nothing only because it predates the rules, so staying there would have greened
CI by un-seeing findings rather than by addressing them.

Move the pin forward to 2.15.1, the version that surfaced them, and fix all four
in the Dockerfiles. No `.hadolint.yaml` ignore entries and no inline
`# hadolint ignore=` directives are used.

DL3066, non-numeric user id (Dockerfile:149, Dockerfile.test:21,
Dockerfile.dev:62). A name-form USER cannot be verified by Kubernetes
`runAsNonRoot` and similar admission checks, which see only the numeric id.
Dockerfile and Dockerfile.test already pinned their uids explicitly (1000 and
10001), so those are substitutions with no behaviour change. Dockerfile.dev
created its user with `adduser -S`, which takes the first free system id and so
varies with the base image; its uid/gid are now pinned to 1001 so the numeric
USER has a value fixed at build time rather than discovered. Nothing outside
Dockerfile.dev refers to `devuser`, so the changed id is contained.

DL3025, JSON notation for CMD/ENTRYPOINT (Dockerfile:165). The flagged line is
the HEALTHCHECK CMD, not the container ENTRYPOINT/CMD, which were already exec
form. It reads `curl -f http://localhost:8080/health || exit 1`, where `||` is a
shell operator: a naive exec-form list would pass `||` and `exit` to curl as
literal arguments and the healthcheck would stop reporting unhealthy correctly.
Written instead as `["/bin/sh", "-c", "..."]`, which is JSON notation and the
same process tree the shell form produced, with the dependency on a shell
declared rather than implied.

Verified, with Docker available locally:

- hadolint 2.15.1 against the three Dockerfiles: exit 0.
- All three images build: Dockerfile, Dockerfile.dev, Dockerfile.test, exit 0.
- Runtime identity is unchanged where it was already pinned:
  uid=1000(cudly) in the main image, uid=10001(e2e) in the test image, and
  uid=1001(devuser) in the dev image.
- The healthcheck is recorded as
  ["CMD","/bin/sh","-c","curl -f http://localhost:8080/health || exit 1"] and
  exercised both directions: exit 1 with no server listening (so `||` is
  evaluated by the shell rather than handed to curl) and exit 0 on the
  short-circuit path.

Closes #1695
@cristim
cristim force-pushed the chore/1695-hadolint-digest-pin branch from bd4aad3 to d7f6a6a Compare August 3, 2026 13:12
@cristim cristim added priority/p3 Polish / idea / may never ship severity/low Minor harm urgency/eventually No deadline and removed priority/p1 Next up; this sprint labels Aug 3, 2026
@cristim cristim changed the title fix(ci): digest-pin hadolint and fix the four findings 2.15.1 reports chore(docker): adopt hadolint 2.15.1 and fix the four findings it reports Aug 3, 2026
@cristim cristim removed severity/medium Moderate harm urgency/now Drop other things labels Aug 3, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.pre-commit-config.yaml:
- Around line 79-83: Update the Hadolint pin reference in the related CI comment
within the pre-commit configuration so it consistently states v2.15.1, matching
the executable hook configuration and the corresponding workflow documentation.

In `@Dockerfile.dev`:
- Around line 58-67: Update the Dockerfile user setup around devuser and USER
1001:1001 to preserve write access to the /app bind mount: accept DEV_UID and
DEV_GID build arguments and use them when creating the group/user, with
docker-compose.yml passing matching host IDs; alternatively configure a writable
named volume for .air.toml’s ./tmp/main output.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3b360697-79ce-4346-ac7d-0ce4e251eea1

📥 Commits

Reviewing files that changed from the base of the PR and between 218f385 and d7f6a6a.

📒 Files selected for processing (4)
  • .pre-commit-config.yaml
  • Dockerfile
  • Dockerfile.dev
  • Dockerfile.test

Comment thread .pre-commit-config.yaml
Comment thread Dockerfile.dev
CodeRabbit found the note in .github/workflows/pre-commit.yml still claiming the
hook is pinned to v2.14.0 after the pin moved to v2.15.1. That is the second
time this comment has gone stale, and the earlier staleness is what hid the tag
float behind the `rev:` pin in the first place.

Rather than update the number and wait for it to rot again, remove the restated
version and the `.pre-commit-config.yaml:81` line reference, which was also
stale (the hook is at line 86 now). The comment points at the hook by `id:`
instead, and names the digest entry as the single source of truth for the
version.

The remaining v2.14.0 mention further down is historical and accurate: it
records what an earlier version of the comment wrongly claimed.

Refs #1701
@cristim
cristim merged commit 1e1e7e4 into main Aug 3, 2026
19 checks passed
@cristim
cristim deleted the chore/1695-hadolint-digest-pin branch August 3, 2026 13:55
@cristim

cristim commented Aug 3, 2026

Copy link
Copy Markdown
Member Author

Review and decision record (merged)

Follow-up to #1697, which unblocked CI by pinning hadolint to v2.14.0 by digest. That was correct as an unblock, but it left the four findings un-seen rather than fixed — 2.14.0 reports nothing only because it predates the rules. Green by freezing is the slow version of the same drift. This PR adopts 2.15.1 and fixes them for real.

DL3025 — the one that could have broken the image. The flagged line is HEALTHCHECK's CMD, not the container's ENTRYPOINT/CMD (both already exec form and untouched): curl -f http://localhost:8080/health || exit 1. || is a shell operator, so a bare exec-form list would pass || and exit to curl as literal arguments and the healthcheck would silently stop reporting unhealthy. Written as ["/bin/sh","-c","curl -f … || exit 1"] instead, and exercised both directions: exit 1 with nothing listening (proving || is shell-evaluated) and exit 0 on the short-circuit path.

DL3066 — uid pinning. Dockerfile and Dockerfile.test already pinned uids (1000, 10001), so those were pure substitutions. Dockerfile.dev used adduser -S, which takes the first free system id.

A pre-authorised exemption that was declined, and why

CodeRabbit raised a 🟠 Major on Dockerfile.dev: docker-compose.yml mounts .:/app, .air.toml writes ./tmp/main under /app, so if the host checkout is not writable by the pinned uid, Air cannot build or reload. A targeted # hadolint ignore=DL3066 was offered on the grounds that bind-mount ownership must match the host user.

That justification does not survive checking. Run against the pinned base image, adduser -S devuser yields uid=100, gid=101 — a system id that would essentially never match a host developer account either. Name form versus numeric form makes no difference to bind-mount writability. So the pin moves the fixed id from 100 to 1001, if anything closer to matching, and does not create or widen any mismatch. An ignore resting on a false premise is worse than no ignore, so the pin was kept.

What could not be verified, stated rather than glossed: Docker Desktop on macOS remaps bind mounts to whatever uid the container runs as, so both the original and the new image write fine locally. That confirms no regression on macOS and proves nothing about Linux. Reproducing the failure needs a Linux host. The underlying problem is real and tracked as #1704, favouring a named volume for Air's output over DEV_UID/DEV_GID build args.

The second thread was taken further than asked. A comment in .github/workflows/pre-commit.yml still named v2.14.0 and a stale line number. Rather than update the version, both the restated version and the line reference were removed — that comment had gone stale twice, and its earlier staleness is what hid the tag float behind the rev: pin. It now points at the hook by id: and names the digest entry as the single source of truth, so nothing in that file can drift again.

Verification: pre-commit run hadolint --all-files exit 0; all three images rebuilt exit 0; bind-mounted dev container exercised on both the current and pre-change images; CI pre-commit green, which is the check that actually covers this change.

cristim added a commit that referenced this pull request Sep 27, 2026
…orts (#1700)

* fix(docker): adopt hadolint 2.15.1 and fix the four findings it reports

Follow-up to the digest pin in the previous commit, which restored the gate by
freezing the linter at 2.14.0. Freezing is not the end state: 2.14.0 reports
nothing only because it predates the rules, so staying there would have greened
CI by un-seeing findings rather than by addressing them.

Move the pin forward to 2.15.1, the version that surfaced them, and fix all four
in the Dockerfiles. No `.hadolint.yaml` ignore entries and no inline
`# hadolint ignore=` directives are used.

DL3066, non-numeric user id (Dockerfile:149, Dockerfile.test:21,
Dockerfile.dev:62). A name-form USER cannot be verified by Kubernetes
`runAsNonRoot` and similar admission checks, which see only the numeric id.
Dockerfile and Dockerfile.test already pinned their uids explicitly (1000 and
10001), so those are substitutions with no behaviour change. Dockerfile.dev
created its user with `adduser -S`, which takes the first free system id and so
varies with the base image; its uid/gid are now pinned to 1001 so the numeric
USER has a value fixed at build time rather than discovered. Nothing outside
Dockerfile.dev refers to `devuser`, so the changed id is contained.

DL3025, JSON notation for CMD/ENTRYPOINT (Dockerfile:165). The flagged line is
the HEALTHCHECK CMD, not the container ENTRYPOINT/CMD, which were already exec
form. It reads `curl -f http://localhost:8080/health || exit 1`, where `||` is a
shell operator: a naive exec-form list would pass `||` and `exit` to curl as
literal arguments and the healthcheck would stop reporting unhealthy correctly.
Written instead as `["/bin/sh", "-c", "..."]`, which is JSON notation and the
same process tree the shell form produced, with the dependency on a shell
declared rather than implied.

Verified, with Docker available locally:

- hadolint 2.15.1 against the three Dockerfiles: exit 0.
- All three images build: Dockerfile, Dockerfile.dev, Dockerfile.test, exit 0.
- Runtime identity is unchanged where it was already pinned:
  uid=1000(cudly) in the main image, uid=10001(e2e) in the test image, and
  uid=1001(devuser) in the dev image.
- The healthcheck is recorded as
  ["CMD","/bin/sh","-c","curl -f http://localhost:8080/health || exit 1"] and
  exercised both directions: exit 1 with no server listening (so `||` is
  evaluated by the shell rather than handed to curl) and exit 0 on the
  short-circuit path.

Closes #1695

* docs(ci): stop restating the hadolint version in the workflow comment

CodeRabbit found the note in .github/workflows/pre-commit.yml still claiming the
hook is pinned to v2.14.0 after the pin moved to v2.15.1. That is the second
time this comment has gone stale, and the earlier staleness is what hid the tag
float behind the `rev:` pin in the first place.

Rather than update the number and wait for it to rot again, remove the restated
version and the `.pre-commit-config.yaml:81` line reference, which was also
stale (the hook is at line 86 now). The comment points at the hook by `id:`
instead, and names the digest entry as the single source of truth for the
version.

The remaining v2.14.0 mention further down is historical and accurate: it
records what an earlier version of the comment wrongly claimed.

Refs #1701
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/s Hours impact/internal Team-internal only priority/p3 Polish / idea / may never ship severity/low Minor harm triaged Item has been triaged type/chore Maintenance / non-user-visible urgency/eventually No deadline

Projects

None yet

Development

Successfully merging this pull request may close these issues.

chore(docker): adopt hadolint 2.15.1 and fix the four findings it reports

1 participant