DEV Community

Cover image for J'ai conteneurisé une app métier complète en 2h : Docker Compose, Nginx, Flask, PostgreSQL
Roméo DOSsOu
Roméo DOSsOu

Posted on

J'ai conteneurisé une app métier complète en 2h : Docker Compose, Nginx, Flask, PostgreSQL

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)
Enter fullscreen mode Exit fullscreen mode

À la fin, une seule commande lance tout :

docker compose up --build
Enter fullscreen mode Exit fullscreen mode

🏗️ 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
Enter fullscreen mode Exit fullscreen mode

🐍 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
Enter fullscreen mode Exit fullscreen mode

La connexion à la base ne contient aucun mot de passe dans le code :

app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL')
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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à
Enter fullscreen mode Exit fullscreen mode

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 ✅
Enter fullscreen mode Exit fullscreen mode

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

  1. "Démarré" ≠ "Prêt"service_healthy existe pour cette raison
  2. DNS interne Docker — les noms de services sont des hostnames, pas des IPs
  3. Le moindre port exposé — chaque port ouvert est une surface d'attaque
  4. Les secrets vivent dans .env, jamais dans le code ou l'image
  5. Un 502 bien géré vaut mieux qu'un timeout silencieux

🔄 La suite naturelle

Ce projet est la base de 3 extensions :

  1. VPS + HTTPS — déployer sur Ubuntu avec Let's Encrypt
  2. Monitoring — Prometheus + Grafana en conteneurs supplémentaires
  3. CI/CD — GitHub Actions qui déploie automatiquement à chaque push

📎 Ressources


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)