Docker - Partie 2 : Dockerfile et Docker Compose

← Partie 1 : Concepts et Fondamentaux | Retour aux tutoriels

Parcours d'apprentissage (novice vers expert)

Objectif: partir d'un Dockerfile simple puis orchestrer plusieurs services avec Docker Compose.

Fonctionnement: lis la section, applique sur un projet test, puis vérifie les conteneurs et logs.

1. Dockerfile : Créer ses propres images

Qu'est-ce qu'un Dockerfile ?

Un Dockerfile est un fichier texte contenant une série d'instructions pour construire une image Docker de manière automatisée et reproductible.

Structure de base

# Commentaire
FROM image-de-base
LABEL maintainer="vous@example.com"
RUN commande-shell
COPY source destination
WORKDIR /chemin/travail
EXPOSE 80
CMD ["executable", "param1", "param2"]

Instructions principales

Instruction Description Exemple
FROM Image de base (obligatoire) FROM ubuntu:22.04
RUN Exécute une commande lors du build RUN apt-get update
COPY Copie fichiers hôte → image COPY app.py /app/
ADD Comme COPY + décompression archives ADD archive.tar.gz /app/
WORKDIR Définit le répertoire de travail WORKDIR /app
ENV Définit une variable d'environnement ENV APP_ENV=production
EXPOSE Documente les ports utilisés EXPOSE 80 443
VOLUME Crée un point de montage VOLUME /data
USER Utilisateur pour RUN/CMD/ENTRYPOINT USER www-data
CMD Commande par défaut (écrasable) CMD ["nginx", "-g", "daemon off;"]
ENTRYPOINT Point d'entrée (non écrasable) ENTRYPOINT ["python", "app.py"]
ARG Variable de build ARG VERSION=1.0

2. Exemple simple : Serveur Apache personnalisé

Structure du projet

mon-apache/
├── Dockerfile
├── html/
│   ├── index.html
│   └── style.css
└── conf/
    └── custom.conf

Dockerfile

# Utiliser l'image officielle Apache
FROM httpd:2.4

# Métadonnées
LABEL maintainer="vous@example.com"
LABEL version="1.0"
LABEL description="Serveur Apache personnalisé"

# Copier la configuration personnalisée
COPY conf/custom.conf /usr/local/apache2/conf/extra/httpd-custom.conf

# Ajouter l'inclusion dans la config principale
RUN echo "Include conf/extra/httpd-custom.conf" >> /usr/local/apache2/conf/httpd.conf

# Copier le site web
COPY html/ /usr/local/apache2/htdocs/

# Définir les permissions
RUN chown -R www-data:www-data /usr/local/apache2/htdocs/

# Exposer le port HTTP
EXPOSE 80

# La commande par défaut est déjà définie dans l'image de base
# CMD ["httpd-foreground"]

html/index.html

<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <title>Mon Apache Docker</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <h1>Serveur Apache dans Docker</h1>
    <p>Cette page est servie par Apache depuis un conteneur Docker personnalisé.</p>
    <ul>
        <li>Image de base : httpd:2.4</li>
        <li>Configuration personnalisée</li>
        <li>Site web statique</li>
    </ul>
</body>
</html>

conf/custom.conf

# Configuration personnalisée Apache
ServerTokens Prod
ServerSignature Off
TraceEnable Off

<Directory "/usr/local/apache2/htdocs">
    Options -Indexes +FollowSymLinks
    AllowOverride All
    Require all granted
</Directory>

Construire et exécuter

# Construire l'image
cd mon-apache
docker build -t mon-apache:1.0 .

# Vérifier l'image créée
docker images mon-apache

# Exécuter le conteneur
docker run -d -p 8080:80 --name apache-custom mon-apache:1.0

# Tester
curl http://localhost:8080

# Voir les logs
docker logs apache-custom

# Arrêter et supprimer
docker stop apache-custom
docker rm apache-custom

3. Optimisation des Dockerfiles

Problème : Layers et cache

Chaque instruction crée un layer (couche). Docker met en cache ces layers pour accélérer les builds.

❌ Mauvais exemple

FROM ubuntu:22.04

# Chaque RUN crée un layer
RUN apt-get update
RUN apt-get install -y nginx
RUN apt-get install -y curl
RUN apt-get install -y vim
RUN apt-get clean

COPY index.html /var/www/html/

CMD ["nginx", "-g", "daemon off;"]

✅ Bon exemple

FROM ubuntu:22.04

# Regrouper les commandes RUN
RUN apt-get update && \
    apt-get install -y \
        nginx \
        curl \
        vim && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# COPY en dernier (change souvent)
COPY index.html /var/www/html/

CMD ["nginx", "-g", "daemon off;"]

Bonnes pratiques

