Problème n° 1 — Le conteneur fantôme

Contexte

Vous êtes d’astreinte. Un collègue a déployé myapp:1.4 vendredi soir avant de partir. Lundi matin, les utilisateurs signalent que l’application est inaccessible. Vous vous connectez au serveur via SSH.

Fichiers du projet :

server.js :

const express = require('express');
const app = express();

const PORT = process.env.PORT || 3000;
const HOST = process.env.HOST || '127.0.0.1';

app.get('/health', (req, res) => res.json({ status: 'ok' }));
app.get('/', (req, res) => res.send('Hello world'));

app.listen(PORT, HOST, () => {
  console.log(`Server started on port ${PORT}`);
});

Dockerfile :

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

Ce que tu observes sur le serveur :

$ docker ps
CONTAINER ID   IMAGE       STATUS            PORTS
a3f91bc2d1e4   myapp:1.4   Up 3h (healthy)   0.0.0.0:3000->3000/tcp

$ curl http://localhost:3000
curl: (7) Failed to connect to localhost port 3000: Connection refused

$ docker logs a3f91bc2d1e4 --tail 5
Server started on port 3000
Database connected
Ready.

$ docker inspect a3f91bc2d1e4 | grep -i restart
"RestartPolicy": { "Name": "unless-stopped" }
"RestartCount": 0

$ sudo ufw status
Status: inactive

Question : Le conteneur est sain, aucun plantage, aucun pare-feu, le port semble correctement mappé. Pourquoi l’application est-elle inaccessible ?

✓ Answer

Le bug est dans server.js, ligne 4 :

js

const HOST = process.env.HOST || '127.0.0.1';

Pourquoi ça casse

À l’intérieur d’un container Docker, 127.0.0.1 est le loopback du container lui-même — pas de l’hôte. L’app écoute donc sur une interface que personne d’autre ne peut atteindre, même avec un port mapping déclaré.

Le docker ps est le piège classique :

0.0.0.0:3000->3000/tcp

Ce 0.0.0.0 décrit le côté hôte du mapping. Ça ne dit rien sur ce que l’app écoute à l’intérieur du container. Docker transfère le trafic entrant sur le port 3000 de l’hôte… vers le port 3000 du container — mais personne n’y répond, parce que l’app est repliée sur son loopback interne.

Les logs ne mentent pas non plus :

Server started on port 3000

Node n’affiche pas l’interface d’écoute dans ce log — juste le port. Tout semble normal.


Le fix

Option 1 — modifier le code (recommandé) :

js

const HOST = process.env.HOST || '0.0.0.0';

Option 2 — passer la variable d’environnement au démarrage :

bash

docker run -e HOST=0.0.0.0 -p 3000:3000 myapp:1.4

Option 3 — vérifier avant de redéployer :

bash

# Depuis l'intérieur du container
docker exec -it a3f91bc2d1e4 curl http://127.0.0.1:3000
# → répond

docker exec -it a3f91bc2d1e4 curl http://0.0.0.0:3000
# → connection refused  ← confirme le diagnostic

À retenir

Dans un container Docker, une app doit toujours écouter sur 0.0.0.0 pour être accessible depuis l’extérieur. Le port mapping Docker (-p) ne suffit pas si l’app elle-même refuse les connexions non-loopback.

Laisser un commentaire