Docker Exercises and Projects
Chapter objectives
- Consolidate the knowledge gained
- Practice with progressive exercises
- Complete full projects
- Prepare for real-world cases
1 - Basic exercises
Exercise 1: First container
Objective: Run and manipulate an nginx container
# 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:
- What is the difference between
docker stopanddocker kill? - What happens if you try to delete a running container?
Exercise 2: Environment variables
Objective: Configure a MySQL database with environment variables
# 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;
Exercise 3: Volumes
Objective: Persist the data of a PostgreSQL database
# 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 - Intermediate exercises
Exercise 4: Create a Dockerfile
Objective: Containerize a Python Flask application
Project structure:
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 (to complete):
# 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"]
Commands:
# 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
Exercise 5: Docker networks
Objective: Make a web application communicate with a database
# 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
Exercise 6: Multi-stage build
Objective: Optimize a Node.js image with a 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"
}
}
Multi-stage Dockerfile:
# 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"]
Compare the sizes:
# 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 - Complete projects
Project 1: MERN Stack application
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:
Project 2: Monitoring stack
Objective: Deploy Prometheus + Grafana for 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']
Project 3: High-availability WordPress application
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 - Advanced challenges
Challenge 1: Extreme optimization
Reduce the size of this image as much as possible:
# Image de départ : ~1.2 GB
FROM node:18
WORKDIR /app
COPY . .
RUN npm install
CMD ["npm", "start"]
Objective: Reach less than 100 MB
Hints:
- Multi-stage build
- Alpine or distroless image
- Production dependencies only
- Clean up the npm cache
Challenge 2: Complete hardening
Secure this 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 to improve:
- Secrets
- Non-root user
- Isolated network
- Healthchecks
- Resource limits
- Versioned images
Challenge 3: CI/CD Pipeline
Create a GitHub Actions pipeline that:
- Builds the Docker image
- Runs the tests
- Scans for vulnerabilities
- Pushes to Docker Hub
- Deploys (optional)
# .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 - Review quiz
Questions
-
What is the difference between
CMDandENTRYPOINT? -
Why use a multi-stage build?
-
Which command shows a container's logs in real time?
-
How do you persist a database's data?
-
What is the difference between a volume and a bind mount?
-
How do containers communicate on a custom Docker network?
-
Why not run a container as root?
-
How do you limit a container's memory to 512 MB?
Answers
See the answers
-
CMDdefines the default command (easily overridable),ENTRYPOINTdefines the main executable -
To create smaller images by separating the build environment from the production environment
-
docker logs -f container_name -
By using Docker volumes:
-v volume:/data/path -
Volumes are managed by Docker, bind mounts link a specific host path
-
Through automatic DNS resolution using the container name
-
For security: limit privileges in case of compromise
-
docker run -m 512mor--memory=512m
Summary
You have now practiced:
- Manipulating containers and images
- Creating optimized Dockerfiles
- Configuring networks and volumes
- Deploying multi-container applications
- Security best practices
- Explore Docker Swarm for orchestration
- Learn Kubernetes for large-scale deployments
- Integrate Docker into your CI/CD pipelines