Safeguard
Containers

Docker Security Issues and How to Fix the Most Common Ones

Most Docker security issues trace back to a handful of predictable mistakes: bloated base images, root containers, and secrets baked into layers. Here is how to find and fix them.

Marcus Chen
DevSecOps Engineer
6 min read

Most Docker security issues are not exotic container escapes; they are ordinary misconfigurations that ship straight to production because nobody looked. A container inherits every problem in its base image, every secret you accidentally left in a layer, and every privilege you granted the runtime. Fixing these is less about clever tooling and more about defaults that fail safe.

This guide walks through the categories that actually cause incidents, with the commands and Dockerfile changes to address each.

Vulnerable base images

The single largest source of Docker security issues is the base image. When you write FROM node:18, you inherit hundreds of OS packages, most of which your app never calls but all of which count against your attack surface. A stale debian:bullseye layer can carry openssl or glibc CVEs for months.

Two habits help. First, prefer minimal images: node:18-slim, python:3.12-slim, or distroless images that ship no shell or package manager. Second, scan the image you actually build, not the tag you think you pulled:

docker build -t myapp:latest .
trivy image myapp:latest

Trivy, Grype, and Docker Scout all report OS and language-package CVEs. The point is to scan on every build in CI so a newly disclosed vulnerability in a pinned dependency fails the pipeline rather than surfacing in a pen test.

Running containers as root

By default a container process runs as UID 0. If an attacker exploits your app, root inside the container plus a kernel bug or a careless bind mount can mean root on the host. There is rarely a reason for this.

Add a non-root user in the Dockerfile and drop to it:

RUN addgroup --system app && adduser --system --ingroup app app
USER app

At runtime, reinforce it and strip Linux capabilities you do not need:

docker run --user 1000:1000 \
  --cap-drop=ALL --cap-add=NET_BIND_SERVICE \
  --read-only --security-opt=no-new-privileges myapp:latest

--read-only mounts the root filesystem read-only, which blocks a whole class of tampering, and no-new-privileges stops setuid binaries from escalating.

Secrets baked into image layers

A classic mistake: COPY .env . or ARG API_KEY followed by using that value at build time. Docker layers are content-addressed and cached, so a secret written into any layer stays recoverable with docker history or by extracting the tarball, even if a later layer deletes the file.

Never bake secrets into images. Use BuildKit secret mounts for build-time credentials, which never persist in a layer:

# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci

For runtime secrets, inject them through the orchestrator (Kubernetes Secrets, a vault sidecar, or environment variables set at deploy) rather than the image. Scan built images for leaked credentials with tools like trufflehog as a backstop.

The Docker daemon and socket exposure

The Docker daemon runs as root, and access to /var/run/docker.sock is effectively root on the host. Two patterns cause trouble here. Mounting the socket into a container (-v /var/run/docker.sock:/var/run/docker.sock) gives that container full control of the daemon, so a compromise of a CI runner or a monitoring agent becomes a host compromise. And exposing the daemon over TCP without TLS (-H tcp://0.0.0.0:2375) hands the host to anyone who can reach the port.

If you must grant Docker access, use a rootless daemon or a brokered proxy that whitelists specific API calls. Treat any container with the socket mounted as a privileged trust boundary.

Overly permissive networking and volumes

Bind-mounting host paths (-v /:/host) or running with --privileged collapses the isolation Docker is supposed to give you. --privileged disables seccomp, AppArmor, and capability restrictions all at once; it is almost never justified outside of building images or running nested Docker in controlled CI.

Be deliberate about published ports, too. -p 5432:5432 on a database container binds to all host interfaces by default. Bind to localhost (-p 127.0.0.1:5432:5432) unless the service genuinely needs external reach, and put internal services on a user-defined bridge network so they talk to each other without exposing ports at all.

Unpinned tags and supply chain drift

FROM ubuntu:latest means your build is not reproducible and can silently pull a different image tomorrow. Pin by digest for anything that matters:

FROM ubuntu:24.04@sha256:<digest>

Beyond the base image, the dependencies your app installs are their own supply chain. A container image is only as trustworthy as the packages layered on top, and vulnerabilities in transitive dependencies do not show up in the Dockerfile at all. Generating an SBOM for each image and tracking it over time closes that gap; an SCA tool such as Safeguard can flag a vulnerable transitive package inside an image before it reaches a registry. Signing images with cosign and verifying signatures at deploy time prevents tampered images from running.

A practical hardening checklist

Roll the fixes above into a baseline every image should meet:

  • Minimal or distroless base image, pinned by digest
  • Non-root USER, dropped capabilities, no-new-privileges
  • Read-only root filesystem where feasible
  • No secrets in layers; BuildKit secret mounts for build-time creds
  • Image and SBOM scanned in CI, build fails on critical CVEs
  • No socket mounts or --privileged unless justified and reviewed
  • Ports bound to localhost unless external access is required

None of this requires a new platform. It requires making the safe option the default in your Dockerfiles and CI templates so that developers get hardened containers without thinking about it. If you want to go deeper on scanning workflows, the Safeguard Academy has hands-on material.

FAQ

What is the most common Docker security issue?

Running containers as root combined with vulnerable base images. Both are defaults you have to opt out of, so they show up constantly. Adding a non-root USER and switching to a slim or distroless base removes the two biggest risks with a few lines of Dockerfile.

Does scanning an image catch all Docker security issues?

No. Image scanning finds known CVEs in OS and language packages, which is essential but partial. It does not catch misconfigurations like --privileged, socket mounts, exposed ports, or secrets you injected at runtime. Pair scanning with runtime and configuration checks.

Is --privileged ever safe to use?

Rarely. It disables seccomp, AppArmor, and capability limits at once, so a container breakout becomes trivial. Legitimate uses are narrow, such as building images inside a controlled CI job. For normal workloads, grant only the specific capabilities you need with --cap-add.

How do I keep secrets out of Docker images?

Use BuildKit secret mounts for anything needed at build time, since those never persist in a layer, and inject runtime secrets through your orchestrator or a vault rather than the image. Scan built images with a secret-detection tool as a safety net.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.