Fichier .dockerignore

# Ne pas copier dans l'image
.git
.gitignore
README.md
docker-compose.yml
Dockerfile
node_modules
*.log
.env
.vscode

4. Multi-stage builds

Concept

Utiliser plusieurs FROM dans un seul Dockerfile pour séparer la compilation de l'exécution.

Exemple : Application Go

# Stage 1 : Build
FROM golang:1.21 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o myapp

# Stage 2 : Runtime (image finale légère)
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/myapp .
EXPOSE 8080
CMD ["./myapp"]

# Image builder : ~1 Go
# Image finale : ~10 Mo !

Exemple : Application Node.js

# Stage 1 : Dependencies
FROM node:18 AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

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

# Stage 3 : Runtime
FROM node:18-alpine
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/server.js"]

5. Docker Compose : Orchestration multi-conteneurs

Qu'est-ce que Docker Compose ?

Docker Compose permet de définir et gérer des applications multi-conteneurs via un fichier YAML. Au lieu de lancer plusieurs docker run, on définit tout dans docker-compose.yml.

Structure d'un fichier docker-compose.yml

version: '3.8'  # Version du format (optionnel depuis 2020)

services:       # Définition des conteneurs
  service1:
    image: nginx
    ports:
      - "8080:80"
  
  service2:
    build: ./app
    volumes:
      - ./data:/data

volumes:        # Volumes nommés
  mon-volume:

networks:       # Réseaux personnalisés
  mon-reseau:

Commandes Docker Compose

# Démarrer tous les services (mode détaché)
docker compose up -d

# Démarrer avec rebuild des images
docker compose up -d --build

# Voir les services actifs
docker compose ps

# Voir les logs
docker compose logs
docker compose logs -f service-name

# Arrêter tous les services
docker compose stop

# Arrêter et supprimer les conteneurs
docker compose down

# Arrêter et supprimer TOUT (conteneurs, réseaux, volumes)
docker compose down -v

# Exécuter une commande dans un service
docker compose exec service-name bash

# Voir la configuration finale
docker compose config

6. Exemple complet : Stack LAMP avec Compose

Rappel de la Partie 1 (avec commandes docker run)

# Partie 1 : Méthode manuelle (complexe)
docker network create lamp-network
docker volume create mysql-data

docker run -d \
  --name lamp-mysql \
  --network lamp-network \
  -v mysql-data:/var/lib/mysql \
  -e MYSQL_ROOT_PASSWORD=rootpass \
  -e MYSQL_DATABASE=myapp \
  -e MYSQL_USER=appuser \
  -e MYSQL_PASSWORD=apppass \
  mysql:8.0

docker run -d \
  --name lamp-apache \
  --network lamp-network \
  -p 8080:80 \
  -v $(pwd)/html:/var/www/html \
  php:8.2-apache

docker exec lamp-apache docker-php-ext-install mysqli
docker restart lamp-apache

Partie 2 : Avec Docker Compose (simple)

Structure du projet

lamp-stack/
├── docker-compose.yml
├── Dockerfile
├── html/
│   └── index.php
└── mysql/
    └── init.sql

docker-compose.yml

services:
  # Service MySQL
  db:
    image: mysql:8.0
    container_name: lamp-mysql
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_DATABASE: myapp
      MYSQL_USER: appuser
      MYSQL_PASSWORD: apppass
    volumes:
      - mysql-data:/var/lib/mysql
      - ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql
    networks:
      - lamp-network
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

  # Service Apache + PHP
  web:
    build: .
    container_name: lamp-apache
    restart: unless-stopped
    ports:
      - "8080:80"
    volumes:
      - ./html:/var/www/html
    networks:
      - lamp-network
    depends_on:
      db:
        condition: service_healthy
    environment:
      DB_HOST: db
      DB_NAME: myapp
      DB_USER: appuser
      DB_PASS: apppass

volumes:
  mysql-data:
    name: lamp-mysql-data

networks:
  lamp-network:
    name: lamp-network
    driver: bridge

Dockerfile (pour le service web)

FROM php:8.2-apache

# Installer les extensions PHP nécessaires
RUN docker-php-ext-install mysqli pdo pdo_mysql

# Activer mod_rewrite Apache
RUN a2enmod rewrite

# Créer le répertoire de travail
WORKDIR /var/www/html

# Exposer le port
EXPOSE 80

html/index.php (même code, simplifié)

<?php
// Les variables viennent de docker-compose.yml
$host = getenv('DB_HOST');
$db   = getenv('DB_NAME');
$user = getenv('DB_USER');
$pass = getenv('DB_PASS');

