Skip to content
Laviyx Learn

Try

Docker and Kubernetes course menu

Docker Security Best Practices

Secure your containers: run as non-root, use minimal and pinned images, scan for vulnerabilities, drop capabilities, use read-only file systems, and protect the Docker socket.

Advanced6 min readUpdated
On this page

Containers isolate apps well, but not perfectly, because they share the host's kernel. Good habits greatly reduce the risk if an app is ever attacked.

Do not run as root#

By default, a container's process runs as root:

Terminal
docker run --rm alpine:3.24 id
docker run --rm --user 1000:1000 alpine:3.24 id
Output
uid=0(root) gid=0(root) groups=0(root),0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)
uid=1000 gid=1000 groups=1000

If an attacker breaks out of a container running as root, they are much closer to controlling the host. Add a user in the Dockerfile:

Terminal
cat > Dockerfile <<'EOF'
FROM alpine:3.24
RUN addgroup -S app && adduser -S app -G app
USER app
CMD ["id", "-un"]
EOF
docker build -q -t nonroot . > /dev/null
docker run --rm nonroot
Output
app

A non-root user cannot listen on ports below 1024 by default in many setups, so apps normally use a port such as 8080. For Nginx, use the nginxinc/nginx-unprivileged image.

Use small, trusted images#

  • Choose official or verified images.
  • Prefer -slim, -alpine or distroless variants.
  • Use multi-stage builds, so that compilers are not in the final image.
  • Pin versions, and rebuild regularly to get security fixes.

Every package you leave out is one less thing to patch.

Scan images#

Scanners compare an image's packages with lists of known vulnerabilities. Trivy is free and popular, and it can run as a container:

Terminal
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy image python:3.13-slim

docker scout cves image is Docker's own tool. Run a scanner in CI, and fail the build on serious problems.

Read-only file system#

If an app does not need to write to its image, make the file system read-only. Attackers then cannot easily add tools:

Terminal
docker run --rm --read-only alpine:3.24 sh -c 'touch /app.txt'
docker run --rm --read-only --tmpfs /tmp alpine:3.24 sh -c 'touch /tmp/ok && echo "tmp works"'
Output
touch: /app.txt: Read-only file system
tmp works

Drop capabilities#

Linux divides root's powers into capabilities. Docker keeps a smaller set by default, and you can remove more. Here root cannot change a file's owner, because CAP_CHOWN was dropped:

Terminal
docker run --rm alpine:3.24 sh -c 'touch /f && chown nobody /f && echo "chown worked"'
docker run --rm --cap-drop=ALL alpine:3.24 sh -c 'touch /f && chown nobody /f && echo "chown worked"'
Output
chown worked
chown: /f: Operation not permitted

A strong default is --cap-drop=ALL, adding back only what the app needs with --cap-add. Also use --security-opt no-new-privileges, which stops a process from gaining rights through setuid programs.

Never use --privileged casually#

--privileged removes nearly all of this isolation, and gives the container access to host devices. Treat it as giving root on the host.

Protect the Docker socket#

Mounting /var/run/docker.sock gives a container full control of Docker, and so of the host:

Terminal
docker run -v /var/run/docker.sock:/var/run/docker.sock some-tool

Only do this for tools that you trust completely. The same goes for membership of the docker group.

Keep secrets out#

Use run-time secrets, a good .dockerignore, and never ENV or ARG for passwords. Do not publish database ports.

A hardened example#

Terminal
docker run -d --name safe \
  --read-only --tmpfs /tmp \
  --cap-drop=ALL \
  --security-opt no-new-privileges \
  --user 1000:1000 \
  --memory=64m --pids-limit=50 \
  alpine:3.24 sleep 20 > /dev/null
docker exec safe id -u
docker inspect --format '{{.HostConfig.ReadonlyRootfs}} {{.HostConfig.CapDrop}} {{.HostConfig.PidsLimit}}' safe
docker rm -f safe > /dev/null
Output
1000
true [ALL] 50

--pids-limit stops a fork bomb from filling the process table.

Lint your Dockerfile#

Hadolint checks a Dockerfile for common problems:

Terminal
cat > Dockerfile <<'EOF'
FROM ubuntu:latest
RUN apt-get update
RUN apt-get install curl
EOF
docker run --rm -i hadolint/hadolint hadolint --no-color - < Dockerfile | cut -c1-74
Output
-:1 DL3007 warning: Using latest is prone to errors if the image will ever
-:2 DL3009 info: Delete the apt lists (/var/lib/apt/lists) after installin
-:3 DL3015 info: Avoid additional packages by specifying `--no-install-rec
-:3 DL3008 warning: Pin versions in apt get install. Instead of `apt-get i
-:3 DL3059 info: Multiple consecutive `RUN` instructions. Consider consoli
-:3 DL3014 warning: Use the `-y` switch to avoid manual input `apt-get -y

Common mistakes#

Practice#

Practice exercise

Run Alpine as user 1000, with a read-only file system, and try to write to /etc/test. Print the error.

Show solution
Terminal
docker run --rm --read-only --user 1000 alpine:3.24 sh -c 'touch /etc/test'
Output
touch: /etc/test: Read-only file system

Key takeaway#

Next: Building Images in CI/CD.

Finished this lesson?

Some links are affiliate links. If you buy through them, Laviyx Learn may earn a commission at no extra cost to you. As an Amazon Associate, we earn from qualifying purchases.