The container-level HEALTHCHECK directive, running as non-root, minimal base images, vulnerability scanning with tools like Trivy, and pinning base image versions explicitly — the checklist between a working image and a production-ready one.
Published September 23, 2026
This lesson is scoped specifically to the CONTAINER/IMAGE level — a companion, more narrowly-scoped set of concerns to the application-level Health Checks lesson in Observability & Operations, which covers liveness/readiness semantics from the running SERVICE's perspective. Here, the focus is what makes a Docker image itself production-ready.
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD curl -f http://localhost:8080/actuator/health || exit 1
Docker's own HEALTHCHECK instruction defines what "healthy" means for the container FROM DOCKER'S PERSPECTIVE — periodically running the specified command, and marking the container unhealthy if it fails the configured number of consecutive --retries. This is a different (and more limited) mechanism than Kubernetes' own liveness/readiness probes (Probes & Autoscaling) — in a Kubernetes deployment, Kubernetes' own probes typically take over this role entirely and the Dockerfile-level HEALTHCHECK becomes redundant; it matters most for plain Docker/Compose deployments without a full orchestrator.
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser # every subsequent instruction, and the container's own process, runs as this user
By default, a container runs as root unless told otherwise — meaning a security vulnerability in the application (or one of its dependencies) that allows arbitrary code execution runs with root privileges INSIDE the container. While container isolation limits (but doesn't eliminate) the practical impact of that, running as a dedicated non-root user is standard security hardening — a real, low-effort mitigation that meaningfully reduces the blast radius of a successful exploit, and is often an explicit requirement in security-conscious organizations' image-build standards.
FROM eclipse-temurin:21-jre-alpine # alpine: minimal Linux distro, small footprint
FROM gcr.io/distroless/java21-debian12 # distroless: no shell, no package manager at all
Alpine-based images are small (a minimal Linux distribution built around musl libc) but still include a shell and basic utilities. Distroless images go further — no shell, no package manager, nothing beyond the language runtime and the application itself. The security argument for distroless: even if an attacker achieves code execution inside the container, there's no shell to pivot to, no package manager to install additional tooling — a meaningfully smaller attack surface than even a minimal but still-general-purpose distro like alpine. The tradeoff: distroless images are harder to debug interactively (you can't docker exec into a shell that doesn't exist), which is a real operational cost worth weighing against the security benefit.
trivy image myapp:1.4.2
# scans every layer's installed packages against known CVE databases,
# reporting severity (CRITICAL/HIGH/MEDIUM/LOW) for each finding
Tools like Trivy (open-source, widely adopted) scan an image's installed packages and dependencies against known vulnerability databases, surfacing CVEs before an image ever reaches production. Integrating this as an automated CI pipeline step (failing the build on CRITICAL findings, per a team's own risk tolerance) turns vulnerability detection into a routine, automatic gate rather than something discovered later during a security audit or, worse, an actual incident — directly connecting to the CI/CD Pipeline Design lesson's "static analysis" pipeline stage.
FROM eclipse-temurin:21-jre-alpine # floating — 'latest patch of 21-jre-alpine', can change silently
FROM eclipse-temurin:21.0.4_7-jre-alpine # pinned — reproducible, identical build every time
This is the same underlying concern as Docker Fundamentals' latest-tag trap, applied one level deeper: even a version-looking tag like 21-jre-alpine is technically a MOVING target (it gets republished with security patches over time) — a build today and the identical build next month can silently pull different underlying bytes. For genuinely reproducible builds (the same Dockerfile producing bit-identical results regardless of when it's built), pinning to an exact, immutable version (or better, a content digest) is the strict correct practice — though many teams deliberately accept SOME floating (patch-level) tags specifically to pick up security patches automatically, a real, debatable tradeoff between reproducibility and staying current.
Q: If Kubernetes' own probes make Docker's HEALTHCHECK redundant, is there ever a reason to still define it?
A: It's still useful as a safety net for plain docker run usage (local testing, a non-Kubernetes deployment target) and as documentation directly in the image itself of what 'healthy' means — but yes, in a Kubernetes-orchestrated deployment, the Kubernetes-level probes are what actually drive traffic-routing and restart decisions, making the Dockerfile HEALTHCHECK mostly informational there.
Q: Does running as non-root fully eliminate the security risk of a container escape vulnerability? A: No — container isolation (namespaces, cgroups) is the primary defense against a container-to-host escape, and non-root is a defense-in-depth layer specifically limiting what an attacker can do WITHIN the container if they achieve code execution there; the two are complementary, not substitutes for each other.
Q: How would a team decide between alpine and distroless given the debugging tradeoff? A: A reasonable middle ground many teams adopt: distroless (or similarly hardened) images for actual production deployments, while keeping a debug-friendly image variant (with a shell, alpine-based) available for local development and troubleshooting — treating production hardening and developer convenience as separate concerns rather than forcing one image to serve both roles.
Q: Should vulnerability scanning block a deployment, or just warn? A: This is a real policy decision most teams tune deliberately — blocking on CRITICAL severity findings is common and defensible, but blocking on every MEDIUM/LOW finding risks the same alert-fatigue problem covered in Alerting Strategy, where an overly strict gate that blocks deployments for low-real-risk findings trains teams to routinely override or ignore it.