intermediate~4h

Container Security: Non-Root, Secrets, Image Scanning & Distroless

Container security is layered defense-in-depth: who the process runs as inside the container (non-root), how secrets reach the container without being baked into the image or visible in plain env vars

Container security is layered defense-in-depth: who the process runs as inside the container (non-root), how secrets reach the container without being baked into the image or visible in plain env vars (secrets management), what's actually in your image (scanning for known CVEs), and how much attack surface the final runtime image even exposes (distroless/minimal images).

By default, a process inside a container that isn't given an explicit USER runs as root (UID 0) inside the container's user namespace. If that root maps to host root (the common case without rootless Docker or user namespace remapping), a container escape vulnerability becomes a host root compromise, not just a contained one.

Image scanning (Trivy, Grype, Docker Scout, Snyk) inspects installed packages and OS libraries against known CVE databases, surfacing vulnerabilities before an image ever reaches production — this should be a mandatory, automated CI gate, not a manual occasional check.

  • USER in a Dockerfile sets the UID/GID that the container's main process (and any RUN instructions after it) executes as; without it, the default is whatever USER the base image specifies, often root.

  • Even running as a non-root UID inside the container doesn't fully neutralize host risk unless user namespace remapping (userns-remap in daemon.json) is enabled, which maps container UID 0 to an unprivileged UID on the host — without it, container root and host root share the same UID space.

  • Docker secrets (in Swarm mode) and Compose-level secrets: mount sensitive files into the container at runtime as in-memory tmpfs files under /run/secrets/, never persisted to the image or visible in docker inspect environment variables — a major improvement over passing secrets via -e.

  • Plain environment variables for secrets are visible via docker inspect, in process listings (/proc/<pid>/environ), and often leak into logs/crash dumps/CI logs — this is why secret-handling is treated as a distinct, more careful problem than ordinary configuration.

  • Image scanners build a Software Bill of Materials (SBOM) — package name + version for every layer — and cross-reference it against vulnerability databases (NVD, OSV, vendor advisories) to flag known CVEs by severity, often blocking CI on Critical/High findings.

  • Distroless images (e.g., gcr.io/distroless/java) contain only the application and its runtime dependencies — no shell, no package manager, no coreutils — drastically shrinking both image size and the tools available to an attacker who gains code execution inside the container.

  • Choose a minimal base image (alpine or distroless) to minimize the pre-existing attack surface before you add anything.

  • In the Dockerfile, create a dedicated unprivileged user and group, then switch to it with USER before the final ENTRYPOINT — never run the main process as root.

  • Avoid putting secrets in ENV/ARG/Dockerfile literals; instead, design the application to read secrets from mounted files (Docker secrets, Kubernetes Secrets volumes) or fetch them at runtime from a vault (HashiCorp Vault, AWS Secrets Manager).

  • Integrate an image scanner (Trivy is the most common open-source choice) into the CI pipeline immediately after the docker build step, configured to fail the build on Critical/High severity findings.

  • Set the container's filesystem to read-only (--read-only at runtime, with explicit tmpfs/volume mounts for the few paths that genuinely need to be writable, like /tmp).

  • Apply resource limits (--memory, --cpus) so a single compromised or buggy container cannot exhaust host resources and affect co-located containers.

  • Java/Spring Boot teams switching their final-stage base image from eclipse-temurin:21-jre to a distroless/java21-debian12 or hardened Alpine-based equivalent to shrink both size and CVE count reported in scans.

  • Database credentials and API keys injected via Kubernetes Secrets (mounted as files) or Docker secrets in Swarm, rather than as Compose environment: entries, for any environment beyond pure local development.

  • Mandatory trivy image --severity CRITICAL,HIGH --exit-code 1 step in GitHub Actions/GitLab CI/ Jenkins pipelines, blocking merges that introduce newly-flagged vulnerabilities.

  • Security review checklists for Solution Architects: confirming non-root USER, scanning integration, secret handling, and minimal base image as standard sign-off criteria before approving a service for production deployment.

  • Always create a dedicated non-root user in the Dockerfile and switch to it with USER before the final ENTRYPOINT/CMD — this is one of the highest-impact, lowest-effort security improvements available.

  • Never bake secrets into ENV, ARG, or COPY'd files inside the image — secrets belong in runtime-mounted files, orchestrator-native secret objects, or external vaults fetched at startup.

  • Run automated image scanning on every build in CI, not just periodically — vulnerabilities are discovered continuously, and a clean scan today doesn't mean a clean scan next week for the same image.

  • Prefer distroless or minimal hardened base images for production runtime stages — fewer installed packages means fewer CVEs to track and a smaller toolkit available to an attacker post-compromise.

  • Drop all Linux capabilities by default (--cap-drop=ALL) and add back only the specific ones the application genuinely requires.

  • Running every container as root 'because it's easier' and never revisiting it before production — the single most common container security gap in real audits.

  • Passing database passwords and API keys via docker run -e or Compose environment: directly, exposing them in docker inspect, shell history, and CI logs.

  • Treating a one-time manual scan as sufficient, rather than wiring scanning into CI so every new build is checked against the latest vulnerability database.

  • Assuming Alpine's smaller size automatically means fewer vulnerabilities — size and CVE count are correlated but not identical; always verify with an actual scan rather than assuming.

  • Forgetting that a container running as root inside the container is still root on the host's UID space unless user namespace remapping is explicitly configured — 'just non-root inside the container' is necessary, not sufficient, for full isolation.

  • Smaller, scanned, minimal images pull faster and start faster in CI/CD pipelines and orchestrators performing frequent rolling deployments — security minimalism and performance optimization point the same direction here.

  • Cache scanner vulnerability databases between CI runs (Trivy supports a local DB cache) to avoid re-downloading multi-hundred-megabyte CVE databases on every pipeline execution.

  • Integrate scanning results into your deployment gate: block promotion to production on any new Critical/High severity finding, with a documented, time-boxed exception process for cases that genuinely can't be remediated immediately.

  • Use a centralized secrets manager (HashiCorp Vault, AWS Secrets Manager, Kubernetes External Secrets Operator) rather than ad hoc per-environment secret files, so rotation and access auditing are consistent across services.

  • Periodically re-scan images already deployed in production, not just at build time — a previously clean image can become vulnerable as new CVEs are published against its dependencies.

  • Combine non-root USER with a read-only root filesystem and explicit writable mounts (tmpfs for /tmp, a volume for any genuinely required persistent writable path) as the default production container profile.

  • Take an existing Dockerfile that runs as root and add a non-root USER; verify with docker inspect --format='{{.Config.User}}'.

  • Run trivy image against a popular public image (e.g., an older tagged version of a base image) and review the severity breakdown of findings.

  • Convert a Compose service's secret from a plain environment: entry to a secrets: file-based mount and verify it appears under /run/secrets/ instead of env.

  • Compare the size and trivy image CVE count of the same application built on eclipse-temurin:21-jre vs gcr.io/distroless/java21-debian12.

  • Run containers as a dedicated non-root user; root-in-container often equals root-on-host without namespace remapping.

  • Never deliver secrets via plain environment variables — use mounted secret files or an external vault.

  • Wire image scanning (Trivy, Grype, Docker Scout) into CI as a blocking gate on Critical/High CVEs, on every build.

  • Prefer distroless/minimal base images for production runtime stages to shrink both size and attack surface.

  • Layer defenses: non-root + read-only filesystem + dropped capabilities + scanning + secrets management + network segmentation.

Want a visual for this concept?

Generate a diagram tailored to “Container Security: Non-Root, Secrets, Image Scanning & Distroless” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.

Sign in to generate a visual →

Practice quiz

Next Step

Continue to Monitoring: Logs, Metrics, Prometheus & Grafana← Back to all Docker chapters