The Container Security Landscape Every Developer Should Understand

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:
- They run as root.
- They rely on unverified or moving base images.
- They include build tooling in production. (compilers, package managers, shells)
- They ship more than necessary—extra packages, language runtimes, package caches.
- 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:
- Build image in CI
- Run image scan
- Gate on severity thresholds (be strict on critical/high; handle medium thoughtfully)
- Publish scan artifacts (so teams can trace what was scanned and when)
- 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
.envfiles into the image - Using
ARGorENVfor 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 installorpip installwith 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.