If you write Dockerfiles, you’re already in the blast radius. Most teams treat container security like an operations problem—something for the platform team to “deal with.” But the truth is sharper: the image you build is the product you ship, and every security shortcut you take in the Dockerfile becomes an attack surface your users can’t afford.

Let’s walk through the container security landscape in a developer-first way—what’s commonly broken, why it matters, and how to fix it with practical, repeatable patterns.

Start with the reality: your Dockerfile decides the threat model

Container security isn’t magic tooling; it’s a chain of decisions:

  • What’s inside the image (packages, binaries, shells, utilities)
  • What runs by default (user, permissions, entrypoint behavior)
  • What gets baked in during build (secrets, compilers, debug tools)
  • Where dependencies come from (base images, package repos, build scripts)
  • What’s exposed (ports, network permissions, filesystem write access)

Most insecure images share a few predictable traits:

  1. They run as root.
  2. They rely on unverified or moving base images.
  3. They include build tooling in production. (compilers, package managers, shells)
  4. They ship more than necessary—extra packages, language runtimes, package caches.
  5. They lack scanning in the build pipeline, so vulnerabilities survive until runtime.

These aren’t “ops mistakes.” They’re developer workflow mistakes.

A good mental model: an image is immutable, but your build process isn’t. The build process determines what immutability preserves.

The biggest easy win: stop running as root

Running as root is a top-tier convenience tax. It usually happens because it “just works” during local development, and it tends to be copy-pasted into production.

Instead, treat non-root as a baseline requirement.

A practical Dockerfile pattern

For many apps, you can do something like:

  • Create a dedicated user/group
  • Ensure the application directory is owned by that user
  • Switch to that user
  • Avoid setuid surprises

Example (Node.js-style, but the pattern generalizes):

  • Use a non-root user like appuser
  • Copy only what you need
  • Ensure file permissions are correct before USER appuser
FROM node:20-slim

WORKDIR /app

# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser

# Install dependencies (keep it minimal)
COPY package*.json ./
RUN npm ci --omit=dev

# Copy application
COPY . .

# Fix ownership
RUN chown -R appuser:appuser /app

USER appuser

EXPOSE 3000
CMD ["node", "server.js"]

Two developer tips here:

  • Don’t rely on Kubernetes SecurityContext alone. It’s a safety net, not a replacement for correct image defaults.
  • Test permissions locally. If your app needs to write to a directory (logs, temp files), create and chown those paths explicitly.

If you’re migrating an existing app, expect failures around filesystem write. Fix them directly in the image rather than granting root privileges “temporarily.”

Reduce attack surface with multi-stage builds and minimal runtimes

A common anti-pattern is “install everything once, keep it forever.” Build tools shouldn’t ship in production—yet many images do exactly that: compilers, debuggers, and package managers included “just in case.”

Multi-stage builds: the developer-native security tool

Multi-stage builds let you compile/build in one stage and copy only runtime artifacts into the final image. That shrinks both the image size and the reachable tools an attacker could leverage.

For example, a typical approach for compiled languages:

  • Stage 1: build your binary (include build dependencies)
  • Stage 2: copy the binary into a lightweight runtime image

Even if you don’t adopt distroless immediately, multi-stage builds are a massive improvement because they reduce what an attacker gets.

Why “less” matters beyond size

Fewer packages means fewer known vulnerabilities, fewer ways to execute code, and fewer shells/utilities for post-exploitation. It also improves your scanning signal—fewer components, fewer false leads.

Prefer distroless (or at least slim) and be deliberate about base images

Your base image is the foundation of your security posture. If you use an outdated base or a constantly changing tag, you inherit risk without noticing.

Use pinned digests, not just tags

Tags like latest and even semver tags can move over time. For deterministic builds, prefer pinning by digest (the immutable identifier for an image).

Instead of:

  • FROM python:3.12-slim

Consider:

  • FROM python@sha256:<digest>

This doesn’t make vulnerabilities disappear, but it makes your build reproducible and your upgrades intentional.

Distroless: fewer utilities, fewer surprises

Distroless images generally omit shells and package managers, which is exactly what you want for production. Attackers rely on tooling; distroless reduces that capability dramatically.

The tradeoff is operational friction: debugging “inside the container” is harder. That’s a fair cost. Debugging should be done via logs, metrics, traces, and local reproduction—not by opening a shell in production.

A practical compromise many teams adopt:

  • Development: full-featured images for convenience
  • CI/testing: minimal images for early feedback
  • Production: distroless (or very slim) with a hard non-root baseline

Make scanning part of the build, not a report card

If you run vulnerability scanning only after deployment, you’re creating a delayed feedback loop. Developers need fast, actionable signals when the Dockerfile and dependencies are still editable.

Two tools commonly used for scanning are:

  • Trivy (often favored for simplicity and Docker-native workflows)
  • Snyk (useful in ecosystems with strong dependency management features)

Regardless of tool choice, the principle is the same: scan the built image and fail fast on serious issues.

What “baseline” looks like in practice

A reasonable workflow:

  1. Build image in CI
  2. Run image scan
  3. Gate on severity thresholds (be strict on critical/high; handle medium thoughtfully)
  4. Publish scan artifacts (so teams can trace what was scanned and when)
  5. Create remediation paths (upgrade base image, rebuild with updated dependencies, or adjust package selection)

Also: treat scanning output as a starting point, not an oracle. For example, a vulnerability in a package you don’t actually include is irrelevant—but an image with build tools and shells makes “maybe exploitable” a more dangerous category. That’s why minimizing the image matters before you even argue about scan results.

Secrets and build-time tooling: the silent security footguns

Containers are often blamed for runtime vulnerabilities, but a more subtle risk is secrets baked into images during build.

Common mistakes:

  • Copying .env files into the image
  • Using ARG or ENV for credentials
  • Leaving package manager caches or build artifacts that contain tokens
  • Running build steps with tools that persist into the final stage

Practical rules of thumb

  • Never copy secret files into the image context. Use proper secret injection mechanisms in your CI system.
  • Use multi-stage builds so build-time tooling and artifacts don’t persist.
  • Don’t run npm install or pip install with dev/test extras unless you truly need them at runtime.
  • Clean up caches if your build pipeline or base image leaves them behind (and again: multi-stage is cleaner than cleanup heroics).

If you must fetch private dependencies, do it in a way that doesn’t leave credentials in layers. The simplest posture is to ensure credentials don’t make it past the build stage—and ideally never into the final layer graph.

OCI and ecosystem-agnostic security: your Dockerfile travels

One reason this topic feels messy is that people conflate “security on Docker” with “security on Kubernetes.” The good news is that modern container standards push toward consistency.

The OCI image specification makes the image format ecosystem-agnostic: your build output isn’t tied to a single platform. That means better Dockerfile practices—non-root defaults, minimal layers, pinned bases, clean runtime artifacts—carry forward regardless of whether you run on Kubernetes, Nomad, or another compatible runtime.

In other words, you’re not just improving one deployment. You’re improving a portable artifact.

Conclusion: treat image authoring as part of secure engineering

Container security isn’t an ops afterthought. It’s a developer responsibility embedded in the Dockerfile, the build pipeline, and the dependency choices you automate.

If you remember nothing else, adopt these baselines:

  • Run as non-root
  • Use multi-stage builds to keep build tooling out of production
  • Prefer minimal or distroless images
  • Pin base images by digest for reproducibility
  • Scan images in CI and fail fast
  • Ensure secrets never make it into image layers

Write fewer “convenient” layers. Ship fewer utilities. Make your builds deterministic. You’ll reduce risk immediately—and you’ll make the rest of your security tooling far more effective.