📑 Table des matières

Docker + IA : conteneuriser ses services intelligents

Self-Hosting 🟡 Intermédiaire ⏱️ 15 min de lecture 📅 2026-02-24

Docker + IA : conteneuriser ses services intelligents

Vous avez un serveur qui fait tourner une API, une base de données, un reverse proxy, et peut-être un modèle de langage. Tout est installé directement sur l'OS. Un jour, vous mettez à jour Python et tout casse. Votre API ne démarre plus, vos dépendances sont en conflit, et vous passez 4 heures à tout réparer.

Avec Docker, ce scénario n'existe plus. Chaque service tourne dans son conteneur isolé, avec ses propres dépendances, sa propre version de Python, son propre environnement. Mettez à jour ce que vous voulez — les autres services ne bougent pas.

Dans ce guide, on va conteneuriser des services IA concrets : une API FastAPI, un pipeline LLM, et tout l'écosystème autour. Du vrai self-hosting propre et reproductible.


🐳 Pourquoi Docker pour l'IA

🏛️ Les 4 piliers

Pilier Sans Docker Avec Docker
Isolation Python 3.11 casse votre app Python 3.9 Chaque service a sa propre version
Reproductibilité "Ça marche sur ma machine" Même image = même résultat partout
Scaling Redémarrer tout le serveur Scaler juste le service qui en a besoin
Déploiement 47 commandes à taper dans l'ordre docker compose up -d

🎯 Pourquoi c'est particulièrement important pour l'IA

Les projets IA ont des dépendances lourdes et conflictuelles :

# Projet A veut :
torch==2.1.0
numpy==1.24.0
transformers==4.35.0

# Projet B veut :
torch==1.13.0
numpy==1.21.0
tensorflow==2.14.0

Sans Docker, c'est le cauchemar des virtual environments. Avec Docker :

# Projet A
docker run -d --name project-a project-a:latest

# Projet B (versions complètement différentes, aucun conflit)
docker run -d --name project-b project-b:latest

⚡ Docker vs VM

Aspect Docker VM (VirtualBox, etc.)
Démarrage Secondes Minutes
Taille MBs (image) GBs (OS complet)
Performance Quasi-native Overhead virtualisation
Isolation Processus OS complet
Cas d'usage IA ✅ Idéal Overkill

📦 Les bases Docker en 5 minutes

📚 Vocabulaire essentiel

Image     = Le plan de construction (comme une classe)
Container = L'instance en cours d'exécution (comme un objet)
Dockerfile = La recette pour créer une image
Volume    = Stockage persistant (survit au restart du conteneur)
Network   = Réseau virtuel entre conteneurs
Compose   = Orchestration multi-conteneurs

💻 Commandes de survie

# Voir les conteneurs qui tournent
docker ps

# Voir TOUS les conteneurs (y compris arrêtés)
docker ps -a

# Voir les images téléchargées
docker images

# Logs d'un conteneur
docker logs mon-conteneur
docker logs -f mon-conteneur  # Follow (temps réel)

# Entrer dans un conteneur
docker exec -it mon-conteneur bash

# Arrêter / supprimer
docker stop mon-conteneur
docker rm mon-conteneur

# Nettoyer (images/conteneurs inutilisés)
docker system prune -a

📥 Installer Docker

# Ubuntu/Debian
curl -fsSL https://get.docker.com | sh

# Ajouter votre user au groupe docker (évite sudo)
sudo usermod -aG docker $USER

# Installer Docker Compose (inclus dans Docker Desktop, sinon :)
sudo apt install docker-compose-plugin

# Vérifier
docker --version
docker compose version

🏗️ Docker Compose : orchestrer plusieurs services

🤔 Pourquoi Compose ?

Un projet IA typique a besoin de :
- Une API (FastAPI, Flask)
- Une base de données (SQLite, PostgreSQL)
- Un reverse proxy (Nginx, Caddy)
- Peut-être un cache (Redis)
- Peut-être un worker (Celery, tâches async)

Gérer tout ça avec des docker run individuels est ingérable. Docker Compose définit tout dans un seul fichier :

# docker-compose.yml
version: '3.8'

