Aller au contenu principal

Exercices et Projets Docker


Objectifs du chapitre

  • Consolider les connaissances acquises
  • Pratiquer avec des exercices progressifs
  • Réaliser des projets complets
  • Se préparer aux cas réels

1 - Exercices de base

Exercice 1 : Premier conteneur

Objectif : Lancer et manipuler un conteneur nginx

# 1. Télécharger l'image nginx
docker pull nginx:alpine

# 2. Lancer un conteneur nommé "web" sur le port 8080
docker run -d --name web -p 8080:80 nginx:alpine

# 3. Vérifier que le conteneur fonctionne
curl http://localhost:8080

# 4. Voir les logs du conteneur
docker logs web

# 5. Arrêter et supprimer le conteneur
docker stop web && docker rm web

Questions :

  • Quelle est la différence entre docker stop et docker kill ?
  • Que se passe-t-il si vous essayez de supprimer un conteneur en cours d'exécution ?

Exercice 2 : Variables d'environnement

Objectif : Configurer une base de données MySQL avec des variables d'environnement

# 1. Lancer MySQL avec configuration personnalisée
docker run -d \
--name mysql-db \
-e MYSQL_ROOT_PASSWORD=rootpass \
-e MYSQL_DATABASE=myapp \
-e MYSQL_USER=appuser \
-e MYSQL_PASSWORD=apppass \
-p 3306:3306 \
mysql:8

# 2. Vérifier que la base de données est prête
docker logs mysql-db

# 3. Se connecter à MySQL
docker exec -it mysql-db mysql -u appuser -papppass myapp

# 4. Dans MySQL, créer une table
CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100));
INSERT INTO users VALUES (1, 'Alice');
SELECT * FROM users;
EXIT;

Exercice 3 : Volumes

Objectif : Persister les données d'une base PostgreSQL

# 1. Créer un volume nommé
docker volume create postgres-data

# 2. Lancer PostgreSQL avec le volume
docker run -d \
--name postgres \
-e POSTGRES_PASSWORD=secret \
-v postgres-data:/var/lib/postgresql/data \
postgres:15

# 3. Créer des données
docker exec -it postgres psql -U postgres -c "CREATE TABLE test (id serial, name text);"
docker exec -it postgres psql -U postgres -c "INSERT INTO test (name) VALUES ('Hello');"

# 4. Supprimer le conteneur
docker rm -f postgres

# 5. Recréer le conteneur avec le même volume
docker run -d \
--name postgres \
-e POSTGRES_PASSWORD=secret \
-v postgres-data:/var/lib/postgresql/data \
postgres:15

# 6. Vérifier que les données sont préservées
docker exec -it postgres psql -U postgres -c "SELECT * FROM test;"

2 - Exercices intermédiaires

Exercice 4 : Créer un Dockerfile

Objectif : Conteneuriser une application Python Flask

Structure du projet :

flask-app/
├── app.py
├── requirements.txt
└── Dockerfile

app.py :

from flask import Flask
import os

app = Flask(__name__)

@app.route('/')
def hello():
name = os.environ.get('APP_NAME', 'World')
return f'Hello, {name}!'

@app.route('/health')
def health():
return {'status': 'healthy'}

if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)

requirements.txt :

flask==3.0.0
gunicorn==21.2.0

Dockerfile (à compléter) :

# Utiliser Python 3.11 slim
FROM python:3.11-slim

# Définir le répertoire de travail
WORKDIR /app

# Copier et installer les dépendances
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copier le code
COPY app.py .

# Créer un utilisateur non-root
RUN useradd -m appuser
USER appuser

# Exposer le port
EXPOSE 5000

# Healthcheck
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:5000/health')"

# Commande de démarrage
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]

Commandes :

# Build
docker build -t flask-app:v1 .

# Run
docker run -d -p 5000:5000 -e APP_NAME=Docker flask-app:v1

# Test
curl http://localhost:5000

Exercice 5 : Réseaux Docker

