J'ai conteneurisé une app métier complète en 2h — Docker Compose, Nginx, Flask, PostgreSQL
La plupart des tutoriels Docker s'arrêtent à docker run hello-world ou au conteneur unique.
En entreprise, une vraie application c'est plusieurs services qui travaillent ensemble : une base de données, un backend, un serveur web. Ce guide montre comment orchestrer tout ça avec Docker Compose, en construisant une app de gestion de stock pour une PME africaine — de zéro à un environnement production-ready.
🎯 Ce qu'on va construire
taskflow-stock : une API REST de gestion de stock et facturation, avec 3 services Docker orchestrés :
INTERNET → NGINX (reverse proxy)
↓
FLASK API (logique métier, Gunicorn)
↓
POSTGRESQL (données persistées)
À la fin, une seule commande lance tout :
docker compose up --build
🏗️ Architecture — Pourquoi 3 services séparés ?
Chaque service a une seule responsabilité :
| Service | Rôle | Image |
|---|---|---|
nginx |
Reçoit le trafic HTTP, redirige vers Flask | nginx:alpine |
api |
Logique métier, endpoints REST | Custom (Python 3.12 Alpine) |
db |
Stockage persistant | postgres:15-alpine |
Principe clé : moindre exposition.
Seul Nginx a un ports: dans le Compose. Flask et PostgreSQL sont invisibles depuis l'extérieur — accessibles uniquement via le réseau Docker interne app-network.
📁 Structure du projet
taskflow-stock/
├── app/
│ ├── app.py # Flask + SQLAlchemy
│ ├── requirements.txt
│ ├── Dockerfile # Multi-stage Alpine
│ └── .dockerignore
├── nginx/
│ └── nginx.conf # Reverse proxy config
├── postgres/
│ └── init.sql # Init automatique au 1er démarrage
├── docker-compose.yml
├── .env # Secrets (jamais sur Git)
└── .env.example # Modèle versionnable
🐍 Le backend Flask — Modèles métier contextualisés
J'ai délibérément choisi des entités qui parlent à la réalité des PME africaines :
class Produit(db.Model):
__tablename__ = 'produits'
id = db.Column(db.Integer, primary_key=True)
nom = db.Column(db.String(100), nullable=False) # "Huile de palme 1L"
reference = db.Column(db.String(50), unique=True, nullable=False)
prix = db.Column(db.Float, nullable=False) # En FCFA
stock = db.relationship('Stock', backref='produit', uselist=False)
class Stock(db.Model):
__tablename__ = 'stocks'
produit_id = db.Column(db.Integer, db.ForeignKey('produits.id'))
quantite = db.Column(db.Integer, default=0)
seuil_alerte = db.Column(db.Integer, default=10) # Alerte si stock critique
La connexion à la base ne contient aucun mot de passe dans le code :
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL')
Tout vient du .env, jamais du code source.
🐳 Le Dockerfile — Multi-stage build
Le secret d'une image légère : séparer la compilation de l'exécution.
# Stage 1 : installation des dépendances (avec outils de compilation)
FROM python:3.12-alpine AS builder
RUN apk add --no-cache gcc musl-dev postgresql-dev
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# Stage 2 : image finale (sans les outils de build)
FROM python:3.12-alpine
RUN apk add --no-cache libpq
RUN adduser -D appuser # Utilisateur non-root
WORKDIR /app
COPY --from=builder /install /usr/local
COPY . .
RUN chown -R appuser:appuser /app
USER appuser
EXPOSE 5000
CMD ["gunicorn", "-w", "2", "-b", "0.0.0.0:5000", "app:app"]
Résultat : image finale < 90MB, sans compilateur, sans outils inutiles.
Astuce cache Docker : toujours copier requirements.txt AVANT le code source. Si les dépendances ne changent pas, Docker saute le pip install — le build prend 5 secondes au lieu de 2 minutes.
⚙️ Nginx — Le reverse proxy
upstream flask_app {
server api:5000; # "api" = nom du service Docker, résolu automatiquement
}
server {
listen 80;
location / {
proxy_pass http://flask_app;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
La magie de api:5000 : dans un réseau Docker, chaque service est joignable par son nom. Docker maintient un DNS interne — pas besoin de connaître les IPs, elles changent à chaque recréation de conteneur.
🎼 Docker Compose — L'orchestrateur
La partie la plus importante : gérer l'ordre de démarrage correctement.
services:
db:
image: postgres:15-alpine
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- pgdata:/var/lib/postgresql/data # Persistance
- ./postgres/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 5s
retries: 5
api:
build: ./app
environment:
DATABASE_URL: ${DATABASE_URL}
depends_on:
db:
condition: service_healthy # ← attend que db soit VRAIMENT prêt
nginx:
image: nginx:alpine
ports:
- "80:80" # ← seul port exposé
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
api:
condition: service_healthy
networks:
app-network:
driver: bridge
volumes:
pgdata:
driver: local
condition: service_healthy vs depends_on simple :
-
depends_on: - db: attend que le conteneur soit démarré -
depends_on: db: condition: service_healthy: attend que PostgreSQL accepte vraiment des connexions
La différence élimine 90% des erreurs "connection refused" au démarrage.
🧪 Tests de résilience — Ce qui distingue un ingénieur
Test 1 — Persistance
docker compose down # Arrête tout (volumes conservés)
docker compose up -d # Redémarre
curl http://localhost/api/produits # ✅ Données toujours là
Test 2 — Panne partielle
docker compose kill api # Simule un crash applicatif
curl http://localhost/api/produits # → 502 (Nginx tient ✅)
docker compose up -d api # Relance uniquement Flask
curl http://localhost/api/produits # → données de retour ✅
Un 502 Bad Gateway est ici une bonne nouvelle : Nginx reste opérationnel et signale proprement l'indisponibilité du backend, au lieu de crasher lui-même.
💡 Les 5 leçons que ce projet m'a enseignées
-
"Démarré" ≠ "Prêt" —
service_healthyexiste pour cette raison - DNS interne Docker — les noms de services sont des hostnames, pas des IPs
- Le moindre port exposé — chaque port ouvert est une surface d'attaque
-
Les secrets vivent dans
.env, jamais dans le code ou l'image - Un 502 bien géré vaut mieux qu'un timeout silencieux
🔄 La suite naturelle
Ce projet est la base de 3 extensions :
- VPS + HTTPS — déployer sur Ubuntu avec Let's Encrypt
- Monitoring — Prometheus + Grafana en conteneurs supplémentaires
- CI/CD — GitHub Actions qui déploie automatiquement à chaque push
📎 Ressources
- 🔗 https://github.com/Bordley/Taskflow-stock.git
- 📹 https://youtu.be/cyUCMntb5KA?si=t0-N-sFcKciiGQZb
- 📚 Doc officielle Docker Compose : docs.docker.com/compose
Si ce guide t'a aidé, partage-le — et laisse un commentaire avec ta question ou ton propre projet Docker 👇
Bonne conteneurisation ! 🐳
Écrit par Bordley — IT Manager & DevOps Engineer, Bénin 🇧🇯
Je publie des contenus sur le Cloud et DevOps dans le contexte africain.
#docker #devops #python #flask #postgresql #nginx #francophone #beginners #tutorial #africa
Top comments (0)