try {
    $pdo = new PDO("mysql:host=$host;dbname=$db", $user, $pass);
    echo "<h1>✅ LAMP Stack avec Docker Compose</h1>";
    echo "<p>Version MySQL : " . $pdo->query('SELECT VERSION()')->fetchColumn() . "</p>";
    
    // Créer une table
    $pdo->exec("CREATE TABLE IF NOT EXISTS visitors (
        id INT AUTO_INCREMENT PRIMARY KEY,
        visit_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    )");
    
    $pdo->exec("INSERT INTO visitors () VALUES ()");
    $count = $pdo->query("SELECT COUNT(*) FROM visitors")->fetchColumn();
    echo "<p>Nombre de visites : $count</p>";
    
} catch (PDOException $e) {
    echo "<h1>❌ Erreur de connexion</h1>";
    echo "<p>" . htmlspecialchars($e->getMessage()) . "</p>";
}
?>

mysql/init.sql (optionnel)

-- Script d'initialisation MySQL
CREATE TABLE IF NOT EXISTS users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL,
    email VARCHAR(100) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO users (username, email) VALUES 
    ('admin', 'admin@example.com'),
    ('user1', 'user1@example.com');

Utilisation

# Démarrer la stack
docker compose up -d

# Vérifier que tout fonctionne
docker compose ps

# Accéder à l'application
http://localhost:8080

# Voir les logs
docker compose logs -f

# Accéder à MySQL
docker compose exec db mysql -u appuser -papppass myapp

# Arrêter proprement
docker compose down

# Tout supprimer (y compris les données)
docker compose down -v

Comparaison Partie 1 vs Partie 2

Aspect Partie 1 (docker run) Partie 2 (docker compose)
Commandes ~15 lignes 1 seule : docker compose up -d
Configuration Lignes de commande Fichier YAML lisible
Dépendances Manuelles (ordre important) Automatiques (depends_on)
Réseau Création manuelle Créé automatiquement
Volumes Création manuelle Créé automatiquement
Reproductibilité Documenter les commandes Fichier versionné (Git)
Partage Difficile Simple (1 fichier)

7. Exemple avancé : Application multi-services

Architecture

┌────────────────┐      ┌────────────────┐
│   Frontend     │─────▶│    Backend     │
│   (React)      │      │   (Node.js)    │
│   Port 3000    │      │   Port 5000    │
└────────────────┘      └────────┬───────┘
                                 │
                        ┌────────▼───────┐      ┌────────────────┐
                        │   PostgreSQL   │      │     Redis      │
                        │   Port 5432    │      │   Port 6379    │
                        └────────────────┘      └────────────────┘

docker-compose.yml

services:
  # Frontend React
  frontend:
    build: ./frontend
    ports:
      - "3000:3000"
    volumes:
      - ./frontend/src:/app/src
    environment:
      - REACT_APP_API_URL=http://localhost:5000
    depends_on:
      - backend

  # Backend Node.js
  backend:
    build: ./backend
    ports:
      - "5000:5000"
    volumes:
      - ./backend/src:/app/src
    environment:
      - NODE_ENV=development
      - DB_HOST=postgres
      - DB_PORT=5432
      - DB_NAME=myapp
      - DB_USER=postgres
      - DB_PASS=secret
      - REDIS_HOST=redis
      - REDIS_PORT=6379
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started

  # Base de données PostgreSQL
  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: secret
    volumes:
      - postgres-data:/var/lib/postgresql/data
      - ./postgres/init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

  # Cache Redis
  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes
    volumes:
      - redis-data:/data

  # Interface d'administration PostgreSQL
  adminer:
    image: adminer
    ports:
      - "8080:8080"
    depends_on:
      - postgres

volumes:
  postgres-data:
  redis-data:

backend/Dockerfile

FROM node:18-alpine
WORKDIR /app

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

# Copier le code source
COPY . .

EXPOSE 5000
CMD ["npm", "run", "dev"]

frontend/Dockerfile

FROM node:18-alpine
WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .

EXPOSE 3000
CMD ["npm", "start"]

Utilisation

# Démarrer tous les services
docker compose up -d

# Services disponibles :
# - Frontend : http://localhost:3000
# - Backend : http://localhost:5000
# - Adminer (DB) : http://localhost:8080

# Rebuilder un service spécifique
docker compose up -d --build backend

# Voir les logs d'un service
docker compose logs -f backend

# Exécuter une commande dans un service
docker compose exec backend npm install express
docker compose exec postgres psql -U postgres myapp

# Redémarrer un service
docker compose restart backend

# Arrêter tout
docker compose down

8. Variables d'environnement et fichier .env

Fichier .env

# Configuration de l'application
APP_NAME=MonApp
APP_ENV=development
APP_PORT=5000

# Base de données
DB_HOST=postgres
DB_PORT=5432
DB_NAME=myapp
DB_USER=postgres
DB_PASS=secret123