services:
  api:
    build: ./api
    ports:
      - "8000:8000"
    volumes:
      - ./data:/app/data
    environment:
      - DATABASE_URL=sqlite:///data/app.db
      - OPENROUTER_API_KEY=${OPENROUTER_API_KEY}
    depends_on:
      - db
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  db:
    image: postgres:16-alpine
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=myapp
      - POSTGRES_USER=app
      - POSTGRES_PASSWORD=${DB_PASSWORD}
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 10s
      timeout: 5s
      retries: 5

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/certs:/etc/nginx/certs:ro
    depends_on:
      - api
    restart: unless-stopped

volumes:
  pgdata:

⌨️ Les commandes Compose essentielles

# Démarrer tous les services
docker compose up -d

# Voir les logs de tous les services
docker compose logs -f

# Logs d'un service spécifique
docker compose logs -f api

# Redémarrer un service
docker compose restart api

# Tout arrêter
docker compose down

# Tout arrêter ET supprimer les volumes (ATTENTION: perte de données)
docker compose down -v

# Rebuilder après modification du Dockerfile
docker compose up -d --build

🤖 Exemple 1 : API FastAPI + SQLite conteneurisée

📂 Structure du projet

mon-api-ia/
├── docker-compose.yml
├── .env
├── api/
│   ├── Dockerfile
│   ├── requirements.txt
│   ├── main.py
│   └── models.py
├── nginx/
│   └── nginx.conf
└── data/
    └── (SQLite DB sera créée ici)

🐋 Le Dockerfile de l'API

# api/Dockerfile
FROM python:3.11-slim

# Éviter les questions interactives
ENV DEBIAN_FRONTEND=noninteractive
ENV PYTHONUNBUFFERED=1

WORKDIR /app

