3.3Exemple filé : application web + base de données
L'essentiel en 30 secondes
- Trois services, un réseau : api (build local), bdd (postgres + volume), adminer (console dev) — l'api joint bdd:5432 par nom, sans IP en dur.
- L'URL de connexion voyage en variable : DATABASE_URL lue par l'api — aucun hôte en dur dans le code, la config change sans toucher l'image.
- depends_on ordonne, retry fiabilise : la base démarre plus lentement — l'api réessaie sa connexion au démarrage, sinon le premier lancement crash.
Mini-cas filé — Idir câble la maquette de l'équipe
Idir, devops junior à Alger, doit fournir une maquette API + base que chacun lance en une commande : son premier essai met l'IP de la base en dur et crash une fois sur deux au démarrage. Il passe au câblage par noms (l'api joint bdd:5432), à l'URL en variable DATABASE_URL, au volume persistant — et ajoute une boucle de retry côté api. Résultat : up -d --build, l'api répond sur :3000, adminer sur :8081, la base n'est joignable que de l'intérieur ; down puis up : la table créée la veille est toujours là.
Ce même cas revient dans le cours, le schéma, le script et le défi 48h.
1. Trois services, un réseau, des noms — pas des IP
Une appli réelle, c'est api (VOTRE image, build local depuis ./api), bdd (postgres + volume nommé) et adminer (console web légère d'administration SQL, publiée en dev). Compose les branche sur le même réseau : l'api joint bdd:5432 par nom de service — les IP changent à chaque recréation, les noms restent. Jamais d'IP en dur dans le code : c'est la règle qui rend le stack portable.
L'api lit DATABASE_URL en variable (hôte = bdd, base, identifiants) : la config voyage, le code reste identique entre dev et prod. On publie l'api (:3000) et adminer (:8081, dev uniquement) ; la base, elle, n'est JAMAIS publiée — joignable uniquement de l'intérieur. Surface exposée minimale : seul ce qui doit sortir, sort.
2. L'ordre ne suffit pas : retry obligatoire, persistance prouvée
depends_on lance la base avant l'api, mais pas sa DISPONIBILITÉ : postgres met plusieurs secondes à accepter les connexions. Sans boucle de retry au démarrage, l'api crash au premier lancement puis « marche » au second — le symptôme classique. La différence entre une démo et un stack qui survit aux redémarrages, c'est ce retry avec délai côté application (le healthcheck ordonnance en plus, niveau 4.2).
La preuve que tout tient : down puis up — la table créée via adminer doit survivre, car elle vit dans le volume, pas dans le conteneur. Idir en fait son test de réception systématique : ce qui ne survit pas au redémarrage n'est pas persisté, point. Base persistée, jamais publiée, retry au démarrage : le trio du stack sain.
🔎 Exemple concret — Stack API + Postgres + Adminer, câblé et persisté
Idir écrit le compose de la maquette : variables, volume, réseau, adminer opt-in en dev.
services:
api:
build: ./api
ports: ["3000:3000"]
environment:
DATABASE_URL: ${DATABASE_URL}
depends_on: [bdd]
networks: [appnet]
bdd:
image: postgres:16.4
environment:
POSTGRES_USER: ${POSTGRES_USER:-app}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB:-appdb}
volumes: [pgdata:/var/lib/postgresql/data]
networks: [appnet]
adminer:
image: adminer:4.8
profiles: [dev]
ports: ["8081:8080"]
networks: [appnet]
volumes:
pgdata:
networks:
appnet:
# Lancer : docker compose --profile dev up -d --build
Lecture : l'api et adminer sont publiés, la base ne l'est pas — joignable de l'intérieur.
Leçon : ouvrez :3000 (api) et :8081 (adminer) ; down/up prouve que la table survit.
Vocabulaire de la section
- DATABASE_URL
- L'URL de connexion complète passée en variable : hôte = nom du service, jamais d'IP en dur.
Exemple simple : L'api d'Idir lit ${DATABASE_URL} pointant vers bdd.
Ce que ce n'est pas : l'IP trouvée hier dans inspect, copiée dans le code.
Réflexe : hôte = nom du service, config = variable.
- Retry
- La boucle de reconnexion au démarrage : l'api réessaie jusqu'à ce que la base accepte.
Exemple simple : Le retry qui guérit le crash du premier démarrage.
Ce que ce n'est pas : une connexion unique qui échoue si la base est lente.
Réflexe : toute connexion au démarrage = retry avec délai.
- Adminer
- Console web légère d'administration SQL, publiée en dev pour voir et remplir la base.
Exemple simple : Idir crée la table de test sur :8081.
Ce que ce n'est pas : la base publiée en production pour « débugger ».
Réflexe : console en dev uniquement, jamais en prod.
- Build local
- Service construit depuis un dossier (./api) au lieu d'une image distante : votre code conteneurisé.
Exemple simple : L'image api construite au up --build.
Ce que ce n'est pas : un code monté à la main dans un conteneur nginx.
Réflexe : --build après chaque changement de code.
- Surface exposée
- L'ensemble des ports publiés : on la minimise — seul le nécessaire sort.
Exemple simple : api + adminer (dev) publiés, bdd interne.
Ce que ce n'est pas : postgres publié sur l'hôte « au cas où ».
Réflexe : chaque port publié doit justifier son existence.
En pratique — Monter la mini-appli câblée et persistée
- Créez ./api (Dockerfile + serveur lisant DATABASE_URL avec retry au démarrage).
- Écrivez le compose ci-dessus (URL en variable, base non publiée) et lancez up -d --build.
- Vérifiez l'api (:3000) et adminer (:8081, profil dev), créez une table.
- Redémarrez tout (down puis up) : la table doit survivre — sinon, le volume est mal monté.
Script prêt à dire — livrer la maquette à l'équipe (30 s)
« Une commande : up -d --build avec le profil dev. L'api répond sur 3000, la console base sur 8081, la base n'est joignable que de l'intérieur. L'URL voyage en variable, le retry encaisse le démarrage, et down/up ne perd rien — testé. »
À dire en livrant la maquette ; montrer le down/up avec la table intacte.
Modèle copier-coller — API + base + console (valide)
services:
api:
build: ./api
ports:
- "${PORT:-3000}:3000"
environment:
DATABASE_URL: ${DATABASE_URL}
depends_on:
- bdd
networks:
- appnet
bdd:
image: postgres:16.4
environment:
POSTGRES_USER: ${POSTGRES_USER:-app}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB:-appdb}
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- appnet
adminer:
image: adminer:4.8
profiles:
- dev
ports:
- "8081:8080"
networks:
- appnet
volumes:
pgdata:
networks:
appnet:
# .env local (NON commité) : DATABASE_URL, POSTGRES_PASSWORD
Indépendant
Câblage par noms + URL en variable et votre maquette démarre chez chaque client.
PME à Alger
Idir livre la maquette en une commande : api sur 3000, console sur 8081, base persistée.
Équipe projet
Standardiser DATABASE_URL + retry au démarrage pour tous les services de l'équipe.
Débutant
Cette semaine : api + base câblées par noms, table créée, down/up qui prouve la persistance.
✓ Bonnes pratiques
- Joignez par noms de services, configurez par variables d'environnement.
- Ajoutez un retry de connexion au démarrage de chaque service client de la base.
- Gardez la console d'admin en dev uniquement (profil dev), surface minimale en prod.
✗ Pièges fréquents
- IP en dur dans le code : elle change à chaque recréation.
- Croire que depends_on = prêt : sans retry, le premier démarrage crash.
- Publier la base en production : surface d'attaque élargie pour rien.
Points clés à retenir
- UN RÉSEAU, des noms, une URL en variable : bdd:5432 via DATABASE_URL, jamais d'IP.
- BASE persistée (volume nommé) et JAMAIS publiée : interne uniquement.
- RETRY obligatoire au démarrage : depends_on ordonne, retry attend vraiment.
- DOWN/UP prouve la persistance : la table doit survivre au redémarrage.
Défi 48h — appliquez demain
Montez api + base câblées par noms avec retry, et prouvez la persistance par un down/up devant un collègue.
- Mes services se joignent par noms via des variables.
- Ma base est persistée et non publiée.
- Mon api réessaie sa connexion au démarrage.
Questions fréquentes
L'api crash au premier démarrage puis marche : normal ?
OUI, si elle ne réessaie pas : la base démarre plus lentement que l'api (depends_on ordonne sans attendre). Ajoutez une boucle de retry avec délai au démarrage de l'api — le crash disparaît. Complément niveau 4 : healthcheck côté base + condition de démarrage côté api pour ordonnancer proprement.
Où mettre le mot de passe et l'URL pour de vrai ?
En DEV : .env non commité à côté du compose (documenté par un .env.example factice). En PROD : secret managé (fichier monté, CI, orchestrateur). JAMAIS en clair dans le compose commité, jamais dans l'image. Et la base reste non publiée : le secret ne transite que sur le réseau interne Compose.
L'api doit joindre postgres sur le réseau Compose. Quelle URL saine ?