Objectif : Faire communiquer une application web avec une base de données

# 1. Créer un réseau
docker network create app-network

# 2. Lancer la base de données
docker run -d \
--name db \
--network app-network \
-e POSTGRES_PASSWORD=secret \
-e POSTGRES_DB=myapp \
postgres:15

# 3. Tester la résolution DNS depuis un autre conteneur
docker run --rm --network app-network alpine ping -c 3 db

# 4. Lancer une application qui se connecte à la base
docker run -d \
--name app \
--network app-network \
-p 8080:8080 \
-e DATABASE_URL=postgres://postgres:secret@db:5432/myapp \
adminer

# 5. Accéder à Adminer via http://localhost:8080

Exercice 6 : Multi-stage build

Objectif : Optimiser une image Node.js avec un multi-stage build

Application :

// index.js
const express = require('express');
const app = express();

app.get('/', (req, res) => {
res.json({ message: 'Hello from Docker!' });
});

app.listen(3000, () => {
console.log('Server running on port 3000');
});

package.json :

{
"name": "node-app",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"express": "^4.18.2"
}
}

Dockerfile multi-stage :

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

# Stage 2: Production
FROM node:18-alpine
WORKDIR /app
RUN addgroup -g 1001 -S nodejs && adduser -S -u 1001 -G nodejs nodejs
COPY --from=deps /app/node_modules ./node_modules
COPY --chown=nodejs:nodejs . .
USER nodejs
EXPOSE 3000
CMD ["node", "index.js"]

Comparer les tailles :

# Build sans multi-stage
# FROM node:18
# WORKDIR /app
# COPY . .
# RUN npm install
# CMD ["node", "index.js"]

docker build -t node-app:basic -f Dockerfile.basic .
docker build -t node-app:optimized .

docker images | grep node-app
# node-app basic ~1GB
# node-app optimized ~150MB

3 - Projets complets

Projet 1 : Application MERN Stack

Architecture :

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│ Frontend │────▶│ Backend │────▶│ MongoDB │
│ (React) │ │ (Express) │ │ │
│ :3000 │ │ :5000 │ │ :27017 │
└─────────────┘ └─────────────┘ └─────────────┘

docker-compose.yml :

version: "3.9"

services:
frontend:
build: ./frontend
ports:
- "3000:3000"
environment:
- REACT_APP_API_URL=http://localhost:5000
depends_on:
- backend

backend:
build: ./backend
ports:
- "5000:5000"
environment:
- MONGODB_URI=mongodb://mongo:27017/mernapp
- JWT_SECRET=supersecretkey
depends_on:
- mongo

mongo:
image: mongo:7
volumes:
- mongo-data:/data/db
environment:
- MONGO_INITDB_DATABASE=mernapp

volumes:
mongo-data:

Projet 2 : Stack de monitoring

Objectif : Déployer Prometheus + Grafana pour le monitoring

docker-compose.yml :

version: "3.9"

services:
prometheus:
image: prom/prometheus:v2.47.0
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'

grafana:
image: grafana/grafana:10.1.0
ports:
- "3000:3000"
volumes:
- grafana-data:/var/lib/grafana
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
depends_on:
- prometheus

node-exporter:
image: prom/node-exporter:v1.6.1
ports:
- "9100:9100"

volumes:
prometheus-data:
grafana-data:

prometheus.yml :

global:
scrape_interval: 15s

scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']

- job_name: 'node'
static_configs:
- targets: ['node-exporter:9100']

Projet 3 : Application WordPress haute disponibilité

docker-compose.yml :

version: "3.9"

services:
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- wordpress1
- wordpress2

wordpress1:
image: wordpress:6-fpm-alpine
environment:
WORDPRESS_DB_HOST: mariadb
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: wordpress
WORDPRESS_DB_NAME: wordpress
volumes:
- wp-content:/var/www/html/wp-content
depends_on:
- mariadb
- redis

