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.
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:
docker run --rm alpine:3.24 id
docker run --rm --user 1000:1000 alpine:3.24 iduid=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=1000If an attacker breaks out of a container running as root, they are much closer to controlling the host. Add a user in the Dockerfile:
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 nonrootappA 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,-alpineor 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:
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy image python:3.13-slimdocker 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:
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"'touch: /app.txt: Read-only file system
tmp worksDrop 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:
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"'chown worked
chown: /f: Operation not permittedA 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:
docker run -v /var/run/docker.sock:/var/run/docker.sock some-toolOnly 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#
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/null1000
true [ALL] 50--pids-limit stops a fork bomb from filling the process table.
Lint your Dockerfile#
Hadolint checks a Dockerfile for common problems:
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-: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 -yCommon 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
docker run --rm --read-only --user 1000 alpine:3.24 sh -c 'touch /etc/test'touch: /etc/test: Read-only file systemKey takeaway#
Next: Building Images in CI/CD.
Finished this lesson?
Related lessons
Recommended Docker and Kubernetes resources
- Docker Deep Dive by Nigel Poulton
A practical guide to how Docker works, for readers who have finished the basics.
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.