Modèles Docker
Bonnes pratiques Docker et Docker Compose pour le développement conteneurisé.
Docker Compose pour le Développement Local
Stack Web Standard
# docker-compose.yml
services:
app:
build:
context: .
target: dev # Utilise l'étape dev du Dockerfile multi-étapes
ports:
- "3000:3000"
volumes:
- .:/app # Montage bind pour le rechargement à chaud
- /app/node_modules # Volume anonyme - préserve les dépendances du conteneur
environment:
- DATABASE_URL=postgres://postgres:postgres@db:5432/app_dev
- REDIS_URL=redis://redis:6379/0
- NODE_ENV=development
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
command: npm run dev
db:
image: postgres:16-alpine
ports:
- "5432:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: app_dev
volumes:
- pgdata:/var/lib/postgresql/data
- ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redisdata:/data
mailpit: # Test d'email local
image: axllent/mailpit
ports:
- "8025:8025" # Interface Web
- "1025:1025" # SMTP
volumes:
pgdata:
redisdata:
Dockerfile de Développement vs Production
# Étape : dépendances
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# Étape : dev (rechargement à chaud, outils de débogage)
FROM node:22-alpine AS dev
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]
# Étape : build
FROM node:22-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build && npm prune --production
# Étape : production (image minimale)
FROM node:22-alpine AS production
WORKDIR /app
RUN addgroup -g 1001 -S appgroup && adduser -S appuser -u 1001
USER appuser
COPY --from=build --chown=appuser:appgroup /app/dist ./dist
COPY --from=build --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=build --chown=appuser:appgroup /app/package.json ./
ENV NODE_ENV=production
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"]
Fichiers de Surcharge
# docker-compose.override.yml (chargé automatiquement, paramètres dev uniquement)
services:
app:
environment:
- DEBUG=app:*
- LOG_LEVEL=debug
ports:
- "9229:9229" # Débogueur Node.js
# docker-compose.prod.yml (explicite pour la production)
services:
app:
build:
target: production
restart: always
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
# Développement (charge automatiquement la surcharge)
docker compose up
# Production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Mise en Réseau
Découverte de Services
Les services dans le même réseau Compose se résolvent par nom de service :
# Depuis le conteneur "app" :
postgres://postgres:postgres@db:5432/app_dev # "db" résout le conteneur db
redis://redis:6379/0 # "redis" résout le conteneur redis
Réseaux Personnalisés
services:
frontend:
networks:
- frontend-net
api:
networks:
- frontend-net
- backend-net
db:
networks:
- backend-net # Accessible uniquement depuis api, pas depuis frontend
networks:
frontend-net:
backend-net:
Exposition Uniquement du Nécessaire
services:
db:
ports:
- "127.0.0.1:5432:5432" # Accessible uniquement depuis l'hôte, pas du réseau
# Omettre complètement les ports en production -- accessible uniquement dans le réseau Docker
Stratégies de Volumes
volumes:
# Volume nommé : persiste entre les redémarrages de conteneur, géré par Docker
pgdata:
# Montage bind : mappe un répertoire hôte dans le conteneur (pour le développement)
# - ./src:/app/src
# Volume anonyme : préserve le contenu généré par le conteneur de la surcharge du montage bind
# - /app/node_modules
Modèles Courants
services:
app:
volumes:
- .:/app # Code source (montage bind pour rechargement à chaud)
- /app/node_modules # Protège le node_modules du conteneur de l'hôte
- /app/.next # Protège le cache de build
db:
volumes:
- pgdata:/var/lib/postgresql/data # Données persistantes
- ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql # Scripts d'initialisation
Sécurité des Conteneurs
Durcissement du Dockerfile
# 1. Utiliser des tags spécifiques (jamais :latest)
FROM node:22.12-alpine3.20
# 2. Exécuter en tant que non-root
RUN addgroup -g 1001 -S app && adduser -S app -u 1001
USER app
# 3. Supprimer les capacités (dans compose)
# 4. Système de fichiers racine en lecture seule si possible
# 5. Pas de secrets dans les couches de l'image
Sécurité Compose
services:
app:
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /tmp
- /app/.cache
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # Uniquement si liaison à des ports < 1024
Gestion des Secrets
# BON : Utiliser des variables d'environnement (injectées à l'exécution)
services:
app:
env_file:
- .env # Ne jamais commiter .env dans git
environment:
- API_KEY # Hérite de l'environnement hôte
# BON : Secrets Docker (mode Swarm)
secrets:
db_password:
file: ./secrets/db_password.txt
services:
db:
secrets:
- db_password
# MAUVAIS : Codé en dur dans l'image
# ENV API_KEY=sk-proj-xxxxx # NE JAMAIS FAIRE CELA
Harnesses d'Installateur CLI Durcis
Utiliser des conteneurs pour tester le comportement de l'installateur contre des copies jetables de projet sans permettre au test de modifier le répertoire source.
Respecter la Frontière de Plateforme
- Exécuter des conteneurs réels pour les distributions Linux telles que Debian et Ubuntu.
- macOS ne peut pas s'exécuter comme un conteneur Docker car Docker partage un noyau Linux. Exécuter le même point d'entrée de test sans shell nativement sur macOS.
- Les conteneurs Windows nécessitent un moteur Docker Windows. Exécuter la logique indépendante de la plateforme sur un exécuteur CI Windows natif et réserver les conteneurs Windows pour un hôte Windows.
- Conserver une matrice CI Ubuntu/macOS/Windows native pour les chemins spécifiques à l'hôte, les shims de commande, les guillemets et le comportement du système de fichiers.
Ne pas prétendre qu'un conteneur Linux valide le comportement macOS ou Windows.
Appliquer le Contrat d'Isolation
- Épingler les images de base par digest immuable et épingler les versions CLI installées.
- Exécuter en tant qu'UID/GID numérique non-root lorsque les noms de compte de distribution diffèrent.
- Monter le dépôt et le projet source en lecture seule.
- Copier le projet source dans un espace de travail
tmpfsinscriptible avant toute mutation. - Monter
/workspaceavecnoexec, UID/GID 1000, etmode=0700afin que seul l'utilisateur du conteneur puisse inspecter les données du projet. - Conserver le cache exécutable de npm et npx à
NPM_CONFIG_CACHE=/tmp/npm-cachesur le montage exécutable/tmp. Sa taille par défaut est de 2 Go et peut être ajustée avecECC_TMPFS_SIZE;ECC_WORKSPACE_SIZEcontrôle séparément le montage de l'espace de travail privé. - Définir
read_only: true,no-new-privileges:true,cap_drop: [ALL], et unpids_limitfini. - Conserver les services CLI réels par défaut sur
network_mode: none. Ajouter l'accès réseau uniquement via un service opt-in nommé visiblement pour une session de fournisseur authentifié ; ne jamais en faire une valeur par défaut accidentelle pilotée par l'environnement. - Créer uniquement les chemins temporaires inscriptibles dont l'outil a besoin.
- Ne pas passer les identifiants de l'hôte dans le conteneur par défaut.
- Par défaut, passer en mode dry run et autoriser uniquement les modes explicites
dry-run,install,plugin, etshell. - Utiliser des tableaux d'arguments ou
spawnSync(..., { shell: false })pour les exécuteurs multiplateformes. Ne jamais interpoler les chemins de projet dans une commande shell.
Utiliser le Harness de Configuration du Plugin ECC
Utiliser docker/plugin-setup/compose.yaml comme implémentation de référence. Il fournit :
fixture-testspour la suite ciblée de manifeste d'installation, de cible et d'exécuteur.real-clipour l'image Linux générique basée sur Debian épinglée.real-cli-ubuntupour l'image Ubuntu épinglée.
Valider le modèle Compose avant de construire :
docker compose -f docker/plugin-setup/compose.yaml config --quiet
Construire les deux images Linux réelles :
docker compose -f docker/plugin-setup/compose.yaml \
build real-cli real-cli-ubuntu
Exécuter le flux par défaut sécurisé dans chaque image :
docker compose -p ecc-plugin-debian-test \
-f docker/plugin-setup/compose.yaml \
run --rm -T real-cli dry-run
docker compose -p ecc-plugin-ubuntu-test \
-f docker/plugin-setup/compose.yaml \
run --rm -T real-cli-ubuntu dry-run
Le dry run exécute le contrat de commande publique actuel :
ecc install --profile core --target claude-project --dry-run --json
Avant que cette commande ne s'exécute, le conteneur crée un artefact npm empaqueté localement à partir du répertoire en lecture seule avec npm pack --ignore-scripts. Il extrait le tarball auto-créé sous /tmp, valide le nom du paquet ecc-universal, les manifestes d'installation requis, et le mapping package.json bin.ecc confiné, puis invoque l'exécutable ecc extrait. L'exécution reste sur network_mode: none, n'exécute pas les scripts de cycle de vie du paquet, et ne repose pas sur les node_modules de l'hôte ; ses dépendances de production exactes épinglées sont déjà présentes dans l'image.
Le harness rejette un plan vide, une cible autre que claude-project, toute opération en dehors de /workspace/project/.claude, ou tout dry run qui crée le répertoire cible. install effectue l'application isolée deux fois, vérifie son état d'installation géré, liste la cible installée et exécute doctor.
Démarrer, Ouvrir, Reconnecter et Nettoyer une Session Nommée
Démarrer un conteneur détaché sans --rm afin que quitter un terminal ne supprime pas la session :
docker compose -p ecc-plugin-session \
-f docker/plugin-setup/compose.yaml \
run --detach --name ecc-plugin-shell real-cli shell
Le conteneur copie le fixture en lecture seule dans le répertoire privé stable /workspace/project. Confirmer qu'il est en cours d'exécution, puis émettre le côté Docker du contrat de données v1 de l'ouvreur de terminal :
docker inspect --format '{{.State.Running}}' ecc-plugin-shell
node docker/plugin-setup/interactive-plan.js \
--container ecc-plugin-shell \
--workdir /workspace/project \
--json \
-- bash
Le résultat JSON a exactement une limite executable et argv (plus contractVersion: 1) : l'exécutable est docker, et argv commence par exec, -it, et -w. Passer ces données à la compétence d'ouvreur de terminal séparée lorsqu'elle est installée. Ce harness Docker n'importe délibérément pas d'adaptateur de terminal, n'interpole pas de commande shell, ni ne gère un processus GUI hôte. Jusque-là, ouvrir le même PTY dans le terminal hôte actuel directement :
docker exec -it -w /workspace/project ecc-plugin-shell bash
Quitter le shell sans arrêter le conteneur détaché. Reconnecter avec la même commande docker exec -it. Lorsque terminé, supprimer le conteneur nommé exact et ses ressources de projet Compose :
docker rm --force ecc-plugin-shell
docker compose -p ecc-plugin-session \
-f docker/plugin-setup/compose.yaml \
down --remove-orphans
Les identifiants de l'hôte sont absents par défaut et les répertoires d'identifiants ne sont jamais montés. Le service par défaut n'a également aucun accès réseau. Lorsqu'une session de fournisseur authentifié a réellement besoin d'un réseau, construire real-cli d'abord, puis opt-in visiblement avec docker compose --profile networked run real-cli-networked shell. Préférer l'authentification à l'intérieur de cette session jetable. Si une exécution CI doit hériter d'un identifiant d'environnement hôte, rendre cet opt-in à l'invocation avec un indicateur explicite Compose --env NAME, comprendre que la valeur est inspectable et peut être exfiltrée pendant la durée de vie du conteneur, et supprimer le conteneur nommé exact immédiatement après.
Exécuter la même suite ciblée nativement sur l'hôte :
npm run test:plugin-setup-platform
Inspecter l'identité et l'environnement produits avant de faire confiance à l'image :
docker image inspect ecc-plugin-setup:debian ecc-plugin-setup:ubuntu
Nettoyer chaque projet de test nommé sans supprimer les volumes ou images non liés :
docker compose -p ecc-plugin-debian-test \
-f docker/plugin-setup/compose.yaml down --remove-orphans
docker compose -p ecc-plugin-ubuntu-test \
-f docker/plugin-setup/compose.yaml down --remove-orphans
.dockerignore
node_modules
.git
.env
.env.*
dist
coverage
*.log
.next
.cache
docker-compose*.yml
Dockerfile*
README.md
tests/
Débogage
Commandes Courantes
# Voir les logs
docker compose logs -f app # Suivre les logs de app
docker compose logs --tail=50 db # Dernières 50 lignes de db
# Exécuter des commandes dans un conteneur en cours d'exécution
docker compose exec app sh # Shell dans app
docker compose exec db psql -U postgres # Se connecter à postgres
# Inspecter
docker compose ps # Services en cours d'exécution
docker compose top # Processus dans chaque conteneur
docker stats # Utilisation des ressources
# Reconstruire
docker compose up --build # Reconstruire les images
docker compose build --no-cache app # Forcer une reconstruction complète
# Nettoyer
docker compose down # Arrêter et supprimer les conteneurs
docker compose down -v # Supprimer également les volumes (DESTRUCTIF)
docker system prune # Supprimer les images/conteneurs inutilisés
Débogage des Problèmes Réseau
# Vérifier la résolution DNS à l'intérieur du conteneur
docker compose exec app nslookup db
# Vérifier la connectivité
docker compose exec app wget -qO- http://api:3000/health
# Inspecter le réseau
docker network ls
docker network inspect <projet>_default
Anti-Modèles
# MAUVAIS : Utiliser ...
[snippet tronqué pour la traduction]