2026年11月14日
2026年11月14日
Docker Security Best Practices: Images, Runtime, Daemon, Network
This article is a localized edition of the Japanese original. Read the Japanese original
Docker containers run on the host's shared Linux kernel. They are not isolated as strongly as virtual machines, so if you run them with default settings, as root, with broad privileges, a vulnerability inside a container can lead to a takeover of the host.
This article explains the settings worth getting right when you run Docker on a server, from these five angles:
- Keep images small and current, and scan them for vulnerabilities
- Run containers as a non-root user in the Dockerfile
- Restrict privileges and resources with
docker run - Docker daemon settings (
daemon.json) - Protecting published ports, the firewall and the Docker API
Each setting was checked against the official Docker documentation as of November 2026, and we ran containers on Docker Engine 29.9 to confirm that it takes effect.
Common dangerous setups
- An old base image is still in use, carrying known vulnerabilities (CVEs)
- The process inside the container runs as root and mounts the host's
/or/var/run/docker.sock - A port published with
-p 8080:80is reachable from outside even though you thought the firewall closed it - The Docker API (port 2375) is exposed to the network without authentication. Exposed APIs are known to be abused to create containers for things like cryptocurrency mining
Step 1: Use small, current base images and scan them
The fewer packages an image contains, the fewer targets for vulnerabilities. Choose the slim or alpine variants of official images, and specify the version in the tag.
# Use a slim variant with an explicit version
docker pull python:3.12-slim
# Scan for HIGH and CRITICAL vulnerabilities with Trivy
trivy image --severity HIGH,CRITICAL python:3.12-slim
# Docker Scout can scan too
docker scout cves python:3.12-slim
A tag such as latest can point to different content later. In production, write the version as in python:3.12-slim, or pin it with a digest (python:3.12-slim@sha256:…). Once pinned, rebuild regularly as the base image is updated and rescan. If you pin and forget, already-fixed vulnerabilities stay in your image.
Scanning is explained in detail in Scanning Docker images for vulnerabilities with Trivy and Snyk.
Step 2: Use a non-root user in the Dockerfile
Without USER, the process inside the container runs as root. Create a dedicated user and switch to it.
FROM node:22-alpine
RUN addgroup -S app && adduser -S app -G app
WORKDIR /app
# Keep app files owned by root so the runtime user cannot modify them
COPY --chown=root:root server.js .
USER app
EXPOSE 8080
CMD ["node", "server.js"]
docker build -t myapp:1.0 .
docker run --rm myapp:1.0 id
If a non-root user such as uid=100(app) is shown, the switch worked (this is the output from our test environment). If the code is owned by the runtime user, an attacker who gets in can rewrite the app itself, so give the runtime user only the directories it must write to.
Non-root users cannot listen on ports below 1024, so have the app listen on a port such as 8080 and remap it on the published side, as in -p 80:8080. For details, see How to run Docker containers as a non-root user.
Step 3: Restrict privileges and resources with docker run
docker run -d --name web \
--cap-drop=ALL \
--read-only --tmpfs /tmp \
--security-opt no-new-privileges:true \
--pids-limit 100 --memory 256m \
-p 127.0.0.1:8080:8080 \
myapp:1.0
| Option | Effect |
|---|---|
--cap-drop=ALL |
Removes all Linux capabilities (root's powers split into fine-grained units). Add back only what you need with --cap-add |
--read-only --tmpfs /tmp |
Makes the container's file system read-only and leaves only /tmp writable for temporary files |
--security-opt no-new-privileges:true |
Prevents privilege escalation through setuid programs and the like |
--pids-limit, --memory |
Caps the number of processes and the memory, so a runaway process or fork bomb cannot bring down the host |
-p 127.0.0.1:8080:8080 |
Publishes only on the host's 127.0.0.1 (when a reverse proxy in front uses it) |
In our test, a container with these settings had zero effective capabilities, NoNewPrivs was 1, writes to /app were refused with "Read-only file system," and /tmp was writable. For an app that listens on port 1024 or above, such as 8080, there is no need to add back NET_BIND_SERVICE.
The following options greatly weaken the isolation between container and host. Use them only when there is a clear reason, and record why you use them.
--privileged: grants almost all privileges and access to devices--pid=host,--network=host: shares the host's processes or network directly-v /var/run/docker.sock:/var/run/docker.sock: allows controlling the Docker daemon. This is equivalent to handing over root on the host
When a socket mount is needed and what to use instead are covered in How to handle the Docker socket safely. For resource limits, see also Resource limits for Docker containers.
Step 4: Configure the Docker daemon (daemon.json)
/etc/docker/daemon.json sets defaults that apply to every container. If the file already exists, add these keys instead of overwriting it.
{
"live-restore": true,
"userland-proxy": false,
"no-new-privileges": true,
"icc": false,
"log-driver": "json-file",
"log-opts": {"max-size": "10m", "max-file": "3"}
}
| Key | Meaning |
|---|---|
live-restore |
Keeps containers running while the daemon restarts. Cannot be combined with Swarm mode |
userland-proxy |
When false, port forwarding is done with iptables instead of a user-space proxy |
no-new-privileges |
Applies no-new-privileges to all containers by default |
icc |
When false, stops container-to-container traffic on the default bridge network (has no effect on networks you create yourself) |
log-opts |
Caps log file size and the number of rotated files, so logs cannot fill the disk |
Check the syntax of the configuration file before applying it.
sudo dockerd --validate --config-file /etc/docker/daemon.json
sudo systemctl restart docker
If configuration OK is shown, the syntax is correct (the configuration above was checked this way). If live-restore is disabled, restarting the daemon also stops running containers, so pick your maintenance window with care.
For stronger isolation, there is rootless mode, which runs the Docker daemon itself as a regular user, and userns-remap, which maps root inside the container to a regular user on the host. Both change volume ownership and which features are available, so check the constraints in the official documentation before trying them.
Step 5: Published ports and the firewall
The official Docker documentation warns about the following when publishing ports:
- If you publish without an IP, as in
-p 8080:80, the port is published on all of the host's addresses - Traffic to published ports is forwarded by Docker's rules before it passes through UFW's settings, so it gets in from outside even if you thought UFW had closed it
Countermeasures:
- For containers that do not need to be directly visible from outside (databases, admin panels, apps behind a reverse proxy), publish on 127.0.0.1, as in
-p 127.0.0.1:8080:80. Do not publish databases with-pat all; connect to them only through Docker networks - To make 127.0.0.1 the default publish address, add
"ip": "127.0.0.1"todaemon.json(it applies to the default bridge network) - Check from another machine whether a published port is reachable from outside
# Check which addresses and ports the host is listening on
sudo ss -tlnp
Step 6: Keep the Docker API off the network
If you expose the Docker API over TCP (port 2375) without authentication, anyone on the network can create containers and mount the host's files. The rule is not to expose it.
# Check that nothing is listening on ports 2375 or 2376
sudo ss -tlnp | grep -E ":2375|:2376"
If nothing is printed, Docker is not listening over TCP. To control Docker from another machine, connecting over SSH is the simple way.
# From your machine, control Docker on a server you can log in to over SSH
docker -H ssh://username@server-address ps
If you have no choice but to expose it over TCP, follow the official "Protect the Docker daemon socket" guide to set up mutual TLS authentication with client certificates on port 2376, and also restrict the source addresses in the firewall. For details, see Setting up secure remote access to the Docker API.
Also note that users in the docker group can do the equivalent of root on the host without sudo. Add only administrators to that group.
Handling secrets
- Passwords or API keys written into a Dockerfile with
ENVremain in the image history. Values passed withdocker run -eare also visible throughdocker inspect. - Use Docker Compose
secrets(files mounted under/run/secrets/), Swarm's Docker secrets, or an external secrets manager, so secrets do not end up in images or environment variables. - Secrets needed only at build time can be passed with
docker build --secret, which keeps them out of the image.
Summary
- Keep images small with pinned versions, and rebuild and rescan them regularly.
- Write
USERin the Dockerfile and make--cap-drop=ALL,--read-only,no-new-privilegesand resource limits your baseline. Avoid--privilegedand mounting docker.sock. - In
daemon.json, set log limits,no-new-privilegesand similar options, and apply them only after checking withdockerd --validate. - Published ports bypass UFW, so publish containers you do not want exposed on 127.0.0.1, and operate the Docker API over SSH instead of exposing it over TCP.
Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)