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
- Qu'est-ce qu'OpenClaw ? — L'agent IA pour automatiser vos déploiements Docker
- Installer OpenClaw sur un VPS — Le serveur qui hébergera vos conteneurs
- Configurer OpenClaw — Connecter Docker à votre agent IA
- Monitoring serveur avec l'IA — Surveiller vos conteneurs intelligemment
- Git + IA : automatiser ses commits — Versionner le code de vos services
- OpenRouter : accéder à tous les LLMs — L'API utilisée dans les exemples
- Hébergez votre infra sur Hostinger — VPS puissant avec 20% de remise
- Sécuriser son serveur — Protéger l'hôte Docker