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 ?

✅ Solution

Le bug est dans server.js, ligne 4 :

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é) :

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

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

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

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

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

Romain Drouche

Romain Drouche

Architecte Système | MCSE: Core Infrastructure

Expert en infrastructures IT avec plus de 15 ans d'expérience sur le terrain. Actuellement Chef de projet Systèmes et Réseaux et Référent SSI (Sécurité des Systèmes d'Information), je mets mon expertise au service de la fiabilité et de la sécurité des environnements technologiques.

GHinM▶