Docker Compose
Le fichier docker-compose.yml, à la racine du dépôt, décrit toute l'infrastructure de BlockLucky : les dépendances (PostgreSQL, Redis, Mailpit, nœud Hardhat) et, derrière le profil app, l'application complète (migrations, API, worker, frontend web, site de l'Office de Tourisme). Le projet Compose s'appelle blocklucky (clé name), ce qui préfixe les volumes et le réseau.
Cette page décrit le fichier tel qu'il est. Pour lancer la stack pas à pas, voir Stack complète en conteneurs ; pour le contenu des images, voir Images Docker.
Services
| Service | Image ou build | Profil | Ports | Dépend de | Sonde | Rôle |
|---|---|---|---|---|---|---|
postgres |
postgres:15-alpine |
par défaut | 127.0.0.1:5434:5432 |
- | oui (Compose) | Base PostgreSQL 15 de l'application (comptes, tickets indexés, EtherPoints, récompenses). Charge pg_stat_statements, journalise les requêtes de plus de 500 ms et les attentes de verrou, coupe les transactions inactives après 60 s. Données dans le volume pgdata. |
redis |
redis:7.4-alpine |
par défaut | 127.0.0.1:6380:6379 |
- | oui (Compose) | Redis 7.4 : files BullMQ, rate limiting partagé, verrous, idempotence et cache. Persistance AOF (appendfsync everysec) dans le volume redisdata, 256 Mo maximum avec la politique noeviction pour ne jamais perdre un job. |
mailpit |
axllent/mailpit:v1.31.4 |
par défaut | 127.0.0.1:1025:1025, 127.0.0.1:8025:8025 |
- | oui (Compose) | Serveur SMTP de capture : reçoit tous les emails du backend (codes de connexion, alertes, RGPD) sur le port 1025 et les affiche dans une interface web sur le port 8025. Aucun email ne sort vers Internet. |
blockchain |
build ./smart-contract |
par défaut | 127.0.0.1:8545:8545 |
- | oui (Dockerfile) | Nœud Hardhat (chain ID 31337) construit depuis smart-contract/. Au démarrage, déploie le coordinateur VRF simulé, BlockLucky et BlockLuckyPOAP, habilite et approvisionne le compte opérateur OPERATOR_PRIVATE_KEY, puis lance l'oracle VRF local. État en mémoire, perdu à chaque redémarrage. |
migrate |
build ./backend, cible migrator |
app | - | postgres (healthy) | oui (Dockerfile) | Tâche ponctuelle (cible migrator du Dockerfile backend) qui applique prisma migrate deploy puis s'arrête. Le backend et le worker attendent sa réussite. |
backend |
build ./backend, cible runtime |
app | 2053:2053 |
migrate (completed_successfully), redis (healthy), blockchain (healthy) | oui (Dockerfile) | API Express et serveur Socket.IO sur le port 2053, avec RUN_WORKERS=false : ni workers BullMQ, ni indexeur, ni keeper dans ce process. |
worker |
build ./backend, cible runtime |
app | - | migrate (completed_successfully), redis (healthy), blockchain (healthy) | oui (Compose) | Même image que le backend, lancée sur dist/src/worker.js : workers BullMQ (emails, EtherPoints, actions on-chain), indexeur des événements du contrat et keeper du tirage. Sonde de vivacité sur le port interne 2054. |
web |
build ./frontend-web |
app | 3000:3000 |
backend (healthy) | oui (Dockerfile) | Frontend Next.js en sortie standalone, servi par node server.js sur le port 3000. Les URL de l'API sont figées au build par les arguments NEXT_PUBLIC_API_URL et NEXT_PUBLIC_WS_URL. |
tourisme |
build ./frontend-office-tourisme |
app | 4000:8080 |
- | oui (Dockerfile) | Site statique de l'Office de Tourisme servi par nginx non privilégié sur le port 8080 du conteneur, publié sur le port 4000 de l'hôte. |
Profils
Compose ne démarre un service doté d'une clé profiles que si ce profil est demandé. Deux modes d'utilisation en découlent.
| Commande | Services démarrés | Usage |
|---|---|---|
docker compose up -d --wait |
postgres, redis, mailpit, blockchain |
Développement : le backend, le web et le mobile tournent sur le poste avec npm run dev et se connectent aux ports publiés sur 127.0.0.1. |
docker compose --profile app up -d --build --wait |
les quatre précédents, puis migrate, backend, worker, web, tourisme |
Stack complète conteneurisée, proche de la production. |
--wait fait attendre la commande jusqu'à ce que chaque service soit sain (ou terminé pour migrate). Le service blockchain n'a pas de profil : il démarre aussi en mode développement, car le backend fonctionne par défaut en BLOCKCHAIN_MODE=onchain (voir Mode onchain ou simulé).
Ordre de démarrage
Les dépendances sont déclarées par depends_on avec une condition. Compose n'ouvre un service que lorsque la condition de chacune de ses dépendances est remplie.
flowchart LR
PG["postgres (healthy)"] --> MIG["migrate (completed)"]
MIG --> API["backend (healthy)"]
MIG --> WK["worker"]
RD["redis (healthy)"] --> API
RD --> WK
BC["blockchain (healthy)"] --> API
BC --> WK
API --> WEB["web"]
TOU["tourisme"]
MP["mailpit"]migrateattend que PostgreSQL réponde àpg_isready, applique les migrations et s'arrête (restart: "no"). Un échec de migration bloque donc le démarrage de l'API et du worker : c'est voulu, l'application ne doit jamais tourner sur un schéma incomplet.backendetworkerattendent la fin réussie demigrate, un Redis sain et une blockchain saine. La blockchain n'est saine qu'une fois les contrats déployés (fichier témoin/tmp/blockchain-ready), ce qui peut prendre jusqu'à une minute au premier démarrage.webattend que l'API soit saine. Sa sonde utiliseHEALTHCHECKdu Dockerfile backend (/health/live).tourismeetmailpitn'ont aucune dépendance.
depends_on ne joue qu'au démarrage. Si Redis redémarre plus tard, l'API et le worker ne sont pas relancés automatiquement : voir Runbooks.
Sondes de santé
| Service | Origine de la sonde | Commande | Rythme |
|---|---|---|---|
postgres |
Compose | pg_isready -U etherbay_admin -d blocklucky_db |
5 s, 10 essais, 10 s de grâce |
redis |
Compose | redis-cli ping |
5 s, 10 essais |
mailpit |
Compose | /mailpit readyz |
15 s, 5 essais |
blockchain |
Dockerfile de smart-contract/ |
test -f /tmp/blockchain-ready |
5 s, 20 essais, 60 s de grâce |
backend |
Dockerfile de backend/ |
fetch sur http://127.0.0.1:2053/health/live |
15 s, 3 essais, 20 s de grâce |
worker |
Compose (remplace celle de l'image) | fetch sur http://127.0.0.1:2054/health/live |
15 s, 3 essais |
web |
Dockerfile de frontend-web/ |
fetch sur http://127.0.0.1:3000/, sain si statut inférieur à 500 |
15 s, 3 essais, 15 s de grâce |
tourisme |
Dockerfile de frontend-office-tourisme/ |
wget sur http://127.0.0.1:8080/health |
30 s |
La colonne Sonde du tableau précise si la sonde est écrite dans le fichier Compose ou héritée de l'instruction HEALTHCHECK du Dockerfile. La sonde du worker porte sur le petit serveur HTTP ouvert par backend/src/worker.ts sur PORT + 1, qui répond 503 pendant l'arrêt.
Durcissement
L'ancre YAML x-hardening regroupe les options de sécurité appliquées par fusion (<<: *hardening) à migrate, backend, worker, web et tourisme :
x-hardening: &hardening
read_only: true
cap_drop: [ALL]
security_opt: [no-new-privileges:true]
tmpfs:
- /tmp:size=64m| Option | Effet |
|---|---|
read_only: true |
Le système de fichiers racine du conteneur est en lecture seule. Un attaquant qui obtient une exécution de code ne peut ni modifier le code de l'application, ni déposer un binaire persistant. |
cap_drop: [ALL] |
Retire toutes les capacités Linux (par exemple NET_RAW, CHOWN, SETUID). Aucun processus n'en a besoin : tous tournent sans privilège et écoutent sur des ports supérieurs à 1024. |
no-new-privileges:true |
Interdit tout gain de privilège par un binaire setuid ou setgid. |
tmpfs: /tmp:size=64m |
Seul répertoire inscriptible, en mémoire et limité à 64 Mo, vidé à chaque redémarrage. |
Le service web redéfinit tmpfs pour ajouter /app/.next/cache (128 Mo, propriétaire uid et gid 1000, c'est-à-dire l'utilisateur node) : le serveur Next.js y écrit le cache d'optimisation des images. Comme la clé tmpfs remplace celle de l'ancre au lieu de la compléter, /tmp y est répété.
Le service blockchain n'utilise pas l'ancre complète : il garde cap_drop et no-new-privileges, mais pas read_only, car Hardhat écrit dans /app (cache de compilation, deployments/localhost.json) et dans /tmp. Les services d'images tierces (postgres, redis, mailpit) ne sont pas durcis par Compose. Tous les conteneurs applicatifs tournent en outre sous un utilisateur non root défini dans leur image.
Réseau
Un seul réseau, default, est déclaré avec un sous-réseau fixe :
networks:
default:
ipam:
driver: default
config:
- subnet: 172.40.0.0/16Les services se joignent par leur nom (postgres:5432, redis:6379, blockchain:8545, mailpit:1025). Le sous-réseau est fixé pour éviter qu'il chevauche un réseau local ou d'entreprise attribué automatiquement par Docker.
Ports publiés sur l'hôte
| Port hôte | Service | Liaison |
|---|---|---|
5434 |
PostgreSQL | 127.0.0.1 uniquement |
6380 |
Redis | 127.0.0.1 uniquement |
1025, 8025 |
Mailpit (SMTP, interface web) | 127.0.0.1 uniquement |
8545 |
Nœud Hardhat | 127.0.0.1 uniquement |
2053 |
API | toutes les interfaces |
3000 |
Frontend web | toutes les interfaces |
4000 |
Office de Tourisme (port 8080 du conteneur) | toutes les interfaces |
Les ports de PostgreSQL et Redis sont décalés (5434, 6380) pour ne pas entrer en conflit avec une instance installée sur le poste. Les ports applicatifs sont ouverts sur toutes les interfaces pour qu'un téléphone du réseau local atteigne l'API pendant le développement.
Attention
Les ports publiés par Docker contournent le pare-feu ufw de l'hôte, car Docker écrit ses propres règles iptables. Sur un serveur exposé à Internet, il faut publier 2053, 3000 et 4000 sur 127.0.0.1 et laisser le proxy inverse seul face au réseau. La procédure est décrite dans Déploiement sur etherbay.tech.
Volumes
| Volume | Monté sur | Contenu |
|---|---|---|
pgdata |
/var/lib/postgresql/data du service postgres |
Données PostgreSQL |
redisdata |
/data du service redis |
Fichiers AOF de Redis |
./docker/postgres/init (bind, lecture seule) |
/docker-entrypoint-initdb.d |
01-extensions.sql, qui crée l'extension pg_stat_statements à l'initialisation d'un volume vierge |
Le script d'initialisation ne s'exécute qu'à la création du volume pgdata. Sur un volume plus ancien, l'extension se crée à la main (voir Runbooks).
La blockchain n'a pas de volume. docker compose down suivi de up repart d'une chaîne neuve, alors que PostgreSQL garde les tickets et le curseur d'indexation de l'ancienne. Pour repartir de zéro, supprimer aussi les volumes avec docker compose down -v. Le cas d'un redémarrage involontaire est traité dans Runbooks blockchain.
Configuration de PostgreSQL et Redis
PostgreSQL est lancé avec ces paramètres, passés en ligne de commande :
| Paramètre | Valeur | Raison |
|---|---|---|
shared_preload_libraries |
pg_stat_statements |
Statistiques par requête normalisée |
pg_stat_statements.track |
all |
Inclut les requêtes imbriquées |
log_min_duration_statement |
500 |
Journalise toute requête de plus de 500 ms |
log_lock_waits |
on |
Journalise les attentes de verrou au-delà de deadlock_timeout |
idle_in_transaction_session_timeout |
60000 |
Coupe une transaction ouverte et inactive depuis 60 s |
max_connections |
100 |
Plafond partagé par toutes les instances de l'API et du worker |
Redis est lancé avec --appendonly yes --appendfsync everysec (au plus une seconde d'écritures perdues en cas de crash), --maxmemory 256mb --maxmemory-policy noeviction (Redis refuse les écritures plutôt que d'évincer une clé, donc un job BullMQ n'est jamais supprimé en silence) et --slowlog-log-slower-than 10000 (commandes de plus de 10 ms dans le slowlog).
Variables passées au backend
L'ancre x-backend-env est partagée par backend et worker. Les valeurs sans ${...} sont écrites en dur dans le fichier ; les autres viennent de l'environnement du shell ou d'un fichier .env placé à la racine du dépôt, à côté de docker-compose.yml.
| Variable | Valeur | Remarque |
|---|---|---|
NODE_ENV |
production |
Active les contrôles de production de env.config.ts |
DATABASE_URL |
postgresql://etherbay_admin:etherbay_password@postgres:5432/blocklucky_db?schema=public |
Identifiants de démonstration en clair |
REDIS_URL |
redis://redis:6379 |
|
BLOCKCHAIN_MODE |
onchain |
Obligatoire en production |
RPC_URL, CHAIN_ID |
http://blockchain:8545, 31337 |
Nœud Hardhat du service blockchain |
LOTTERY_CONTRACT_ADDRESS, POAP_CONTRACT_ADDRESS |
0xCf7E…0Fc9, 0x5FC8…5707 |
Adresses déterministes du déploiement local |
SMTP_HOST, SMTP_PORT |
mailpit, 1025 |
Emails capturés par Mailpit |
FRONTEND_URL |
http://localhost:3000 |
Liens des emails, parrainage, métadonnées POAP |
CORS_ORIGINS |
http://localhost:3000,http://localhost:8081 |
Origines web et Expo autorisées |
JWT_SECRET, ENCRYPTION_KEY, RELAYER_PRIVATE_KEY, WEBHOOK_SECRET, METRICS_TOKEN |
${NOM:-} |
Secrets à fournir, vides sinon |
Le backend reçoit en plus RUN_WORKERS: "false". Toutes les autres variables prennent la valeur par défaut du schéma (voir Variables d'environnement) : notamment TRUST_PROXY=false, PORT=2053, AWS_S3_ENDPOINT=http://localhost:9000 (aucun service S3 n'est fourni, les envois échouent et sont journalisés en avertissement) et FALLBACK_RPC_URL vers un RPC public Sepolia.
Secrets obligatoires
Avec NODE_ENV=production, le schéma de configuration refuse de démarrer si une valeur est absente ou égale à sa valeur de développement :
JWT_SECRET: 16 caractères au moins ;ENCRYPTION_KEY: exactement 64 caractères hexadécimaux ;RELAYER_PRIVATE_KEY: clé privée0xsuivie de 64 caractères hexadécimaux, différente de la clé du compte Hardhat n° 0 ;WEBHOOK_SECRET: 8 caractères au moins ;METRICS_TOKEN: obligatoire tant queMETRICS_ENABLEDvauttrue(valeur par défaut).
La substitution ${NOM:-} transmet une chaîne vide quand la variable manque. Une chaîne vide n'est pas « absente » pour Zod : la valeur par défaut ne s'applique pas et la validation échoue. Le conteneur s'arrête alors avec le message Configuration invalide, démarrage interrompu suivi de la liste des variables en cause. Un exemple de génération des valeurs :
cat > .env <<EOF
JWT_SECRET=$(openssl rand -hex 32)
ENCRYPTION_KEY=$(openssl rand -hex 32)
RELAYER_PRIVATE_KEY=0x$(openssl rand -hex 32)
WEBHOOK_SECRET=$(openssl rand -hex 24)
METRICS_TOKEN=$(openssl rand -hex 24)
EOFDanger
ENCRYPTION_KEY chiffre les clés privées des portefeuilles intégrés et les secrets TOTP. La perdre rend tous les portefeuilles inutilisables ; la changer impose de rechiffrer les données (voir Clés et garde des portefeuilles). Le fichier .env ne doit jamais être commité.
Service blockchain et compte opérateur
Le service blockchain est construit depuis smart-contract/Dockerfile et lance start-local-chain.sh, qui enchaîne :
npx hardhat node --hostname 0.0.0.0 --port 8545en arrière-plan ;- une boucle qui interroge
eth_chainIdjusqu'à ce que le nœud réponde ; npx hardhat run scripts/deploy-local.ts --network localhost, qui déploieVRFCoordinatorV2_5Mock, crée et finance un abonnement, déploieBlockLucky(tours de 30 minutes) etBlockLuckyPOAP, enregistre le contrat comme consommateur VRF, puis répartit les rôles (administrateur et propriétaire VRF au compte n° 2, trésorerie au compte n° 1, opérateur selon la clé reçue) ;- la création de
/tmp/blockchain-ready, qui rend la sonde saine ; scripts/local-vrf-responder.tsen arrière-plan : il surveille les événementsRandomWordsRequestedtoutes les 2 secondes et répond avec le compte Hardhat n° 9.
Le service reçoit deux variables :
| Variable | Valeur | Rôle |
|---|---|---|
OPERATOR_PRIVATE_KEY |
${RELAYER_PRIVATE_KEY:-} |
Clé du compte opérateur du backend |
POAP_BASE_URI |
http://localhost:2053/api/v1/lottery/poap/ |
Préfixe des métadonnées des badges (tokenURI) |
OPERATOR_PRIVATE_KEY est la même clé que RELAYER_PRIVATE_KEY côté backend. deploy-local.ts en déduit l'adresse, lui accorde OPERATOR_ROLE et PAUSER_ROLE sur BlockLucky, MINTER_ROLE sur BlockLuckyPOAP et, en local, DEFAULT_ADMIN_ROLE en administrateur additionnel pour setTreasury (sauf LOCAL_STRICT_ROLES=true). Si elle diffère du compte qui déploie (compte Hardhat n° 0, ce qui est le cas avec le profil app), le script lui envoie 1 000 ethers fictifs (OPERATOR_FUNDING_ETH) et retire tous ses rôles au compte n° 0. La répartition est faite par scripts/lib/roles.ts, commun au déploiement Sepolia. Sans cette habilitation, le keeper ne pourrait pas demander le tirage, le disjoncteur serait refusé et les badges ne seraient pas émis. Le rôle du compte opérateur est détaillé dans Client RPC et compte opérateur.
Les adresses des contrats sont déterministes : elles dépendent seulement du compte qui déploie et de son nonce. Les transactions de rôles sont envoyées après les deux déploiements et ne modifient pas ces adresses. Tant que l'ordre des transactions de deploy-local.ts ne change pas, BlockLucky est en 0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9 et BlockLuckyPOAP en 0x5FC8d32690cc91D4c39d9d3abcBD16989F875707, les valeurs écrites dans x-backend-env. Modifier le script de déploiement impose de vérifier ces adresses.
Important
Le nœud Hardhat garde son état en mémoire. Tout redémarrage du conteneur (docker compose restart blockchain, redémarrage de Docker ou de la machine, restart: unless-stopped après un crash) recrée une chaîne vierge aux mêmes adresses. La base PostgreSQL, elle, conserve tickets, tours et curseur : voir Runbooks blockchain.
Arrêt
backend et worker déclarent stop_grace_period: 30s. À docker compose stop, Docker envoie SIGTERM (le Dockerfile fixe STOPSIGNAL SIGTERM) et attend 30 secondes avant SIGKILL. Le backend ferme ses ressources dans l'ordre et se force à quitter après SHUTDOWN_TIMEOUT_MS (25 s par défaut), donc avant l'échéance de Docker. Le détail de la séquence est dans Observabilité.
Valider le fichier
La CI vérifie la syntaxe et la résolution des ancres, profil app compris :
docker compose --profile app config -qSans -q, la commande affiche le fichier final fusionné, utile pour contrôler une surcharge.