Steps to check when using docker
- Keep image small
- Keep build fast
- Make build reproducible
- Improve security
- Improve usability
- Setup linting
- Deploy right
- Provide documentation
- See checklists for specific tools
Image size matters
But if you are using multi-stage builds, you should do steps below only in final stage. You can still reduce size of images of build stages, but it's not so important.
- Use alpine (except for python)
- Use multi-stage builds
- Use whitelist dockerignore
- Alternatively, use this dockerignore
- Reduce number of image layers
- Combine repeated commands (
RUN
with\ &&
,ENV
with\
, etc.)- Note: sometimes this is in conflict with build time
- Combine repeated commands (
Building image faster as well as running application in container faster
Build performance is really important, cause it provide better development experience and allow to speed up CI cycle.
- Place instructions that are less likely to change (and easier to cache) first
- Install dependencies first, then copy application sources (see documentation)
- Prefer exec form for
CMD
,ENTRYPOINT
andRUN
instructions - Place
ENV
instructions as late as possible
Making build process reproducible and error prune
- Make container stateless and universal
- Use
VOLUME
instruction to store state in volumes - Settings should be provided via environment variables when container is running
- Use
- Add healthchecks
- Ensure healthcheck exit code is either 0 (healthy) or 1 (unhealthy)
- Use autoheal
- Pin virtually all versions
- Explicitly specify base image version
- Use lock files
- See checklists for specific tools
- Use shell with additional flags to improve robustness
- See The Eeuo pipefail option and best practice to write a shell script
- Use
set -Eeuo pipefail
in all bash scripts (e.g. inENTRYPOINT
) - For bash:
SHELL ["/bin/bash", "-Eeuo", "pipefail", "-c"]
- For ash (alpine):
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
- Use CI tool to build and publish image
- Do not use
latest
tag, always explicitly tag images (see 1 and 2)
Security matters, and there are no excuses
Docker containers are quite secure itself, but there are vulnerabilities in docker daemon itself, that allow to escape attacks.
- Use trusted base images
- Build using unprivileged user (check out podman)
- Run final process from unprivileged user (Dockerfile)
- Consider using
--read-only
flag - Make secrets unavailable to unprivileged users on host system
Making image easier to use
- Expose used ports
- Prefer common, traditional ports
- Use port 8080 for http (see List of TCP and UDP port numbers)
- Add a development image
- Mount source directory to development container as running
- Enable debug mode, autoreload, increase log verbosity
Check your
Dockerfile
for errors automatically
- Use hadolint
- Run linting in CI pipeline
- Lint your
entrypoint
,healthcheck
and other scripts
Then it comes to deploy on the production
- Have single image for QA/Staging/Production
- Build image once and publish it to the registry
- Consider using a private registry
- Consider using watchtower to automate deploy
Make users (including future yourself) suffer less using your image
- Have a nice
README.md
file, like official images have - Update it automatically with dockerhub-description
- Add badges
- CI pipeline status
- Image size
- Layers count
- DockerHub stars
- DockerHub pulls
- DockerHub latest version
- Find more at shields.io and badgen.net.
- Define opencontainers annotations as labels
Check if you are using
docker run
correctly
- Use
--rm
,-it
- Specify name:
--name=app
- Make sure container will restart on failure:
--restart=<on-failure|always|unless-stopped>
- Prevent resource depletion
- Limit memory usage:
--memory=1G
(upper bound) and--memory-reservation=100M
(lower bound) - Limit CPU usage:
--cpus=0.5
/--cpus=16
- Set CPU usage priority:
--cpu-shares=512
(see documentation) - Set I/O priority:
--blkio-weight=100
(see documentation) - Configure log rotation globally or per-container
- Make sure you have good PID 1 (or use
--init
) to prevent zombie processes
- Limit memory usage:
docker-compose.yml
is the way to specify multiple services
- Specify compose version
- Service names as what they are, not what they use (e.g. database instead of postgres, api instead of fastapi)
- Configure services
- Pin image versions
- Configure log rotation for background services
- Limit resource usage if necessary via
mem_limit
- Split critical services from utility and limit cpu and IO usage
with
blkio_config.weight
andcpu_shares
- Split critical services from utility and limit cpu and IO usage
with
- Specify listen address for
ports
- Specify
restart
- Specify service dependencies (
depends_on
) - Use
read_only
if possible - Use yaml anchors to extract common parts
- Split special services
- Specify one-shot tasks in separate profile
- Specify debug tasks in separate profile
- Specify development compose
- Lint docker-compose files with
yamllint
Alpine package manager
- Use
--no-cache
key
Ubuntu / Debian package manager
- Update before installing:
apt-get update
- Use
--no-install-recommends
key - Use
--yes
key - Remove redundant state information:
rm -rf /var/lib/apt/lists/*
- Pin versions (
apt-get install <package>=<version>
)- Use
apt-cache madison <package>
to get available versions
- Use
Arch Linux package manager
WARNING: Arch Linux is not recommended as a base image
- Update system:
pacman -Syu
- Use
--noconfirm
option - Cleanup package cache after installation:
rm -rf /var/cache/pacman/pkg/*
- Use faulthandler:
PYTHONFAULTHANDLER=yes
- Disable output buffering:
PYTHONUNBUFFERED=yes
- Disable bytecode writing:
PYTHONDONTWRITEBYTECODE=yes
(if process is running not too often)
Python package manager
- Pin versions of all packages in
requirements.txt
(usepip freeze
) - Do not check for pip version on start:
PIP_DISABLE_PIP_VERSION_CHECK=yes
- Disable cache:
PIP_NO_CACHE_DIR=yes
- Use
PIP_DEFAULT_TIMEOUT=120
to preventConnectTimeoutError
Python package manager
- Pin version:
POETRY_VERSION=1.16.0
- Disable interactivity:
POETRY_NO_INTERACTION=true
- Install in recommended and secure way with checksum check:
curl -sSL https://install.python-poetry.org -o install-poetry.py
echo "$POETRY_HASHSUM install-poetry.py" | sha256sum --check
python3 install-poetry.py
- Store virtualenvs in project's root:
POETRY_VIRTUALENVS_IN_PROJECT=true
- Copy
.venv
dir from build stage to final - Remove
*.pyc
files from.venv
:RUN find /app/.venv -name '*.pyc' -delete
(this reduces image size by ~10%) - Remove
pip
,setuptools
andwheel
from.venv
- Remove
*.pyc
,ensurepip
,lib2to3
anddistutils
from final image - Do not copy to main image (or remove from builder image) poetry files:
pyproject.toml
andpoetry.lock
- Copy
- Use
--no-dev
key - Use
--no-root
key
- Use cargo-chef for
cargo build
- Increase
shm_size