← 全部新闻
security5 分钟阅读

Docker security mistakes developers make in production

作者:Seaworthy · 2026年4月21日

Assorted shipping containers stacked at a dock, viewed from above

Photo by Harrison Broadbent on Unsplash

The container illusion

Containers feel secure. You have this isolated box, namespaces, cgroups, a whole separate filesystem. It feels like a VM. It is not a VM, and the sense of security it creates is genuinely dangerous when you stop interrogating it.

Here are the Docker mistakes that keep showing up in production codebases, from developers who know enough to containerise their app but not quite enough to do it safely.


Running as root

The default user inside a Docker container is root. That means if anything in your container gets exploited, the attacker has root privileges inside that process. Container escapes are rare but they happen, and when they do, you want the blast radius to be a low-privilege user, not root.

Fix it in your Dockerfile:

RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser
USER appuser

Do this near the end of your Dockerfile, after all the installation steps that actually need elevated privileges.


Bloated base images

FROM ubuntu:latest or FROM node:20 pulls in hundreds of packages your app will never use. Every one of those packages is an attack surface, and every one of them needs patching when CVEs drop.

Switch to distroless or slim variants:

# Before
FROM node:20

# After
FROM node:20-slim
# or for production builds with no shell at all:
FROM gcr.io/distroless/nodejs20-debian12

Smaller images also build and pull faster, which is a nice side effect. The main point is that you cannot exploit a curl binary that is not there.


Not pinning image digests

FROM node:20-slim sounds pinned. It is not. The tag can be silently updated and you will pull a different image on your next build. Use digest pinning in production:

FROM node:20-slim@sha256:a3b5c8d1e2f4...

This is verbose. It is also the only way to guarantee you are running exactly what you tested. Update digests deliberately, not accidentally, on every docker pull.


Secrets baked into layers

I have seen .env files copied into images. I have seen API keys set as ENV variables in Dockerfiles. Both of these end up in image layers, which means they end up in every image you push to your registry, readable by anyone with pull access.

# This bakes the secret into the image layer permanently
ENV DATABASE_URL=postgresql://user:supersecret@host/db
COPY .env .

Use build secrets for anything you need at build time:

RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm install

For runtime secrets, inject them via environment variables at container start, or use a secrets manager. The image itself should contain zero credentials.


No resource limits

An unconstrained container can exhaust all memory on a host and take down every other service running on it. This is a denial-of-service vector, and it costs you nothing to prevent:

# docker-compose.yml
services:
  api:
    image: myapp:latest
    deploy:
      resources:
        limits:
          memory: 512m
          cpus: "0.5"

Set limits that reflect what your application actually needs, not aspirational limits. If the container keeps hitting them, that is a signal to investigate, not to raise the limit.


Writable container filesystems

By default, a container's filesystem is writable. An attacker who gets code execution inside your container can write files, install tools, and make persistence harder to detect. Run with a read-only root filesystem and explicitly mount writable volumes only where needed:

docker run --read-only \
  --tmpfs /tmp \
  --tmpfs /var/run \
  myapp:latest

Your application might need a few writable paths. Mount those explicitly. Everything else should be immutable.


Exposing the Docker socket

Mounting /var/run/docker.sock into a container gives that container full control over the Docker daemon, which means full control over the host. This is a complete container escape by design.

# Do not do this unless you have an extremely good reason
volumes:
  - /var/run/docker.sock:/var/run/docker.sock

I genuinely cannot think of a production use case that justifies this risk. Monitoring agents that need it should use the Docker API over TCP with TLS, or be replaced with tools that do not need host-level daemon access.


Not scanning images for CVEs

Shipping an image without scanning it is the same as deploying code without running tests. You might be fine. You might not be. The difference is whether you chose to find out.

# Trivy is free and runs locally
trivy image myapp:latest

# Or use Docker Scout if you are already on Docker Hub
docker scout cves myapp:latest

Add scanning to CI so you find out about new CVEs before they reach production, not after.


The pattern here

None of these mistakes are exotic. They are all defaults, or convenience choices made under deadline pressure, or things that "felt fine" because the container was running correctly. The gap between "runs correctly" and "is secure" is where production incidents live.

Seaworthy's ops and security checks flag several of these patterns automatically when you run it against a repository, including exposed secrets, missing resource constraints, and privileged container configurations.

本文由 AI 生成,总结了公开可用的来源。

来自 Seaworthy 团队

Seaworthy 会扫描你的代码仓库,发现类似本文提到的各种问题。

安全漏洞、泄露的密钥和配置错误——在部署前捕获。免费运行,无需账户。

$ npx seaworthycode