Short answer
- Secure Docker containers by running them as a non-root user declared with USER in the Dockerfile, without --privileged and with --cap-drop ALL, adding back only the capabilities you strictly need.
- Never mount /var/run/docker.sock into a container, because access to the socket is equivalent to root on the host.
- Use minimal images pinned by digest, scanned with Trivy or Grype in CI and run with a read-only filesystem and resource limits.
- Audit the setup periodically with docker-bench-security, which automates the CIS Docker Benchmark, and move to rootless Docker where you can.
- Configuration reference
- CIS Docker Benchmark, automated by docker-bench-security
- User inside the container
- non-root, declared with USER in the Dockerfile
- What you never mount
- /var/run/docker.sock
- Runtime flags
- --cap-drop ALL --read-only --security-opt no-new-privileges
Why a root container is root on the host
Docker isolates processes with namespaces and cgroups, but the kernel is shared. If the container process runs as root and someone finds a kernel vulnerability or a permissive setting, escaping the container leaves them root on the server. --privileged switches off almost all isolation. Mounting the Docker socket is equivalent: whoever talks to the daemon can start a privileged container with the host's root filesystem mounted. So the image declares an unprivileged user, and the container starts with capabilities dropped and no-new-privileges.
Minimal, pinned and scanned images
An image based on a full distribution brings hundreds of packages and their vulnerabilities, although the application uses a few libraries. Choose minimal images, alpine, slim or distroless, and build multi-stage so compilers stay in the build stage. Pin the base image by digest rather than by tag, and rebuild when the scanner reports a patch. Scanning runs in CI before the push to the registry; an image with critical vulnerabilities does not reach production. Do not put secrets in the image or run npm or pip in the production container.
- FROM node:22-alpine@sha256:... instead of FROM node:latest
- Limits: --memory, --cpus, --pids-limit, so one container cannot choke the host
- One Docker network per application; the database is not on the public network
Security controls for Docker containers in production| Control | How you apply it | What it prevents |
|---|
| Non-root user | USER app in the Dockerfile | Root in the container = root on the host after an escape |
|---|
| No unneeded capabilities | --cap-drop ALL, --cap-add only what is needed | Escalation through kernel capabilities |
|---|
| No new privileges | --security-opt no-new-privileges | Escalation through setuid binaries |
|---|
| Read-only filesystem | --read-only, --tmpfs /tmp | Writing backdoors inside the container |
|---|
| Docker socket not mounted | No -v /var/run/docker.sock | Full control over the daemon |
|---|
| Scanned and pinned images | Trivy in CI, FROM by digest | Known vulnerabilities in production |
|---|
Commands and configuration
Multi-stage Dockerfile with a non-root userFROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --ignore-scripts
COPY . .
RUN npm run build && npm prune --omit=dev
FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build --chown=node:node /app/dist ./dist
COPY --from=build --chown=node:node /app/node_modules ./node_modules
USER node
HEALTHCHECK CMD wget -qO- http://127.0.0.1:3000/health || exit 1
CMD ["node", "dist/server.js"]
Running with hardened isolationdocker run -d --name app \
--read-only --tmpfs /tmp \
--cap-drop ALL \
--security-opt no-new-privileges \
--memory 512m --cpus 1 --pids-limit 200 \
--network app-net \
-p 127.0.0.1:3000:3000 \
example.ro/app@sha256:<digest>
Scanning and the CIS audittrivy image --severity HIGH,CRITICAL --exit-code 1 example.ro/app:1.4.0
# The CIS audit is the one exception to the 'never mount the socket' rule: run it once,
# by hand, not as a permanent service. Without the mounts below, CIS sections 1-3
# report wrongly or are skipped. `:ro` on a Unix socket limits nothing.
docker run --rm --net host --pid host --userns host --cap-add audit_control \
-v /var/lib:/var/lib:ro -v /etc:/etc:ro \
-v /usr/bin/containerd:/usr/bin/containerd:ro -v /usr/bin/runc:/usr/bin/runc:ro \
-v /usr/lib/systemd:/usr/lib/systemd:ro \
-v /var/run/docker.sock:/var/run/docker.sock docker/docker-bench-security
Practical examples
SaaS team in Iasi with a Node.js API in Docker
The image ran as root because nobody had added USER. A vulnerability in an image-processing library allowed files to be written anywhere in the container. Switching to the node user and a read-only filesystem confined the next incident to the temporary directory.
Web agency in Timisoara with monitoring in a container
A monitoring container had /var/run/docker.sock mounted so it could see the other containers. In effect it was root on a host running ten client sites. It was replaced by a read-only socket proxy, and direct mounting was banned.
Common mistakes
- Assuming containers are isolated by default, when run as root or with --privileged they are root on the host.
- Mounting /var/run/docker.sock into a container “just for monitoring”.
- Using FROM image:latest and not knowing which version runs in production.
- Exposing the Docker API over TCP without TLS, or publishing ports with -p and no address, which bypasses the firewall.
How 4hosting helps
- VPS plans are ready for Docker and Kubernetes; Docker & Kubernetes designs the container architecture, the separate networks and the rollouts.
- Hardening & Compliance includes container hardening against the CIS Docker Benchmark; CI/CD & Monitoring adds image scanning and centralises logs in Grafana, Prometheus and Loki.
- The DevOps hours included each month, 1–8 on VPS, 5–10 on dedicated servers depending on the plan and 10 on GPU servers, are requested from the client panel, in the “DevOps Services” section; a docker-bench-security run with fixes usually fits inside them.
Related questions
Does rootless Docker work with every application?
With most of them. Limits appear with ports below 1024, which need extra configuration, with some network and storage drivers and with applications that need kernel capabilities. Test in staging; if the application runs, you gain a layer of isolation without changing code.
How do I apply security patches in containers?
You do not update inside the container; you rebuild the image. Change the base image digest, run the build and scan in CI again and roll out the new version. Containers are immutable: an apt upgrade inside a running container disappears at the next restart.
Do I have to move to Kubernetes to be safe?
No. Every control above applies to plain Docker and to Compose. Kubernetes adds admission policies, managed secrets and network isolation at scale, but also a far bigger configuration surface. Choose it when you need orchestration, not for security in itself.