# Installer les dépendances système
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl && \
    rm -rf /var/lib/apt/lists/*

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

# Copier le code
COPY . .

# Port exposé
EXPOSE 8000

# Healthcheck
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
    CMD curl -f http://localhost:8000/health || exit 1

# Lancer l'API
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

📋 Les dépendances

# api/requirements.txt
fastapi==0.109.0
uvicorn[standard]==0.27.0
sqlalchemy==2.0.25
httpx==0.26.0
pydantic==2.5.3

⚡ L'API FastAPI

# api/main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from sqlalchemy import create_engine, Column, Integer, String, DateTime, Text
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from datetime import datetime
import httpx
import os

app = FastAPI(title="Mon API IA", version="1.0.0")

# Database
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///data/app.db")
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(bind=engine)
Base = declarative_base()

class Query(Base):
    __tablename__ = "queries"
    id = Column(Integer, primary_key=True)
    question = Column(Text, nullable=False)
    answer = Column(Text)
    model = Column(String(100))
    created_at = Column(DateTime, default=datetime.utcnow)

Base.metadata.create_all(engine)

# Schemas
class QuestionRequest(BaseModel):
    question: str
    model: str = "anthropic/claude-sonnet-4"

class QuestionResponse(BaseModel):
    answer: str
    model: str
    query_id: int

@app.get("/health")
async def health():
    return {"status": "healthy", "timestamp": datetime.utcnow().isoformat()}

@app.post("/ask", response_model=QuestionResponse)
async def ask_question(req: QuestionRequest):
    api_key = os.getenv("OPENROUTER_API_KEY")
    if not api_key:
        raise HTTPException(status_code=500, detail="API key not configured")

    async with httpx.AsyncClient() as client:
        response = await client.post(
            "https://openrouter.ai/api/v1/chat/completions",
            headers={"Authorization": f"Bearer {api_key}"},
            json={
                "model": req.model,
                "messages": [{"role": "user", "content": req.question}],
                "max_tokens": 1000
            },
            timeout=30.0
        )

    if response.status_code != 200:
        raise HTTPException(status_code=502, detail="LLM API error")

    answer = response.json()["choices"][0]["message"]["content"]

    # Sauvegarder en base
    db = SessionLocal()
    query = Query(question=req.question, answer=answer, model=req.model)
    db.add(query)
    db.commit()
    query_id = query.id
    db.close()

    return QuestionResponse(answer=answer, model=req.model, query_id=query_id)

@app.get("/queries")
async def list_queries(limit: int = 10):
    db = SessionLocal()
    queries = db.query(Query).order_by(Query.created_at.desc()).limit(limit).all()
    db.close()
    return [{"id": q.id, "question": q.question[:100], "model": q.model, "date": q.created_at.isoformat()} for q in queries]

🔧 Configuration Nginx

# nginx/nginx.conf
events {
    worker_connections 1024;
}

http {
    upstream api {
        server api:8000;
    }

    server {
        listen 80;
        server_name _;

        # Limite de taille pour les requêtes
        client_max_body_size 10M;

        location / {
            proxy_pass http://api;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            # Timeouts pour les requêtes IA (peuvent être lentes)
            proxy_read_timeout 60s;
            proxy_connect_timeout 10s;
        }

        location /health {
            proxy_pass http://api/health;
            access_log off;
        }
    }
}

🔑 Le fichier .env

# .env (NE PAS committer ce fichier !)
OPENROUTER_API_KEY=sk-or-v1-votre-cle-ici
DB_PASSWORD=un-mot-de-passe-fort-ici

🚀 Lancer le tout

# Créer le dossier data
mkdir -p data

# Lancer
docker compose up -d --build

# Vérifier
docker compose ps

# Tester
curl http://localhost/health
curl -X POST http://localhost/ask \
  -H "Content-Type: application/json" \
  -d '{"question": "Explique Docker en une phrase"}'

🧠 Exemple 2 : Pipeline LLM conteneurisé

Architecture

Un pipeline LLM typique :

Requête utilisateur
    ↓
[Conteneur: API Gateway]
    ↓
[Conteneur: Preprocessing]
  - Nettoyage du texte
  - Extraction d'intent
    ↓
[Conteneur: LLM Service]
  - Appel au modèle
  - Gestion du cache
    ↓
[Conteneur: Postprocessing]
  - Formatage de la réponse
  - Logging
    ↓
Réponse

Docker Compose multi-services

Pour aller plus loin sur ce sujet, consultez notre guide VPS + IA : le setup complet pour tout auto-héberger.

# docker-compose.yml
version: '3.8'

Pour aller plus loin sur ce sujet, consultez notre guide [APIs IA : OpenRouter vs appels directs](/article/apis-ia-openrouter-vs-appels-directs).

services:
  gateway:
    build: ./gateway
    ports:
      - "8080:8080"
    environment:
      - LLM_SERVICE_URL=http://llm:8001
      - REDIS_URL=redis://cache:6379
    depends_on:
      cache:
        condition: service_healthy
      llm:
        condition: service_healthy
    restart: unless-stopped

  llm:
    build: ./llm-service
    environment:
      - OPENROUTER_API_KEY=${OPENROUTER_API_KEY}
      - REDIS_URL=redis://cache:6379
      - DEFAULT_MODEL=anthropic/claude-sonnet-4
    depends_on:
      cache:
        condition: service_healthy
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8001/health"]
      interval: 15s
      timeout: 5s
      retries: 3

  cache:
    image: redis:7-alpine
    volumes:
      - redis_data:/data
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3
    command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru

  worker:
    build: ./worker
    environment:
      - REDIS_URL=redis://cache:6379
      - DATABASE_URL=sqlite:///data/logs.db
    volumes:
      - ./data:/app/data
    depends_on:
      cache:
        condition: service_healthy
    restart: unless-stopped

volumes:
  redis_data:

Le service LLM avec cache

# llm-service/main.py
from fastapi import FastAPI
import httpx
import redis
import hashlib
import json
import os

app = FastAPI()

# Connexion Redis pour le cache
r = redis.from_url(os.getenv("REDIS_URL", "redis://localhost:6379"))
CACHE_TTL = 3600  # 1 heure

@app.get("/health")
def health():
    # Vérifier que Redis est accessible
    try:
        r.ping()
        return {"status": "healthy", "cache": "connected"}
    except Exception:
        return {"status": "degraded", "cache": "disconnected"}

@app.post("/complete")
async def complete(prompt: str, model: str = None):
    model = model or os.getenv("DEFAULT_MODEL", "anthropic/claude-sonnet-4")

    # Vérifier le cache
    cache_key = hashlib.md5(f"{model}:{prompt}".encode()).hexdigest()
    cached = r.get(cache_key)
    if cached:
        result = json.loads(cached)
        result["cached"] = True
        return result

    # Appel API
    api_key = os.getenv("OPENROUTER_API_KEY")
    async with httpx.AsyncClient() as client:
        response = await client.post(
            "https://openrouter.ai/api/v1/chat/completions",
            headers={"Authorization": f"Bearer {api_key}"},
            json={
                "model": model,
                "messages": [{"role": "user", "content": prompt}],
                "max_tokens": 1000
            },
            timeout=30.0
        )

    data = response.json()
    result = {
        "text": data["choices"][0]["message"]["content"],
        "model": model,
        "cached": False,
        "usage": data.get("usage", {})
    }

    # Mettre en cache
    r.setex(cache_key, CACHE_TTL, json.dumps(result))

    return result

🔧 Tips avancés

Volumes : persister les données

services:
  api:
    volumes:
      # Bind mount : dossier local -> conteneur
      - ./data:/app/data

      # Named volume : géré par Docker
      - app_models:/app/models

      # Read-only (sécurité)
      - ./config:/app/config:ro

volumes:
  app_models:
    # Ce volume persiste même après docker compose down
    # Supprimé uniquement avec docker compose down -v

Règle d'or : tout ce qui doit survivre à un docker compose down doit être dans un volume.

Networking : communication entre conteneurs

services:
  api:
    networks:
      - frontend  # Accessible depuis l'extérieur
      - backend   # Communication interne

  db:
    networks:
      - backend   # PAS accessible depuis l'extérieur

  nginx:
    networks:
      - frontend
    ports:
      - "80:80"   # Seul point d'entrée

networks:
  frontend:
  backend:
    internal: true  # Pas d'accès internet

Les conteneurs sur le même réseau se trouvent par nom de service :

# Dans le conteneur 'api', on accède à 'db' par son nom
DATABASE_URL = "postgresql://app:password@db:5432/myapp"
#                                         ^^
#                              Nom du service Docker

Health checks : ne pas démarrer tant que ce n'est pas prêt

services:
  api:
    depends_on:
      db:
        condition: service_healthy  # Attend que db soit healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s      # Vérifier toutes les 30s
      timeout: 10s       # Timeout de la vérification
      retries: 3         # Nombre d'échecs avant "unhealthy"
      start_period: 40s  # Délai avant la première vérification

Optimiser les images Docker

# ❌ Mauvais : image énorme
FROM python:3.11
RUN pip install torch transformers fastapi uvicorn

# ✅ Bon : image optimisée
FROM python:3.11-slim

# Installer les dépendances en premier (cache Docker)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copier le code ensuite (changements plus fréquents)
COPY . .

Pourquoi l'ordre compte ? Docker met en cache chaque couche (layer). Si requirements.txt n'a pas changé, Docker réutilise le cache pour pip install — même si votre code a changé.

Astuce Impact
Utiliser -slim ou -alpine Image 3-5x plus petite
--no-cache-dir sur pip Pas de cache pip dans l'image
Ordonner les COPY (dépendances avant code) Builds plus rapides
Multi-stage builds Image finale minimale
.dockerignore Exclure fichiers inutiles du contexte

Le .dockerignore

# .dockerignore
.git
.env
__pycache__
*.pyc
node_modules
.vscode
*.md
tests/
docker-compose.yml

🔐 Sécurité Docker

Les bases

services:
  api:
    # Ne PAS tourner en root
    user: "1000:1000"

    # Limiter les ressources
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 2G
        reservations:
          cpus: '0.5'
          memory: 512M

    # Lecture seule sauf les volumes explicites
    read_only: true
    tmpfs:
      - /tmp

    # Pas de privilèges escalation
    security_opt:
      - no-new-privileges:true

Secrets : ne jamais mettre dans l'image

# ❌ JAMAIS ça
ENV API_KEY=sk-1234567890

# ✅ Passer via .env
docker compose --env-file .env up -d

# ✅ Ou via Docker secrets (Swarm mode)
echo "sk-1234567890" | docker secret create api_key -

Scanner les vulnérabilités

# Scanner une image pour les vulnérabilités connues
docker scout cves mon-image:latest

# Ou avec Trivy (gratuit)
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
  aquasec/trivy image mon-image:latest

📊 Monitoring des conteneurs

Intégrez le monitoring Docker avec votre surveillance serveur IA :

# Ressources en temps réel
docker stats

# Format personnalisé
docker stats --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}"

# Santé des conteneurs
docker inspect --format='{{.Name}}: {{.State.Health.Status}}' $(docker ps -q)

Script de monitoring Docker pour OpenClaw

#!/bin/bash
# /root/scripts/docker-health.sh

echo "=== DOCKER HEALTH ==="
echo "Date: $(date -u +'%Y-%m-%d %H:%M UTC')"
echo ""

# Conteneurs en cours
echo "--- RUNNING CONTAINERS ---"
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" 2>/dev/null
echo ""

# Conteneurs arrêtés (potentiel problème)
STOPPED=$(docker ps -a --filter "status=exited" --format "{{.Names}}: exited {{.Status}}" 2>/dev/null)
if [ -n "$STOPPED" ]; then
    echo "--- STOPPED CONTAINERS (potential issue) ---"
    echo "$STOPPED"
    echo ""
fi

# Ressources
echo "--- RESOURCE USAGE ---"
docker stats --no-stream --format "{{.Name}}: CPU={{.CPUPerc}} MEM={{.MemUsage}}" 2>/dev/null
echo ""

# Espace disque Docker
echo "--- DOCKER DISK USAGE ---"
docker system df 2>/dev/null
echo ""

# Health checks
echo "--- HEALTH STATUS ---"
for id in $(docker ps -q 2>/dev/null); do
    NAME=$(docker inspect --format='{{.Name}}' $id | tr -d '/')
    HEALTH=$(docker inspect --format='{{if .State.Health}}{{.State.Health.Status}}[[else]]no-healthcheck[[end]]' $id)
    echo "$NAME: $HEALTH"
done

🚀 Déploiement en production

Checklist pré-production

# 1. Vérifier que .env n'est pas dans git
grep -r ".env" .gitignore

# 2. Vérifier les health checks
docker compose ps  # Tous en "healthy" ?

# 3. Vérifier les restart policies
grep "restart:" docker-compose.yml  # Tous en "unless-stopped" ?

# 4. Vérifier les limites de ressources
grep -A5 "resources:" docker-compose.yml

# 5. Tester un arrêt/relance
docker compose down && docker compose up -d
# Tout redémarre correctement ?

# 6. Tester la persistance
docker compose down
docker compose up -d
# Les données sont toujours là ?

Mise à jour sans downtime

# Rebuilder uniquement le service modifié
docker compose build api

# Relancer avec zero-downtime (si vous avez un load balancer)
docker compose up -d --no-deps api

# Vérifier
docker compose ps
docker compose logs -f api --tail=20

Automatiser avec OpenClaw

Configurez votre agent OpenClaw pour gérer le déploiement :

## Docker Deployment Manager

Quand je dis "déploie [service]" :
1. `cd /root/projects/mon-api && git pull`
2. `docker compose build [service]`
3. `docker compose up -d --no-deps [service]`
4. Attendre 30 secondes
5. Vérifier les health checks
6. Si healthy → confirmer sur Telegram
7. Si unhealthy → rollback et alerter

🗺️ Architectures types

Simple : API + Base de données

[Nginx:80/443] → [FastAPI:8000] → [SQLite (volume)]

Parfait pour : un projet personnel, un MVP, un outil interne.

Intermédiaire : API + PostgreSQL + Cache

[Nginx:80/443] → [FastAPI:8000] → [PostgreSQL:5432]
                                  → [Redis:6379]

Parfait pour : une app avec des utilisateurs, besoin de performance.

Avancé : Pipeline LLM complet

[Nginx:80/443] → [API Gateway:8080] → [LLM Service:8001] → [OpenRouter API]
                                     → [Worker] → [Redis:6379]
                                     → [PostgreSQL:5432]

Parfait pour : un produit SaaS, une plateforme IA.


⚠️ Erreurs courantes

Erreur Symptôme Solution
Pas de volume pour la DB Données perdues au restart Ajouter un volumes:
Port déjà utilisé "bind: address already in use" Changer le port ou stopper le conflit
Oublier depends_on "Connection refused" au démarrage Ajouter les dépendances + health checks
Image trop grosse Build lent, disque plein Utiliser -slim, .dockerignore, multi-stage
Secrets dans l'image Fuite de credentials Utiliser .env + variables d'environnement
Pas de restart policy Services qui ne reviennent pas après reboot restart: unless-stopped
Logs qui remplissent le disque Disque plein après quelques semaines Configurer le log driver

Limiter les logs

services:
  api:
    logging:
      driver: "json-file"
      options:
        max-size: "10m"    # Max 10MB par fichier
        max-file: "3"      # Max 3 fichiers de rotation

📚 Articles liés