Skip to main content

Docker best practices


Chapter objectives

  • Optimize image size
  • Improve container security
  • Follow industry conventions
  • Prepare your images for production

1 - Image optimization

Choose the right base image

# ❌ Image complète (1 Go+)
FROM ubuntu:22.04

# ⚠️ Image standard (~900 Mo)
FROM node:18

# ✅ Image slim (~200 Mo)
FROM node:18-slim

# ✅ Image Alpine (~50 Mo)
FROM node:18-alpine

# ✅ Image distroless (~20 Mo)
FROM gcr.io/distroless/nodejs18-debian11

# ✅ Image scratch (0 Mo, binaire statique)
FROM scratch

Size comparison

ImageSizeUsage
ubuntu:22.04~77 MBDevelopment
node:18~900 MBDev with full tooling
node:18-slim~200 MBLightweight production
node:18-alpine~50 MBOptimized production
distroless~20 MBSecure production

Minimize layers

# ❌ Plusieurs couches
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
RUN apt-get clean

# ✅ Une seule couche optimisée
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
git && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

Clean up in the same layer

# ❌ Mauvais - le cache reste dans la couche précédente
RUN apt-get update && apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*

# ✅ Bon - nettoyage dans la même couche
RUN apt-get update && \
apt-get install -y --no-install-recommends curl && \
rm -rf /var/lib/apt/lists/*

2 - Cache optimization

Order of instructions

# ❌ Mauvais - le cache est invalidé à chaque changement de code
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "index.js"]

# ✅ Bon - les dépendances sont en cache
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
CMD ["node", "index.js"]

The .dockerignore file

# Dépendances
node_modules
vendor
__pycache__

# Git
.git
.gitignore

# IDE
.idea
.vscode
*.swp

# Logs et données
*.log
logs
data

# Tests
coverage
.nyc_output
test
tests
__tests__

# Docker
Dockerfile*
docker-compose*
.docker

# Documentation
README.md
docs

# Environnement
.env
.env.*
*.local

3 - Security

Do not run as root

# Créer un utilisateur non-root
FROM node:18-alpine

# Créer le groupe et l'utilisateur
RUN addgroup -g 1001 -S nodejs && \
adduser -S -u 1001 -G nodejs nodejs

WORKDIR /app

# Changer la propriété des fichiers
COPY --chown=nodejs:nodejs . .

# Utiliser l'utilisateur non-root
USER nodejs

CMD ["node", "index.js"]

Use signed images

# Activer le content trust
export DOCKER_CONTENT_TRUST=1

# Les images doivent être signées
docker pull nginx:latest

Scan for vulnerabilities

# Avec Docker Scout
docker scout quickview nginx:latest
docker scout cves nginx:latest

# Avec Trivy
trivy image nginx:latest

# Avec Snyk
snyk container test nginx:latest

Secrets and sensitive data

# ❌ Ne JAMAIS faire ça
ENV API_KEY=secret123
COPY credentials.json /app/

# ✅ Utiliser des secrets Docker
# docker run --secret id=api_key,src=./api_key.txt myapp
# Docker Compose avec secrets
services:
app:
image: myapp
secrets:
- db_password

secrets:
db_password:
file: ./secrets/db_password.txt

Read-only image

# Exécuter en lecture seule
docker run --read-only \
--tmpfs /tmp \
--tmpfs /var/run \
myapp

4 - Logging and monitoring

Configure logs

# Rediriger les logs vers stdout/stderr
CMD ["nginx", "-g", "daemon off;"]
# Limiter la taille des logs
docker run -d \
--log-driver json-file \
--log-opt max-size=10m \
--log-opt max-file=3 \
myapp

Global log configuration

// /etc/docker/daemon.json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}

Healthchecks

FROM nginx:alpine

HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost/ || exit 1
# Docker Compose
services:
web:
image: nginx
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s

5 - Multi-stage builds

Node.js example

# Stage 1: Build
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: Production
FROM node:18-alpine AS production
WORKDIR /app

# Créer un utilisateur non-root
RUN addgroup -g 1001 -S nodejs && \
adduser -S -u 1001 -G nodejs nodejs

# Copier uniquement le nécessaire
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/package.json ./

USER nodejs
EXPOSE 3000
CMD ["node", "dist/index.js"]

Go example

# Build
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o main .

# Production - Image scratch minimale
FROM scratch
COPY --from=builder /app/main /main
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
USER 1000:1000
ENTRYPOINT ["/main"]

Python example

# Build
FROM python:3.11 AS builder
WORKDIR /app
RUN pip install --user pipenv
COPY Pipfile* ./
RUN pipenv requirements > requirements.txt
RUN pip install --user -r requirements.txt

# Production
FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
RUN useradd -m appuser && chown -R appuser:appuser /app
USER appuser
CMD ["python", "app.py"]

6 - Resource management

CPU and memory limits

# Ligne de commande
docker run -d \
--memory=512m \
--memory-swap=512m \
--cpus=0.5 \
myapp
# Docker Compose
services:
app:
image: myapp
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M

Restart policy

# Options de restart
docker run -d --restart=no myapp # Par défaut
docker run -d --restart=on-failure myapp # Seulement en cas d'erreur
docker run -d --restart=always myapp # Toujours
docker run -d --restart=unless-stopped myapp # Sauf si arrêté manuellement
# Docker Compose
services:
app:
image: myapp
restart: unless-stopped

7 - Tags and versioning

Naming convention

# Structure recommandée
registre/organisation/image:version

# Exemples
ghcr.io/myorg/myapp:1.2.3
docker.io/myuser/myapp:latest
registry.example.com/myapp:v1.0.0-alpine

Semantic tags

# Tagger avec version sémantique
docker tag myapp:latest myapp:1.0.0
docker tag myapp:latest myapp:1.0
docker tag myapp:latest myapp:1

# Un utilisateur peut choisir le niveau de spécificité
docker pull myapp:1.0.0 # Version exacte
docker pull myapp:1.0 # Dernière patch de 1.0
docker pull myapp:1 # Dernière minor de 1.x

CI/CD tags

# Tags pour CI/CD
myapp:main # Branche principale
myapp:develop # Branche de développement
myapp:pr-123 # Pull request
myapp:sha-abc123 # Commit SHA
myapp:1.0.0-rc.1 # Release candidate

8 - Production checklist

Before deployment

## Checklist Image Docker

### Sécurité
- [ ] Image de base officielle ou vérifiée
- [ ] Version de base épinglée (pas :latest)
- [ ] Exécution en tant qu'utilisateur non-root
- [ ] Pas de secrets hardcodés
- [ ] Scan de vulnérabilités effectué
- [ ] Permissions minimales

### Optimisation
- [ ] Multi-stage build utilisé
- [ ] .dockerignore configuré
- [ ] Couches ordonnées pour le cache
- [ ] Nettoyage dans les couches RUN
- [ ] Image de base légère (alpine, slim, distroless)

### Opérations
- [ ] HEALTHCHECK configuré
- [ ] Logs vers stdout/stderr
- [ ] Limites de ressources définies
- [ ] Politique de restart configurée
- [ ] Labels/metadata ajoutés

### Documentation
- [ ] README avec instructions de build
- [ ] Variables d'environnement documentées
- [ ] Ports exposés documentés

OCI labels

LABEL org.opencontainers.image.source="https://github.com/user/repo"
LABEL org.opencontainers.image.description="Description de l'application"
LABEL org.opencontainers.image.licenses="MIT"
LABEL org.opencontainers.image.version="1.0.0"
LABEL org.opencontainers.image.vendor="MyCompany"
LABEL org.opencontainers.image.authors="[email protected]"

9 - Complete optimized Dockerfile

Production-ready Node.js application

# syntax=docker/dockerfile:1

###################
# BUILD STAGE
###################
FROM node:18-alpine AS builder

# Installer les dépendances de build
RUN apk add --no-cache python3 make g++

WORKDIR /app

# Copier les fichiers de dépendances
COPY package*.json ./

# Installer les dépendances
RUN npm ci --only=production && \
npm cache clean --force

# Copier le code source
COPY . .

# Build l'application
RUN npm run build

###################
# PRODUCTION STAGE
###################
FROM node:18-alpine AS production

# Labels OCI
LABEL org.opencontainers.image.source="https://github.com/example/myapp"
LABEL org.opencontainers.image.description="My Production App"
LABEL org.opencontainers.image.version="1.0.0"

# Variables d'environnement
ENV NODE_ENV=production \
PORT=3000

# Créer un utilisateur non-root
RUN addgroup -g 1001 -S nodejs && \
adduser -S -u 1001 -G nodejs nodejs

WORKDIR /app

# Copier depuis le builder
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/package.json ./

# Utiliser l'utilisateur non-root
USER nodejs

# Exposer le port
EXPOSE 3000

# Healthcheck
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

# Commande de démarrage
CMD ["node", "dist/index.js"]

Summary

CategoryBest practice
ImagesUse lightweight images (alpine, slim)
LayersMinimize and optimize the order
CacheCopy dependencies before the code
SecurityDo not run as root
SecretsNever hardcode, use Docker secrets
LogsRedirect to stdout/stderr
HealthAlways define a HEALTHCHECK
ResourcesDefine CPU/memory limits
TagsUse semantic versioning
Key points
  • Multi-stage builds for lightweight and secure images
  • Always use a non-root user
  • Regularly scan for vulnerabilities
  • Document and version your images

Hands-on exercises

  1. Optimize an existing Dockerfile with a multi-stage build
  2. Add a non-root user and a healthcheck
  3. Reduce an image's size by 50%+
  4. Configure resource limits in Docker Compose

← Docker Compose | Exercises and projects →