Skip to main content

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 stop and docker 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:

  1. Builds the Docker image
  2. Runs the tests
  3. Scans for vulnerabilities
  4. Pushes to Docker Hub
  5. 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

  1. What is the difference between CMD and ENTRYPOINT?

  2. Why use a multi-stage build?

  3. Which command shows a container's logs in real time?

  4. How do you persist a database's data?

  5. What is the difference between a volume and a bind mount?

  6. How do containers communicate on a custom Docker network?

  7. Why not run a container as root?

  8. How do you limit a container's memory to 512 MB?

Answers

See the answers
  1. CMD defines the default command (easily overridable), ENTRYPOINT defines the main executable

  2. To create smaller images by separating the build environment from the production environment

  3. docker logs -f container_name

  4. By using Docker volumes: -v volume:/data/path

  5. Volumes are managed by Docker, bind mounts link a specific host path

  6. Through automatic DNS resolution using the container name

  7. For security: limit privileges in case of compromise

  8. docker run -m 512m or --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
Next steps
  • Explore Docker Swarm for orchestration
  • Learn Kubernetes for large-scale deployments
  • Integrate Docker into your CI/CD pipelines

← Best practices | Table of Contents