# Redis
REDIS_HOST=redis
REDIS_PORT=6379

# Secrets (NE PAS COMMITER !)
JWT_SECRET=super-secret-key
API_KEY=abc123def456

docker-compose.yml avec .env

services:
  backend:
    build: ./backend
    ports:
      - "${APP_PORT}:5000"
    environment:
      - NODE_ENV=${APP_ENV}
      - DB_HOST=${DB_HOST}
      - DB_PORT=${DB_PORT}
      - DB_NAME=${DB_NAME}
      - DB_USER=${DB_USER}
      - DB_PASS=${DB_PASS}
      - JWT_SECRET=${JWT_SECRET}
    env_file:
      - .env  # Ou directement comme ça

  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: ${DB_NAME}
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASS}

.env.example (à commiter dans Git)

# Configuration de l'application
APP_NAME=MonApp
APP_ENV=development
APP_PORT=5000

# Base de données
DB_HOST=postgres
DB_PORT=5432
DB_NAME=myapp
DB_USER=postgres
DB_PASS=CHANGEZ_MOI

# Secrets
JWT_SECRET=CHANGEZ_MOI
API_KEY=CHANGEZ_MOI

9. Profils et environnements multiples

docker-compose.yml avec profils

services:
  backend:
    build: ./backend
    
  postgres:
    image: postgres:15-alpine
    
  # Service de développement uniquement
  adminer:
    image: adminer
    ports:
      - "8080:8080"
    profiles:
      - dev
    
  # Service de test uniquement
  test-runner:
    build: ./backend
    command: npm test
    profiles:
      - test

Utilisation des profils

# Développement (avec adminer)
docker compose --profile dev up -d

# Test
docker compose --profile test up

# Production (services de base uniquement)
docker compose up -d

Fichiers séparés par environnement

# docker-compose.yml (base)
services:
  backend:
    build: ./backend
    
# docker-compose.dev.yml (développement)
services:
  backend:
    volumes:
      - ./backend/src:/app/src
    environment:
      - NODE_ENV=development
      
# docker-compose.prod.yml (production)
services:
  backend:
    restart: always
    environment:
      - NODE_ENV=production

Lancer avec plusieurs fichiers

# Développement
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d

# Production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

# Ou avec variable d'environnement
export COMPOSE_FILE=docker-compose.yml:docker-compose.dev.yml
docker compose up -d

10. Bonnes pratiques Docker Compose

Exemple avec toutes les bonnes pratiques

services:
  backend:
    build: ./backend
    container_name: myapp-backend
    restart: unless-stopped
    
    # Limites de ressources
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M
    
    # Health check
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    
    # Logging
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
    
    # Variables d'environnement
    env_file:
      - .env
    
    # Volumes
    volumes:
      - ./backend/src:/app/src:ro  # Read-only
    
    # Réseau
    networks:
      - backend-network
    
    # Dépendances
    depends_on:
      postgres:
        condition: service_healthy

  postgres:
    image: postgres:15-alpine
    container_name: myapp-postgres
    restart: unless-stopped
    
    # Variables d'environnement
    environment:
      POSTGRES_DB: ${DB_NAME}
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    
    # Secrets (Docker Swarm)
    secrets:
      - db_password
    
    # Volumes
    volumes:
      - postgres-data:/var/lib/postgresql/data
    
    # Health check
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5
    
    # Réseau isolé
    networks:
      - backend-network

volumes:
  postgres-data:
    driver: local

networks:
  backend-network:
    driver: bridge

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

11. Commandes utiles

# Voir la configuration finale (avec variables résolues)
docker compose config

# Valider le fichier sans démarrer
docker compose config --quiet

# Lister les services
docker compose ps

# Voir les services avec les ports
docker compose ps -a

# Logs de tous les services
docker compose logs -f

# Logs d'un service spécifique
docker compose logs -f backend

# Suivre les logs avec timestamps
docker compose logs -f -t

# Exécuter une commande
docker compose exec backend bash
docker compose exec postgres psql -U postgres

# Lancer un one-off container
docker compose run backend npm install
docker compose run --rm backend npm test

# Rebuilder sans cache
docker compose build --no-cache

# Pull des nouvelles versions d'images
docker compose pull

# Redémarrer un service
docker compose restart backend

# Pause/Unpause
docker compose pause
docker compose unpause

# Voir les processus
docker compose top

# Voir les événements en temps réel
docker compose events

# Supprimer les volumes orphelins
docker compose down --volumes --remove-orphans

Défi progression (vers expert)

Mission: décrire et lancer une application multi-services complète avec Compose, profils et variables d'environnement.

← Partie 1 : Concepts et Fondamentaux | Retour aux tutoriels