wordpress2:
image: wordpress:6-fpm-alpine
environment:
WORDPRESS_DB_HOST: mariadb
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: wordpress
WORDPRESS_DB_NAME: wordpress
volumes:
- wp-content:/var/www/html/wp-content
depends_on:
- mariadb
- redis

mariadb:
image: mariadb:11
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress
MYSQL_PASSWORD: wordpress
volumes:
- db-data:/var/lib/mysql

redis:
image: redis:7-alpine

volumes:
wp-content:
db-data:

4 - Défis avancés

Défi 1 : Optimisation extrême

Réduisez la taille de cette image au maximum :

# Image de départ : ~1.2 GB
FROM node:18
WORKDIR /app
COPY . .
RUN npm install
CMD ["npm", "start"]

Objectif : Atteindre moins de 100 MB

Indices :

  • Multi-stage build
  • Image Alpine ou distroless
  • Production dependencies only
  • Nettoyage du cache npm

Défi 2 : Sécurisation complète

Sécurisez cette configuration :

# Configuration NON sécurisée
services:
app:
image: myapp
ports:
- "3000:3000"
environment:
- DB_PASSWORD=secret123

db:
image: postgres
environment:
- POSTGRES_PASSWORD=root

Points à améliorer :

  • Secrets
  • Utilisateur non-root
  • Réseau isolé
  • Healthchecks
  • Limites de ressources
  • Images versionnées

Défi 3 : CI/CD Pipeline

Créez un pipeline GitHub Actions qui :

  1. Build l'image Docker
  2. Exécute les tests
  3. Scanne les vulnérabilités
  4. Pousse vers Docker Hub
  5. Déploie (optionnel)
# .github/workflows/docker.yml
name: Docker CI/CD

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Build image
run: docker build -t myapp:${{ github.sha }} .

- name: Run tests
run: docker run myapp:${{ github.sha }} npm test

- name: Scan vulnerabilities
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}

- name: Push to Docker Hub
if: github.ref == 'refs/heads/main'
run: |
echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
docker tag myapp:${{ github.sha }} ${{ secrets.DOCKER_USERNAME }}/myapp:latest
docker push ${{ secrets.DOCKER_USERNAME }}/myapp:latest

5 - Quiz de révision

Questions

  1. Quelle est la différence entre CMD et ENTRYPOINT ?

  2. Pourquoi utiliser un multi-stage build ?

  3. Quelle commande permet de voir les logs d'un conteneur en temps réel ?

  4. Comment persister les données d'une base de données ?

  5. Quelle est la différence entre un volume et un bind mount ?

  6. Comment les conteneurs communiquent-ils sur un réseau Docker personnalisé ?

  7. Pourquoi ne pas exécuter un conteneur en tant que root ?

  8. Comment limiter la mémoire d'un conteneur à 512 MB ?

Réponses

Voir les réponses
  1. CMD définit la commande par défaut (facilement remplaçable), ENTRYPOINT définit l'exécutable principal

  2. Pour créer des images plus petites en séparant l'environnement de build de l'environnement de production

  3. docker logs -f nom_conteneur

  4. En utilisant des volumes Docker : -v volume:/chemin/donnees

  5. Les volumes sont gérés par Docker, les bind mounts lient un chemin spécifique de l'hôte

  6. Par résolution DNS automatique en utilisant le nom du conteneur

  7. Pour la sécurité : limiter les privilèges en cas de compromission

  8. docker run -m 512m ou --memory=512m


Résumé

Vous avez maintenant pratiqué :

  • La manipulation de conteneurs et d'images
  • La création de Dockerfiles optimisés
  • La configuration de réseaux et volumes
  • Le déploiement d'applications multi-conteneurs
  • Les bonnes pratiques de sécurité
Prochaines étapes
  • Explorez Docker Swarm pour l'orchestration
  • Apprenez Kubernetes pour les déploiements à grande échelle
  • Intégrez Docker dans vos pipelines CI/CD

← Bonnes pratiques | Table des matières