Skip to content

Commit d7f6a6a

Browse files
committed
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
1 parent 218f385 commit d7f6a6a

4 files changed

Lines changed: 37 additions & 16 deletions

File tree

‎.pre-commit-config.yaml‎

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,21 +67,26 @@ repos:
6767
# The upstream hadolint-docker hook (hadolint/hadolint's own
6868
# .pre-commit-hooks.yaml) declares `entry: ghcr.io/hadolint/hadolint
6969
# hadolint` with no image tag, so Docker resolves it to `:latest` on every
70-
# run -- independent of the `rev:` pin above, which only pins which
71-
# commit of the *hook definition* is used, not the Docker image it runs.
72-
# When upstream published hadolint 2.15.1 as `latest`, CI silently
73-
# started linting with a newer ruleset (new DL3066/DL3025 findings on
74-
# unchanged Dockerfiles) with no corresponding change in this repo.
70+
# run -- independent of any `rev:` pin, which only pins which commit of the
71+
# *hook definition* is used, not the Docker image it runs. When upstream
72+
# published hadolint 2.15.1 as `latest`, CI silently started linting with a
73+
# newer ruleset (new DL3066/DL3025 findings on unchanged Dockerfiles) with no
74+
# corresponding change in this repo, and `main` went red (issue #1695).
7575
#
76-
# Defined as a local hook instead, pinned to the immutable digest of the
77-
# v2.14.0 image, so the linter version can only change via an explicit
78-
# bump here.
76+
# Defined as a local hook instead, pinned to an immutable digest, so the
77+
# linter version can only change through an explicit edit to this line.
78+
#
79+
# Pinned to 2.15.1 -- the version that surfaced the findings -- rather than
80+
# back to 2.14.0. Pinning to the older image would also have made CI green,
81+
# but only by un-seeing findings that are real; the four it reported are
82+
# fixed in the Dockerfiles rather than silenced. Bumping this digest is
83+
# expected to require fixing whatever the new version finds.
7984
- repo: local
8085
hooks:
8186
- id: hadolint
8287
name: Lint Dockerfiles
8388
language: docker_image
84-
entry: ghcr.io/hadolint/hadolint:v2.14.0@sha256:27086352fd5e1907ea2b934eb1023f217c5ae087992eb59fde121dce9c9ff21e hadolint
89+
entry: ghcr.io/hadolint/hadolint:v2.15.1@sha256:32dac94127fd60b7b7e3fbfc65e1383b9b5e25c9bfd7b8536de7a539fe68a12d hadolint
8590
types: [dockerfile]
8691

8792
# Markdown linting

‎Dockerfile‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,11 @@ COPY --from=frontend-builder --chown=cudly:cudly /frontend/dist /app/static
145145
COPY --chown=cudly:cudly scripts/entrypoint.sh /entrypoint.sh
146146
RUN chmod +x /entrypoint.sh
147147

148-
# Switch to non-root user
149-
USER cudly
148+
# Switch to non-root user. Numeric form so the identity is resolvable without
149+
# the image's /etc/passwd: Kubernetes `runAsNonRoot` and similar admission
150+
# checks cannot verify a name-form USER and will refuse to start the pod.
151+
# 1000:1000 is exactly the uid:gid created above, so this is a rename only.
152+
USER 1000:1000
150153

151154
# Environment defaults
152155
ENV DB_MIGRATIONS_PATH=/app/migrations \
@@ -162,8 +165,14 @@ ENV DB_MIGRATIONS_PATH=/app/migrations \
162165
EXPOSE 8080
163166

164167
# Health check (works for HTTP mode, ignored in Lambda mode)
168+
#
169+
# JSON (exec) form, but invoking /bin/sh explicitly: the `||` is a shell
170+
# operator, so a bare exec-form list would hand `||` and `exit` to curl as
171+
# literal arguments and the healthcheck would never report unhealthy correctly.
172+
# This is the same process tree the shell form produced, written so the
173+
# dependency on a shell is declared rather than implied.
165174
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
166-
CMD curl -f http://localhost:8080/health || exit 1
175+
CMD ["/bin/sh", "-c", "curl -f http://localhost:8080/health || exit 1"]
167176

168177
# Unified entrypoint handles both Lambda and HTTP modes
169178
ENTRYPOINT ["/entrypoint.sh"]

‎Dockerfile.dev‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,16 @@ RUN if [ ! -f .air.toml ]; then air init; fi
5555

5656
EXPOSE 8080
5757

58-
# Create non-root user for security; grant ownership of app dir and Go paths
59-
RUN addgroup -S devuser && adduser -S devuser -G devuser && \
58+
# Create non-root user for security; grant ownership of app dir and Go paths.
59+
# uid/gid are pinned explicitly rather than left to `adduser -S`, which picks
60+
# the first free system id and so varies with the base image's existing users.
61+
# A numeric USER below needs a value that is known here, not discovered later.
62+
RUN addgroup -g 1001 devuser && \
63+
adduser -D -u 1001 -G devuser devuser && \
6064
chown -R devuser:devuser /app /root/go /root/.cache 2>/dev/null || true
6165

62-
USER devuser
66+
# Numeric form so the identity is resolvable without the image's /etc/passwd.
67+
USER 1001:1001
6368

6469
# Start with Air for hot reload
6570
CMD ["air", "-c", ".air.toml"]

‎Dockerfile.test‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ WORKDIR /e2e
1818

1919
COPY --chown=e2e:e2e tests/e2e/ ./
2020

21-
USER e2e
21+
# Numeric form so the identity is resolvable without the image's /etc/passwd.
22+
# 10001 is exactly the uid created above, so this is a rename only.
23+
USER 10001
2224

2325
# Pre-compile the test binary so `docker compose up` failures are runtime
2426
# failures, not compile errors discovered after the stack is already up.

0 commit comments

Comments
 (0)