3.3 · Exemple filé : application web + base de données

Niveau 3 · Docker Compose

3.3Exemple filé : application web + base de données

Objectif : faire tourner une vraie mini-application (API + base) câblée par noms.
Temps estimé : 15 min · lecture autonome · Si vous n'avez que 5 min : lisez « L'essentiel en 30s » + le schéma.

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.

Erreur d'interprétation fréquente : « depends_on suffit pour la disponibilité, et publier la base aide au debug ». Non, deux fois. depends_on ordonne sans attendre : sans retry, le premier démarrage crash — c'est normal et prévisible, pas un mystère. Et publier postgres sur l'hôte (surtout en production) élargit la surface d'attaque pour un confort illusoire : on debug via adminer en dev et exec en prod, jamais en exposant la base.
Appli câblée, persistée, sobre Noms, variable, retry, surface minimale Câbler par nomsapi → bdd:5432, URL en DATABASE_URL+Persister + retryVolume nommé, boucle de reconnexion+Exposer le minimumapi et adminer (dev) publiés, base interne down/up prouve la persistance ; retry prouve la robustesse.
Noms, variable, retry, surface minimale
Version texte du schéma (SEO / lecteurs d'écran / traduction) : 1) Câbler : Idir fait joindre bdd:5432 par nom via DATABASE_URL au lieu d'une IP en dur. 2) Fiabiliser : retry au démarrage côté api (depends_on ordonne, retry attend vraiment) + volume nommé pour la base. 3) Exposer : api et adminer publiés, base interne uniquement — down/up prouve que la table survit. Leçon : noms, variable, retry, surface minimale.

🔎 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

  1. Créez ./api (Dockerfile + serveur lisant DATABASE_URL avec retry au démarrage).
  2. Écrivez le compose ci-dessus (URL en variable, base non publiée) et lancez up -d --build.
  3. Vérifiez l'api (:3000) et adminer (:8081, profil dev), créez une table.
  4. Redémarrez tout (down puis up) : la table doit survivre — sinon, le volume est mal monté.
Une vraie appli multi-services tourne, câblée par noms, persistée et à surface minimale.

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.

Prudence : Mots de passe et URL de connexion via le .env local, jamais en dur : ${DATABASE_URL} et ${POSTGRES_PASSWORD} dans le compose, valeurs réelles uniquement dans le .env non commité. En production : secret managé (fichier, CI, orchestrateur) — jamais le compose commité en clair, jamais la base publiée.

✓ 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.

Checklist sortie de section
  • 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.
Auto-évaluation 1-5 : une IP en dur traîne-t-elle encore dans mon code ou mon compose ?est-ce que mon stack survit à un down/up sans perte ?

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.

Vérifiez votre compréhension

L'api doit joindre postgres sur le réseau Compose. Quelle URL saine ?

Ressources supplémentaires (optionnel, pour aller plus loin)

Examen du niveau 3Questions inédites et durcies pour valider vos acquis