Skip to content
Docker Container Security Mistakes Web Developers Keep Making
All articles
Docker securitycontainer securityweb developmentDevOpsapplication security

Docker Container Security Mistakes Web Developers Keep Making

Common Docker container security mistakes web developers make, and the practical fixes that actually close the gaps before attackers find them.

WebSentry TeamOctober 22, 20265 min read

Docker made shipping web apps faster, but it also introduced a whole new attack surface that a lot of developers never properly lock down. A container that runs fine in production isn't the same thing as a container that's secure in production. Here's where things actually go wrong, and what to do about it.

Running Containers as Root

This is the single most common mistake. By default, if you don't specify otherwise, your container process runs as root inside the container, and if an attacker breaks out of your app (through a dependency vulnerability, a bad deserialization bug, whatever), they inherit root privileges on the container itself.

The Fix

  • Add a dedicated user in your Dockerfile: RUN addgroup -S appgroup && adduser -S appuser -G appgroup
  • Switch to it before the app starts: USER appuser
  • For Node.js images specifically, the official images already ship a node user, use it instead of creating your own
  • Verify with docker inspect | grep User to confirm it's not blank or "root"

Shipping Secrets Inside the Image

It's shockingly common to find API keys, database passwords, and .env files baked directly into image layers. Even if you delete the file in a later layer, it's still recoverable because Docker images are layered, not overwritten.

What To Do Instead

  1. Never COPY .env into an image. Add it to .dockerignore, not just .gitignore
  2. Use build-time secrets with BuildKit: docker build --secret id=mysecret,src=secret.txt
  3. Pass runtime secrets as environment variables injected at deploy time, or better, use a secrets manager (AWS Secrets Manager, Doppler, HashiCorp Vault)
  4. Scan existing images for leaked secrets with a tool like trufflehog or docker scout

Using "latest" or Bloated Base Images

Pulling node:latest or python:latest means you have no idea what you're actually running, and it changes under you every time you rebuild. These full images also carry hundreds of unnecessary packages, each one a potential CVE.

  • Pin specific versions: node:20.11.1-alpine instead of node:latest
  • Prefer Alpine or distroless variants where your app allows it, distroless images have no shell, no package manager, nothing for an attacker to use post-compromise
  • Use multi-stage builds so build tools and dev dependencies never make it into the final image

Example Multi-Stage Build

FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
USER node
CMD ["node", "dist/index.js"]

Ignoring Image Vulnerability Scanning

A huge number of teams build and deploy images without ever scanning them for known CVEs in the base image or dependencies. This is table stakes now, not optional.

  • Run docker scout cves or trivy image as part of your CI pipeline, not as an afterthought
  • Fail the build on critical/high severity findings, don't just log them and move on
  • Re-scan running images periodically, new CVEs get disclosed against old images constantly

Exposing the Docker Socket

Mounting /var/run/docker.sock into a container (common with CI runners, monitoring tools, or "Docker-in-Docker" setups) effectively gives that container root access to the host. If that container is compromised, the whole host is compromised.

  • Avoid mounting the socket unless absolutely necessary
  • If you must, use a proxy like docker-socket-proxy that limits which API calls are allowed
  • Never do this on a container that also handles untrusted input, like a web-facing app

Missing Resource Limits

Without CPU and memory limits, a single compromised or buggy container can exhaust host resources and take down every other service on the same machine. This is a denial-of-service vector a lot of developers don't think about until it happens.

  • Set limits in Compose: mem_limit: 512m, cpus: 0.5
  • In Kubernetes, always define resources.limits and resources.requests
  • Monitor actual usage with docker stats before guessing at numbers

Forgetting Network Isolation

Default bridge networks let every container on the host talk to every other container. A compromised frontend container shouldn't have direct network access to your database container.

  1. Create purpose-specific networks in Compose: separate your app tier, database tier, and cache tier
  2. Only expose ports that genuinely need to be public
  3. Use internal networks for service-to-service traffic and keep the database off any network with internet-facing exposure

Read-Only Filesystems and Dropped Capabilities

Most web apps don't need to write to their own filesystem at runtime, and almost none need the full set of Linux capabilities Docker grants by default.

  • Run containers with --read-only and mount specific writable volumes only where needed (like a tmp directory)
  • Drop unnecessary capabilities: --cap-drop=ALL --cap-add=NET_BIND_SERVICE if you only need to bind to a port
  • Add --security-opt=no-new-privileges to prevent privilege escalation inside the container

Container Security Is Only Half the Picture

Locking down your containers matters, but it doesn't help if the app serving traffic from that container is missing basic web security controls, weak TLS configuration, missing security headers, permissive CORS policies, or cookies without the right flags set. These are exactly the kinds of issues that sit outside the container boundary but still get exploited every day.

This is where a tool like WebSentry is useful as a complement to container hardening: it checks your live site's SSL setup, HTTP security headers, CSP, cookie configuration, DNS records, and CORS policy, then gives you an A-F grade so you can see what's actually exposed from the outside, regardless of how well the container itself is locked down.

If you've just gone through hardening your Dockerfiles and compose files, it's worth running a free scan at websentry.dev to check the parts of your security posture that container-level fixes don't touch, the headers, certificates, and policies your app actually sends to the browser.

Check your own site

Run a free security scan and see if your site has the issues covered in this article. Results in under 30 seconds.