Docker best practices
1. Pin Exact Image Tags
Why it matters:
The latest tag is a moving target. If you deploy postgres:latest today, it might be version 15. Later, a restart could pull version 16, potentially breaking your application due to backward-incompatible changes or database migrations that haven't run yet. This breaks the principle of Immutability.
Commentary:
While pinning a version number (e.g., node:18.16.0) is the standard best practice, you can get even stricter by pinning the SHA256 digest. Tags can technically be overwritten by the maintainer, but a hash digest is immutable.
Example:
❌ Bad Practice:
# Who knows what version this will be next week?
FROM python:latest✅ Good Practice:
# Explicitly states the version and OS flavor
FROM python:3.9.18-slim-bullseye🚀 Best Practice (Digest Pinning):
# Guaranteed to be the exact same bits, forever
FROM python@sha256:2419f18375...2. Combine RUN Commands
RUN CommandsWhy it matters:
Docker images are built as a series of read-only layers. Each RUN instruction commits a new layer. If you download a file in Layer A and delete it in Layer B, the file is hidden in the final image, but it is still physically present in Layer A, bloating the image size.
Commentary:
This is most critical when managing package managers (like apt or apk). You must update the repository, install the package, and clean up the cache in a single chain. If you separate them, the cache remains permanently in the intermediate layer.
Example:
❌ Bad Practice:
✅ Good Practice:
3. Use Multi-Stage Builds
Why it matters:
Production images should contain only what is necessary to run the app, not what is needed to build it. Tools like Go compilers, Java JDKs, C headers, and Maven/Gradle wrappers are heavy and increase the attack surface (hackers can use compilers to build malware inside your container).
Commentary:
This is the single most effective way to reduce image size. It allows you to use a heavy image (like maven) for the build and a tiny image (like alpine or distroless) for the runtime.
Example (Go Application):
4. Don't Run as Root
Why it matters:
By default, Docker containers run as root. If a hacker exploits a vulnerability in your application (e.g., Remote Code Execution) and breaks out of the container (container escape), they potentially gain root access to the host machine.
Commentary:
Using a non-root user adheres to the Principle of Least Privilege. Many modern orchestrators (like Kubernetes with strict Pod Security Policies) will refuse to run containers that attempt to run as root.
Example:
❌ Bad Practice:
✅ Good Practice:
5. Scan Images for Vulnerabilities
Why it matters:
You inherit the vulnerabilities of your base image. Even if your code is secure, the version of OpenSSL or glibc inside the container might have a critical CVE (Common Vulnerabilities and Exposures).
Commentary:
docker scout is the modern replacement for docker scan. It provides a quick view of CVEs. Other popular tools include Trivy and Grype. These should be automated in your CI/CD pipeline so that a build fails if high-severity vulnerabilities are found.
Example:
Command Line:
Typical Output:
2 vulnerabilities found
CVE-2023-1234 (High): openssl
CVE-2023-5678 (Medium): libxml2
6. Use a .dockerignore File
.dockerignore FileWhy it matters: When you run docker build, the first thing Docker does is send all files in your current directory (the "build context") to the Docker daemon. If you don't ignore unnecessary files, you might accidentally copy sensitive secrets (like .env files or AWS credentials) or massive local folders (like node_modules or .git history) into your production image.
Commentary: Think of .dockerignore exactly like .gitignore. It is a safety net. It ensures that even if you write COPY . . in your Dockerfile, the sensitive or heavy files on your laptop are physically blocked from entering the container image. This improves security and significantly speeds up the build process.
Example:
❌ Bad Practice (No exclusion): You have a local folder structure like this:
If you run COPY . . in your Dockerfile, your production image now contains your database passwords and your entire git history.
✅ Good Practice: Create a file named .dockerignore in the root directory:
Summary of these best practices:
Pin Exact Tags (Stability)
Combine RUN Commands (Size)
Multi-Stage Builds (Size & Security)
Don't Run as Root (Security)
Scan for Vulnerabilities (Security)
Use .dockerignore (Security & Speed)
This list effectively covers the lifecycle of a container from code to build to security.
Benefits of smaller images are:
Deploy faster
Cost less to store
Fewer security issues
Last updated