Vous déployez un service, il tente de se lier, et le noyau répond par la vérité la moins aidante en informatique : bind: Address already in use. Les alertes se déclenchent. Quelqu’un dit « redémarre simplement ». Quelqu’un d’autre dit « c’est probablement le DNS ». Aucun des deux n’est votre ami pour l’instant.
Sur Debian 13, vous disposez de tous les outils pour identifier précisément qui tient un port — processus, unité systemd, conteneur, activation de socket, voire un namespace obsolète — puis corriger le conflit sans dégâts collatéraux. L’astuce est de connaître l’ordre des vérifications et ce que chaque outil vous indique réellement.
Mode opératoire de diagnostic rapide
Voici la séquence « arrêter l’hémorragie ». Elle est optimisée pour trouver rapidement le propriétaire, pas pour l’enseignement. L’enseignement vient après.
Première étape : confirmer ce qui échoue réellement (logs du service)
Ne commencez pas par des scanners de ports. Commencez par le gestionnaire de service. Si systemd est impliqué, il vous indiquera souvent l’adresse et le port exacts qui ont échoué au bind.
Deuxième étape : identifier l’écouteur (ss)
Utilisez ss en premier. C’est moderne, rapide et affiche la vue du noyau sur les sockets avec l’association de processus.
Troisième étape : mapper le PID au véritable propriétaire (unité systemd / conteneur)
La propriété du PID n’est pas la même chose que « qui a causé ça en production ». Vous voulez l’unité, le paquet ou l’image du conteneur derrière.
Quatrième étape : vérifier l’activation de socket systemd et les forwarders de port
Le port peut être détenu par une unité socket, pas par le démon que vous imaginez. Ou par un proxy (Envoy, HAProxy, nginx) que quelqu’un a oublié de mentionner.
Cinquième étape : choisir une correction propre
Privilégiez la désactivation de l’écouteur incorrect, la correction de la configuration ou la migration du service vers un port dédié. Évitez « kill -9 » sauf si vous aimez les post-mortems.
Faits et contexte intéressants (pourquoi ça revient)
- Fait 1 : La chaîne d’erreur provient de
EADDRINUSE, un errno POSIX utilisé parbind(2)quand un tuple adresse/port local est déjà réservé. - Fait 2 : Avant que
ssne soit courant, les admins Linux utilisaientnetstat(depuis net-tools). Debian pousse depuis des années à moins utiliser net-tools, et Debian 13 est fermement dans l’ère « utilisez iproute2 ». - Fait 3 : L’activation de socket systemd peut binder des ports avant que le service ne démarre. Cela signifie que le propriétaire du port peut être systemd, pas votre démon.
- Fait 4 : IPv6 peut « couvrir » IPv4 via des sockets v6-mappées selon
net.ipv6.bindv6only. Vous pensez binder IPv4, mais un écouteur IPv6 squatte le port. - Fait 5 : Les ports privilégiés (sous 1024) nécessitaient historiquement root sur Unix. Le Linux moderne peut accorder cette capacité à un binaire via file capabilities, ce qui change qui peut binder quoi — et qui peut entrer en conflit.
- Fait 6 : Un conflit de port peut être causé par un namespace réseau entièrement différent (conteneur, systemd-nspawn). Les outils sur l’hôte peuvent ne pas le voir sauf si vous regardez dans le bon namespace.
- Fait 7 :
SO_REUSEADDRest très mal compris. Cela ne veut pas dire « deux programmes différents peuvent écouter sur le même port TCP ». C’estSO_REUSEPORTqui le permet, et il a des effets secondaires. - Fait 8 : UDP se comporte différemment. Plusieurs sockets UDP peuvent parfois binder le même port selon les options et adresses, ce qui rend « qui le possède » moins évident.
- Fait 9 : « Rien n’écoute mais bind échoue » signifie parfois que votre appli tente de binder une IP qui est toujours présente sur la machine mais pas sur l’interface que vous pensez (ou gérée par VRRP, keepalived ou un agent cloud).
Ce que signifie réellement « Address already in use » sur Linux
Quand un serveur démarre, il fait généralement quelque chose comme : créer une socket, régler des options, puis bind() sur une adresse/port locale, puis listen(). Si le noyau ne peut pas réserver ce couple adresse/port, il retourne EADDRINUSE. Votre programme affiche l’erreur et quitte (ou réessaie si poli).
Voici la partie importante : « Adresse » inclut plus qu’un numéro de port. Cela peut signifier :
- TCP vs UDP : Le port TCP 53 et le port UDP 53 sont des sockets différents. L’un peut être utilisé pendant que l’autre est libre.
- Spécifique IP vs wildcard : Se lier à
127.0.0.1:8080est différent de0.0.0.0:8080(toutes les IPv4). Mais un bind wildcard peut bloquer un bind spécifique selon la façon dont il est déjà lié. - Wildcard IPv6 :
[::]:8080peut, sur certains systèmes, accepter aussi des connexions IPv4 à moins que le noyau soit configuré en v6-only. - Namespaces réseau : Si vous êtes dans un namespace de conteneur, « port 8080 » n’est pas nécessairement le port 8080 de l’hôte — sauf si vous l’avez publié.
Si vous ne retenez qu’une leçon : capturez toujours la cible complète du bind depuis les logs — protocole, IP et port — avant de courir après des fantômes.
Une idée paraphrasée de Richard Cook (chercheur en fiabilité) : Les défaillances surviennent dans les écarts entre la façon dont le travail est imaginé et la façon dont il est réellement fait.
La propriété des ports est l’un de ces écarts.
Tâches pratiques : commandes, sorties, décisions (12+)
Voici les tâches à exécuter sur un hôte Debian 13 lorsqu’un service ne peut démarrer à cause de « Address already in use ». Chacune indique ce que la sortie signifie et quelle décision prendre ensuite.
Task 1: Lire les logs de l’unité en erreur (systemd)
cr0x@server:~$ sudo systemctl status myapp.service
× myapp.service - MyApp API
Loaded: loaded (/etc/systemd/system/myapp.service; enabled; preset: enabled)
Active: failed (Result: exit-code) since Mon 2025-12-29 09:12:01 UTC; 8s ago
Process: 18422 ExecStart=/usr/local/bin/myapp --listen 0.0.0.0:8080 (code=exited, status=1/FAILURE)
Main PID: 18422 (code=exited, status=1/FAILURE)
CPU: 48ms
Dec 29 09:12:01 server myapp[18422]: bind: Address already in use
Dec 29 09:12:01 server systemd[1]: myapp.service: Main process exited, code=exited, status=1/FAILURE
Dec 29 09:12:01 server systemd[1]: myapp.service: Failed with result 'exit-code'.
Signification : Vous avez la cible du bind : 0.0.0.0:8080. C’est le port wildcard IPv4 complet. Maintenant vous pouvez chercher précisément.
Décision : Trouvez qui écoute déjà sur TCP/8080, en IPv4 ou IPv6 wildcard.
Task 2: Trouver les écouteurs avec ss (rapide, précis)
cr0x@server:~$ sudo ss -H -ltnp 'sport = :8080'
LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:(("nginx",pid=1312,fd=8))
Signification : Le port TCP 8080 est déjà lié sur toutes les adresses IPv4 par nginx PID 1312.
Décision : Déterminez si nginx doit posséder le 8080. Sinon, ajustez la config nginx ou arrêtez/désactivez l’unité.
Task 3: Confirmer qu’IPv6 n’est pas impliqué
cr0x@server:~$ sudo ss -H -ltnp 'sport = :8080' -6
LISTEN 0 4096 [::]:8080 [::]:* users:(("nginx",pid=1312,fd=9))
Signification : nginx écoute aussi sur le wildcard IPv6. Même si vous corrigez l’IPv4, l’IPv6 peut toujours entrer en collision selon le comportement de bind de votre appli.
Décision : Décidez si votre nouveau service doit aussi écouter en IPv6, et assurez-vous que nginx soit entièrement retiré du port, pas « partiellement corrigé ».
Task 4: Traduire le PID en unité systemd (le vrai propriétaire)
cr0x@server:~$ ps -o pid,comm,args -p 1312
PID COMMAND COMMAND
1312 nginx nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
cr0x@server:~$ systemctl status nginx.service
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/lib/systemd/system/nginx.service; enabled; preset: enabled)
Active: active (running) since Mon 2025-12-29 08:55:10 UTC; 17min ago
Main PID: 1312 (nginx)
Tasks: 2 (limit: 18920)
Memory: 4.9M
CPU: 1.205s
Signification : C’est un service géré, pas un processus errant. Parfait : vous pouvez corriger proprement.
Décision : Vérifiez la config nginx pour un écouteur sur 8080, puis changez-la ou retirez-la, puis rechargez.
Task 5: Localiser la directive de binding dans nginx
cr0x@server:~$ sudo nginx -T 2>/dev/null | grep -nE 'listen\s+8080'
47: listen 8080 default_server;
98: listen [::]:8080 default_server;
Signification : nginx est explicitement configuré pour utiliser 8080 sur les deux piles.
Décision : Soit changez nginx pour un autre port, soit réaffectez votre appli à un port différent, soit placez votre appli derrière nginx et conservez 8080 pour nginx. Choisissez une option, ne « partagez » pas.
Task 6: Si ce n’est pas un service géré, identifier le paquet ou le parent
cr0x@server:~$ sudo ss -H -ltnp 'sport = :5432'
LISTEN 0 244 127.0.0.1:5432 0.0.0.0:* users:(("postgres",pid=2221,fd=6))
cr0x@server:~$ ps -fp 2221
UID PID PPID C STIME TTY TIME CMD
postgres 2221 1 0 08:41 ? 00:00:01 /usr/lib/postgresql/17/bin/postgres -D /var/lib/postgresql/17/main
Signification : Postgres écoute en localhost seulement, occupant toujours 5432.
Décision : Si vous essayez de démarrer un autre Postgres ou une appli qui veut 5432, arrêtez l’instance existante ou déplacez-en une. Démarrer « un autre Postgres sur 5432 » n’est pas une stratégie.
Task 7: Utiliser lsof quand ss ne montre pas ce que vous attendez
cr0x@server:~$ sudo lsof -nP -iTCP:8080 -sTCP:LISTEN
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
nginx 1312 root 8u IPv4 24562 0t0 TCP *:8080 (LISTEN)
nginx 1312 root 9u IPv6 24563 0t0 TCP *:8080 (LISTEN)
Signification : Même histoire, outil différent. lsof est plus lent mais parfois plus clair, surtout sous pression.
Décision : Si les outils divergent, faites confiance à la vue du noyau mais vérifiez le namespace (voir plus loin). La plupart du temps, la divergence signifie que vous regardez dans des namespaces différents ou que vous avez mal filtré.
Task 8: Vérifier si l’activation de socket systemd retient le port
cr0x@server:~$ systemctl list-sockets --all | grep -E ':(80|443|8080)\b'
nginx.socket 0 128 0.0.0.0:8080 0.0.0.0:*
nginx.socket 0 128 [::]:8080 [::]:*
Signification : Le port peut être détenu par une unité socket, qui peut rester à l’écoute même si le service est arrêté. C’est le classique « je l’ai arrêté, pourquoi c’est encore utilisé ? »
Décision : Gérez l’unité .socket, pas seulement le .service. Désactivez/maskez la socket si vous ne voulez plus que systemd possède le port.
Task 9: Arrêter et désactiver l’unité correcte (service vs socket)
cr0x@server:~$ sudo systemctl stop nginx.service
cr0x@server:~$ sudo ss -H -ltnp 'sport = :8080'
LISTEN 0 128 0.0.0.0:8080 0.0.0.0:* users:(("systemd",pid=1,fd=67))
cr0x@server:~$ sudo systemctl stop nginx.socket
cr0x@server:~$ sudo ss -H -ltnp 'sport = :8080'
cr0x@server:~$ sudo systemctl disable --now nginx.socket
Removed "/etc/systemd/system/sockets.target.wants/nginx.socket".
Signification : Arrêter le service n’a rien fait parce que systemd a gardé la socket. Arrêter la socket a libéré le port.
Décision : Si nginx ne doit plus gérer ce port, désactivez la socket (et assurez-vous que le service n’est pas activé par inadvertance non plus).
Blague 1 : Si vous « corrigez » un conflit de port en redémarrant, vous ne l’avez pas résolu — vous avez juste redémarré le mystère.
Task 10: Confirmer sur quelle adresse IP votre appli se lie
cr0x@server:~$ ip -br addr show
lo UNKNOWN 127.0.0.1/8 ::1/128
ens3 UP 10.10.5.21/24 2001:db8:10:10::21/64
Signification : Vous ne pouvez binder que des adresses que vous possédez (ou que vous aurez via un gestionnaire comme keepalived). Si votre appli tente de binder 10.10.5.99 et que vous ne l’avez pas, vous obtiendrez une autre erreur (généralement Cannot assign requested address), mais les gens lisent souvent mal les logs et poursuivent la mauvaise piste.
Décision : Validez que l’adresse de bind correspond à la réalité. Si c’est une VIP, confirmez que la VIP est présente sur ce nœud.
Task 11: Attraper un écouteur « caché » dans un autre namespace réseau (conteneurs)
cr0x@server:~$ sudo ss -H -ltnp 'sport = :9090'
LISTEN 0 4096 0.0.0.0:9090 0.0.0.0:* users:(("docker-proxy",pid=5144,fd=4))
Signification : Le port de l’hôte est détenu par docker-proxy (ou parfois par des règles nftables sans docker-proxy, selon la configuration). Votre vraie application est à l’intérieur d’un conteneur, mais le bind côté hôte est ce qui bloque votre service.
Décision : Trouvez quel conteneur a publié le port. Ne tuez pas docker-proxy ; corrigez le mapping du conteneur.
Task 12: Identifier le conteneur qui a publié le port (Docker)
cr0x@server:~$ sudo docker ps --format 'table {{.ID}}\t{{.Names}}\t{{.Ports}}'
CONTAINER ID NAMES PORTS
c2f1d3a7b9c1 prometheus 0.0.0.0:9090->9090/tcp, [::]:9090->9090/tcp
8bca0e23ab77 node-exporter 9100/tcp
cr0x@server:~$ sudo docker inspect -f '{{.Name}} {{json .HostConfig.PortBindings}}' c2f1d3a7b9c1
/prometheus {"9090/tcp":[{"HostIp":"","HostPort":"9090"}]}
Signification : Le conteneur prometheus possède le 9090 de l’hôte.
Décision : Si votre nouveau service a besoin de 9090, déplacez Prometheus (recommandé : conserver 9090 pour Prometheus et déplacer votre service), ou changez le port publié et mettez à jour les configs de scrape.
Task 13: Identifier les écouteurs créés par une session utilisateur (pas root, pas systemd)
cr0x@server:~$ sudo ss -H -ltnp 'sport = :3000'
LISTEN 0 4096 127.0.0.1:3000 0.0.0.0:* users:(("node",pid=27533,fd=21))
cr0x@server:~$ ps -o pid,user,cmd -p 27533
PID USER CMD
27533 alice node /home/alice/dev/server.js
cr0x@server:~$ sudo loginctl session-status
2 - alice (1000)
Since: Mon 2025-12-29 07:58:02 UTC; 1h 17min ago
Leader: 27102 (sshd)
Seat: n/a
TTY: pts/1
Service: sshd
State: active
Unit: session-2.scope
└─27533 node /home/alice/dev/server.js
Signification : Un humain exécute un serveur de dev en production. Ça arrive plus souvent qu’on ne l’admet.
Décision : Coordonnez. Soit déplacez le processus de dev, soit réservez le port pour la production et dites aux gens d’utiliser un port non conflictuel lié à localhost (ou mieux : un hôte de dev).
Task 14: Gérer les conflits UDP (DNS, métriques, serveurs de jeu)
cr0x@server:~$ sudo ss -H -lunp 'sport = :53'
UNCONN 0 0 127.0.0.1:53 0.0.0.0:* users:(("systemd-resolve",pid=812,fd=13))
Signification : UDP/53 est déjà lié sur localhost par un résolveur. Si vous tentez de démarrer un serveur DNS, vous serez en conflit.
Décision : Décidez si cet hôte doit exécuter un serveur DNS. Si oui, reconfigurez le service résolveur local pour qu’il cesse de binder, ou liez votre service DNS à une interface/IP spécifique qui ne rentre pas en conflit (avec prudence).
Task 15: Vérifier les Listen* et ports configurés d’un service via systemd
cr0x@server:~$ systemctl cat nginx.socket
# /lib/systemd/system/nginx.socket
[Unit]
Description=nginx Socket
[Socket]
ListenStream=8080
ListenStream=[::]:8080
Accept=no
[Install]
WantedBy=sockets.target
Signification : L’unité socket est la source du bind. C’est l’élément incriminant quand un port « revient sans cesse ».
Décision : Changez/overridez cette unité, ou désactivez/maskez-la.
Task 16: Confirmer que le port est vraiment libre après un changement
cr0x@server:~$ sudo ss -H -ltnp 'sport = :8080'
cr0x@server:~$ sudo systemctl start myapp.service
cr0x@server:~$ sudo ss -H -ltnp 'sport = :8080'
LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:(("myapp",pid=19201,fd=7))
Signification : La propriété est propre : le processus attendu écoute maintenant.
Décision : Passez aux vérifications de santé, au routage et à ce que les dieux des incidents ont prévu ensuite.
Pièges systemd : sockets, unités, écouteurs persistants
Sur Debian 13, systemd est le centre de gravité. Même si vous pensez « je lance juste un binaire », vous le faites probablement via un fichier d’unité, un timer, une socket, ou un service de conteneur.
L’activation de socket est excellente — jusqu’à ce que vous l’oubliiez
L’activation de socket signifie que systemd bind le port, puis lance le service à la demande et lui passe le descripteur de fichier. Cela améliore la latence de démarrage pour les services pilotés par requêtes, et ça peut lisser les redémarrages. Cela crée aussi un mode d’échec : vous arrêtez un service, mais le port reste bindé par PID 1.
Si vous voyez users:(("systemd",pid=1,...)) dans la sortie de ss pour votre port, n’argumentez pas. Cherchez l’unité socket :
cr0x@server:~$ systemctl list-sockets --all | grep ':8080'
nginx.socket 0 128 0.0.0.0:8080 0.0.0.0:*
nginx.socket 0 128 [::]:8080 [::]:*
Masquage : l’option nucléaire parfois correcte
Désactiver une unité empêche son démarrage au boot, mais autre chose peut encore la démarrer manuellement ou comme dépendance. Masker la bloque complètement en la liant à /dev/null. En réponse d’incident, le masquage peut être la bonne action quand un écouteur indésirable ressuscite sans cesse.
cr0x@server:~$ sudo systemctl mask --now nginx.socket
Created symlink /etc/systemd/system/nginx.socket → /dev/null.
Règle de décision : Désactivez quand vous changez le comportement prévu et que vous avez le temps de le faire proprement. Masquez quand vous devez garantir qu’il ne reviendra pas (et que vous nettoierez ensuite).
Les drop-ins valent mieux que modifier les unités fournisseur
Sur Debian, les unités fournisseur vivent sous /lib/systemd/system. Les modifier fonctionne jusqu’à la prochaine mise à jour du paquet, quand votre correctif sera poliment supprimé.
Créez plutôt un override :
cr0x@server:~$ sudo systemctl edit nginx.socket
[Socket]
ListenStream=
ListenStream=127.0.0.1:8080
Signification : La ligne vide ListenStream= réinitialise la liste, puis vous définissez la nouvelle. C’est la façon de systemd de s’assurer que vous vouliez vraiment « remplacer », pas « ajouter ».
Décision : Utilisez des drop-ins pour un comportement stable lors des mises à jour. Modifier les fichiers fournisseur est un bricolage temporaire que vous devriez traiter comme tel.
Conteneurs et orchestration : Docker, Podman, Kubernetes
Les conflits de port dans les environnements conteneurisés ne sont rarement « deux démons veulent 8080 ». Ils sont plutôt « quelque chose a publié 8080 sur l’hôte il y a des mois et tout le monde a oublié ». Les conteneurs facilitent l’expédition de logiciels ; ils rendent aussi facile de squatter des ports indéfiniment.
Docker et publication de port hôte
Quand vous lancez -p 8080:8080, vous réclamez explicitement le port de l’hôte. Sur de nombreuses configurations, vous verrez docker-proxy posséder la socket. Sur d’autres, des règles nftables gèrent le forwarding, mais le port hôte peut toujours paraître réservé selon le mode.
Le flux propre est :
- Identifier qu’un proxy est l’écouteur (
ssmontre docker-proxy). - Le mapper à un conteneur (
docker ps,docker inspect). - Changer le port publié ou supprimer le conteneur.
Podman : même idée, plomberie différente
Podman peut exécuter des conteneurs rootless. Le bind de port rootless introduit une nuance : des processus peuvent vivre sous une session utilisateur, et le forwarding de port peut être fait par slirp4netns ou pasta. Le symptôme reste le même : votre port hôte est pris.
Si vous suspectez Podman :
cr0x@server:~$ podman ps --format 'table {{.ID}}\t{{.Names}}\t{{.Ports}}'
CONTAINER ID NAMES PORTS
4fdb08b1b3a8 grafana 0.0.0.0:3000->3000/tcp
Décision : Si c’est rootless et lié à un utilisateur, vous devrez peut-être vous coordonner avec cette session utilisateur ou désactiver le lingering pour ce compte.
Kubernetes : NodePort et hostNetwork
Kubernetes ajoute un type spécial de chaos :
- NodePort prend des ports dans une plage, et vous pouvez entrer en collision si autre chose écoute là.
- hostNetwork: true fait que les pods se lient directement sur la pile réseau du nœud.
- DaemonSets signifient « c’est partout », ce qui est génial quand c’est intentionnel et extrêmement ennuyeux sinon.
Sur un nœud avec kubelet, ss montrera le processus propriétaire (parfois le processus de l’application lui-même, parfois un proxy). Votre travail est de le mapper au pod et à la charge. Les commandes exactes dépendent de votre accès au cluster, mais le diagnostic côté hôte ne change pas : identifiez d’abord l’écouteur.
IPv4/IPv6 et « c’est en écoute, mais pas là où vous avez regardé »
La confusion classique : vous lancez ss -ltnp, vous ne voyez pas votre port, mais l’appli échoue quand même avec « Address already in use ». Puis vous relancez et vous voyez quelque chose sur [::]. Vous dites « nous n’utilisons pas IPv6 ». Le noyau se moque de vos croyances.
Comprendre les écouteurs wildcard
Voici les motifs à reconnaître :
0.0.0.0:PORTsignifie toutes les adresses IPv4.[::]:PORTsignifie toutes les adresses IPv6. Selon le sysctl, il peut aussi accepter les connexions IPv4 mappées.127.0.0.1:PORTsignifie localhost seulement (généralement le plus sûr pour des endpoints admin internes).
Vérifier bindv6only quand le comportement surprend
cr0x@server:~$ sysctl net.ipv6.bindv6only
net.ipv6.bindv6only = 0
Signification : Avec 0, une socket wildcard IPv6 peut aussi accepter des connexions IPv4 via des adresses v4-mappées sur beaucoup de systèmes. Cela peut faire que [::]:8080 bloque les attentes sur 0.0.0.0:8080 selon la façon dont les applications définissent les options.
Décision : Ne « corrigez » pas ça en changeant les sysctls pendant un incident sauf si vous comprenez le périmètre d’impact. Corrigez-le au niveau de la config de l’application/service : liez explicitement en IPv4 ou IPv6 selon l’intention.
Courses, redémarrages et mythes TIME_WAIT
Les gens imputent TIME_WAIT aux échecs de bind comme ils imputent « le réseau » aux requêtes lentes. Parfois il est impliqué, mais rarement la cause pour qu’un serveur ne puisse pas binder son port d’écoute.
TIME_WAIT concerne généralement les connexions sortantes
Les sockets en TIME_WAIT sont typiquement des ports éphémères côté client, pas des ports d’écoute serveur. Votre serveur peut en général rebind son port d’écoute sans problème. Si ce n’est pas le cas, vous avez presque certainement un vrai écouteur, une unité socket ou un processus bloqué.
Quand les redémarrages causent des conflits de bind malgré tout
Le vrai problème de redémarrage est le chevauchement de deux instances :
- systemd démarre une nouvelle instance avant que l’ancienne ne soit vraiment sortie (mauvaise configuration
Type=forking, suivi de PID incorrect). - Une application se daemonise mais l’unité est écrite pour un process non-daemon, donc systemd pense qu’elle est morte et la redémarre — créant des tentatives multiples de bind.
- Un script wrapper lance le serveur réel et quitte immédiatement.
Pour détecter cela, surveillez plusieurs processus avec le même binaire, ou des boucles de redémarrage rapides dans le journal.
cr0x@server:~$ sudo journalctl -u myapp.service -n 50 --no-pager
Dec 29 09:11:58 server systemd[1]: Started MyApp API.
Dec 29 09:12:01 server myapp[18422]: bind: Address already in use
Dec 29 09:12:01 server systemd[1]: myapp.service: Main process exited, code=exited, status=1/FAILURE
Dec 29 09:12:01 server systemd[1]: myapp.service: Failed with result 'exit-code'.
Dec 29 09:12:01 server systemd[1]: myapp.service: Scheduled restart job, restart counter is at 5.
Dec 29 09:12:01 server systemd[1]: Stopped MyApp API.
Dec 29 09:12:01 server systemd[1]: Started MyApp API.
Décision : S’il y a une boucle de redémarrage, arrêtez l’unité pour stabiliser l’hôte, puis corrigez le conflit. Sinon vous poursuivrez une cible mouvante pendant que systemd relance sans cesse des échecs.
Corrections propres que vous pouvez défendre en revue
Vous pouvez « libérer le port » de nombreuses façons. La plupart sont paresseuses. L’objectif est de corriger le modèle de propriété pour que le conflit ne revienne pas au prochain reboot, redéploiement ou ingénieur bien intentionné le vendredi après-midi.
Option de correction A : Déplacer le service sur le bon port (et le documenter)
Si deux services veulent 8080 parce que personne n’a choisi un plan de ports, choisissez-en un maintenant. Mettez-le dans la gestion de configuration. Mettez-le dans les labels de monitoring. Rendez-le ennuyeux.
Option de correction B : Mettre un service derrière un reverse proxy
Si nginx est déjà sur 80/443 et votre appli veut 8080, l’architecture propre est souvent : nginx possède les ports publics ; les applis vivent sur des ports privés élevés ou des unix sockets ; nginx fait le routage.
Soyez cohérent. « Parfois on lie les applis directement sur 0.0.0.0 » est la façon d’obtenir des guerres de ports surprises.
Option de correction C : Stopper/désactiver le mauvais service (service + socket si nécessaire)
Si le port est possédé par quelque chose que vous ne voulez pas, retirez-le du graphe de démarrage :
cr0x@server:~$ sudo systemctl disable --now nginx.service
Removed "/etc/systemd/system/multi-user.target.wants/nginx.service".
cr0x@server:~$ sudo systemctl disable --now nginx.socket
Removed "/etc/systemd/system/sockets.target.wants/nginx.socket".
Décision : Désactiver les deux empêche la danse « je l’ai arrêté mais il est revenu ».
Option de correction D : Rendre les binds explicites (éviter le wildcard quand inutile)
Lier à 0.0.0.0 est pratique et fréquemment une erreur. Pour les endpoints d’administration, liez à localhost. Pour les services internes, liez à l’IP de l’interface interne ou à un VRF dédié. Pour les services publics, liez à la VIP du load balancer ou laissez le proxy la posséder.
Option de correction E : Utiliser des file capabilities pour les ports privilégiés au lieu d’exécuter en root
Si votre service a besoin de 80/443 mais ne devrait pas tourner en root, définissez des capabilities :
cr0x@server:~$ sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/myapp
cr0x@server:~$ getcap /usr/local/bin/myapp
/usr/local/bin/myapp cap_net_bind_service=ep
Signification : Le binaire peut binder des ports privilégiés sans les privilèges root complets.
Décision : Utilisez ceci quand nécessaire, mais suivez-le — les capabilities sont faciles à oublier et elles changent votre posture de sécurité. De plus : les capabilities ne préviennent pas les conflits ; elles changent juste qui peut les créer.
Option de correction F : Réserver des ports et appliquer une politique
Dans des environnements sérieux, vous maintenez un registre de ports pour chaque rôle d’hôte ou cluster. Vous l’appliquez dans le CI, dans les charts helm, dans les templates systemd. Sans cela, les conflits de ports ne sont pas des « incidents », ils sont « inévitables ».
Blague 2 : L’allocation de ports sans registre est comme la répartition des places à la cantine — tout le monde se bat pour la table près de la prise.
Erreurs fréquentes : symptôme → cause racine → correction
1) « J’ai arrêté le service mais le port est toujours utilisé »
Symptôme : systemctl stop foo.service réussit, mais ss montre encore le port en LISTEN.
Cause racine : Une unité foo.socket bind le port (activation de socket), ou un service dépendant le retient encore.
Correction : Vérifiez systemctl list-sockets. Arrêtez/désactivez/masquez l’unité socket. Vérifiez avec ss que PID 1 n’est plus l’écouteur.
2) « ss ne montre rien, mais mon appli dit toujours Address already in use »
Symptôme : Vous cherchez avec ss et ne voyez aucun écouteur sur ce port.
Cause racine : Vous avez cherché le mauvais protocole (UDP vs TCP), la mauvaise famille d’adresses (IPv4 vs IPv6), ou vos filtres l’ont exclu. Moins commun : vous êtes dans un namespace réseau différent de l’écouteur.
Correction : Cherchez sur les deux piles et protocoles : ss -ltnp, ss -lunp, incluez -6. Si des conteneurs sont impliqués, cherchez docker-proxy et inspectez les mappings de conteneurs.
3) « Le port est possédé par ‘systemd’ et je ne sais pas pourquoi »
Symptôme : ss montre users:(("systemd",pid=1,...)).
Cause racine : Une unité socket est configurée pour binder (parfois sous un nom inattendu, ou tirée par un paquet).
Correction : Identifiez la socket via systemctl list-sockets --all, puis systemctl cat NAME.socket. Désactivez ou overridez ses ListenStream/ListenDatagram.
4) « Ça marche en IPv4 mais pas en IPv6 » (ou l’inverse)
Symptôme : Le service démarre lié à 127.0.0.1 mais échoue sur 0.0.0.0, ou il échoue uniquement quand configuré pour [::].
Cause racine : Un autre service est lié sur une famille ; ou le comportement dual-stack IPv6 bloque IPv4.
Correction : Inspectez les écouteurs pour les deux familles. Rendre les binds explicites dans les configs. Ne comptez pas sur les valeurs par défaut.
5) « Nous avons changé le port, mais il redevient »
Symptôme : Après modification d’un fichier d’unité ou d’une config, l’ancien port revient après upgrade/reboot.
Cause racine : Vous avez modifié la config fournisseur sous /lib, ou un outil de gestion de configuration a écrasé votre changement.
Correction : Utilisez des drop-ins systemd sous /etc/systemd/system. Si une gestion de config existe, faites-en la source de vérité.
6) « Tuer le PID a corrigé, mais d’autres choses sont cassées »
Symptôme : Vous avez utilisé kill -9 sur l’écouteur ; le port s’est libéré ; plus tard vous découvrez des effets secondaires.
Cause racine : Vous avez tué un composant partagé (proxy, ingress, agent de métriques) ou un service supervisé qui a redémarré sur le même port.
Correction : Mappez PID → unité/conteneur avant de tuer. Arrêtez proprement via systemd/Docker pour qu’il reste arrêté (ou changez sa config). N’utilisez kill que quand le périmètre d’impact est acceptable.
Listes de contrôle / plan pas à pas
Checklist : Identifier le propriétaire en moins de 2 minutes
- Lisez la cible exacte du bind depuis les logs (
systemctl statusou logs d’appli). Capturez protocole, IP et port. - Exécutez
ss -ltnp 'sport = :PORT'pour TCP ;ss -lunp 'sport = :PORT'pour UDP. - Si rien n’apparaît, vérifiez explicitement IPv6 avec
-6et assurez-vous de ne pas avoir fait de faute de frappe sur le port. - Une fois PID/commande obtenus, mappez à une unité systemd via
systemctl statusetps. - Si l’écouteur est systemd PID 1, listez les sockets et trouvez l’unité
.socket. - Si l’écouteur est docker-proxy/plomberie conteneur, identifiez le conteneur publiant le port.
Checklist : Rendre la correction durable
- Décidez quel service doit posséder le port (décision d’architecture, pas pile ou face).
- Implémentez les changements de config via des drop-ins ou config gérée, pas en modifiant les fichiers fournisseur.
- Arrêtez/désactivez l’ancien propriétaire (service et socket si pertinent).
- Démarrez le propriétaire prévu et vérifiez avec
ss. - Faites un test de connectivité local (curl, nc) et vérifiez le routage externe si applicable.
- Ajoutez une vérification de monitoring qui détecte les écouteurs inattendus sur les ports critiques.
Checklist : Procédure sûre « libérer un port en urgence »
- Arrêtez le service en erreur pour éviter les boucles de redémarrage.
- Identifiez le propriétaire actuel avec
ss/lsof. - Arrêtez-le proprement via son superviseur (systemd/Docker) plutôt que de tuer.
- Si l’arrêt propre échoue : envoyez SIGTERM, attendez, puis envisagez SIGKILL.
- Documentez ce que vous avez fait et pourquoi. Le vous futur ne s’en souviendra pas à 3h du matin.
Trois micro-histoires du monde corporate
Micro-histoire 1 : L’incident causé par une mauvaise hypothèse
L’équipe avait un déploiement simple : déployer une nouvelle API interne sur le port 8080, derrière un reverse proxy existant. Tout le monde « savait » que 8080 était le port interne des applis. C’était dans la tête de quelqu’un et dans une page wiki vieille de deux ans que personne ne faisait confiance pour la mise à jour.
Le déploiement a échoué avec « Address already in use ». L’ingénieur d’astreinte a fait le contrôle standard ss et a vu nginx écouter sur 8080. Cela semblait impossible — nginx « possédait » 80 et 443. Ils ont donc supposé que ss montrait un artefact périmé, et ont redémarré la machine. (Tout est revenu pareil, parce que bien sûr.)
Une fois qu’on a arrêté de redémarrer des machines comme méthode d’enquête, la cause racine était embarrassante : une migration précédente avait déplacé une ancienne appli legacy de 80 à 8080 temporairement, et nginx était resté à l’écoute sur 8080 comme redirection. C’était un « temporaire » qui a duré trois trimestres.
La correction n’a pas été « tuer nginx ». C’était une petite décision d’architecture : nginx garde les ports publics ; les applis internes reçoivent un port élevé réservé par service ; 8080 n’est pas « par défaut », c’est « alloué ». Ils ont remis le redirecteur legacy sur 80/443 et libéré 8080 (ou plus précisément, ils ont cessé d’utiliser « 8080 » comme croyance magique).
L’action post-incident qui a réellement compté : un registre de ports lié à la propriété des services. Pas un tableur — quelque chose appliqué dans la revue de configuration. L’erreur a cessé d’être mystérieuse parce que la propriété a cessé d’être une connaissance tribale.
Micro-histoire 2 : L’optimisation qui a mal tourné
Un ingénieur soucieux des performances voulait des redémarrages plus rapides pendant les déploiements. Il a activé l’activation de socket systemd pour un petit service HTTP afin que systemd retienne la socket et la transmette à un nouveau process avec moins de downtime. Bonne idée, utilisée aux bons endroits.
Puis il a oublié l’avoir fait. Des mois plus tard, une autre équipe a voulu déployer un service séparé sur le même port pendant une consolidation. Ils ont arrêté l’ancien service, l’ont vu inactif, et ont tenté de démarrer le nouveau. Échec avec « Address already in use ». ss montrait PID 1 tenant le port. La confusion a vite monté.
Dans la réalité corporate, l’incident n’était pas juste technique. Une demande de changement est arrivée pour « tuer le process systemd tenant 8443 ». Voilà comment on sait qu’on est en mauvaise posture : quand quelqu’un propose de tuer init à cause d’un conflit de port.
La vraie correction a été propre : arrêter et désactiver l’unité .socket, puis démarrer le nouveau service. La leçon était nette : l’activation de socket est une fonctionnalité de déploiement, pas un interrupteur qu’on active et oublie. Si vous l’activez, vous assumez la complexité opérationnelle qu’elle ajoute, y compris la façon dont elle change la notion « d’arrêt » d’un service.
Après cela, ils ont codifié une règle : les services socket-activés doivent être documentés dans la description de l’unité et monitorés avec une alerte « socket liée alors que le service est arrêté ». Ce garde-fou ennuyeux empêche qu’une « optimisation » devienne un « mystère ».
Micro-histoire 3 : La pratique ennuyeuse mais correcte qui a sauvé la situation
Un service lié au stockage tournait sur une flotte Debian qui hébergeait aussi métriques, shippeurs de logs et quelques daemons legacy qu’on n’osait pas supprimer. Les ports étaient un champ de mines. Mais cette équipe avait une pratique terne : avant tout rollout, ils exécutaient un script de précontrôle qui vérifiait une petite liste de ports réservés et confirmait le propriétaire attendu.
Pendant une fenêtre de patch, un nouveau conteneur de métriques a été déployé avec -p 9100:9100 par défaut parce que l’auteur du chart supposait que Node Exporter était toujours « à l’intérieur du cluster ». Sur ces hôtes, Node Exporter tournait déjà en tant que service systemd. Le conteneur a récupéré le port en premier sur un sous-ensemble de nœuds à cause de l’ordre d’ordonnancement. Le rollout semblait « globalement ok », ce qui est une excellente façon d’être blessé.
Le préflight l’a détecté sur le premier nœud du lot : le script a vu que 9100 n’était plus possédé par l’unité attendue. Le déploiement a été mis en pause, pas après avoir cassé la moitié de la flotte, mais après avoir menacé un seul hôte.
La correction a pris des minutes : retirer la publication du port côté hôte du conteneur, utiliser le Node Exporter existant, et empêcher le chart de réclamer le port à nouveau. Pas de drame, pas de reboot, pas de salle de crise. Juste une petite vérification ennuyeuse qui a empêché un gros incident ennuyeux.
C’est ça, l’excellence opérationnelle en pratique : ce n’est pas des actes héroïques, c’est refuser les surprises en production.
FAQ
1) Pourquoi « Address already in use » arrive quand rien ne tourne ?
Généralement parce que quelque chose tourne, juste pas ce que vous attendez : une unité socket systemd, un proxy de conteneur, ou un écouteur lié en IPv6 alors que vous avez cherché en IPv4. Vérifiez avec ss sur les deux familles et contrôlez systemctl list-sockets.
2) Quelle est la meilleure commande pour trouver qui utilise un port sur Debian 13 ?
ss -ltnp (TCP) et ss -lunp (UDP). Utilisez un filtre précis comme 'sport = :8080' pour ne pas vous noyer dans la sortie.
3) Dois-je installer net-tools pour netstat ?
Non, sauf si votre mémoire musculaire vous sauve pendant un incident et que vous acceptez l’échange. Debian 13 est conçu pour les outils iproute2. Apprenez ss ; ça vaut le coup.
4) Deux processus peuvent-ils écouter sur le même port TCP ?
Pas de la façon simple que les gens espèrent. Il existe des cas avancés avec SO_REUSEPORT où plusieurs workers partagent un port, mais c’est normalement une conception interne à un service. Pour deux démons non liés, considérez la réponse comme « non ».
5) TIME_WAIT empêche-t-il mon service de binder son port ?
Presque jamais pour le port d’écoute serveur. TIME_WAIT concerne plutôt les connexions sortantes fermées. Si bind échoue, trouvez l’écouteur réel ou l’unité socket.
6) Pourquoi ss montre que systemd possède le port ?
Activation de socket. systemd bind intentionnellement le port via une unité .socket. Arrêtez/désactivez/masquez l’unité socket si vous voulez que le port soit libéré définitivement.
7) Comment corriger un conflit de port sans redémarrer ?
Identifiez le propriétaire, arrêtez-le via son superviseur (systemd/Docker), désactivez le chemin d’auto-démarrage (service/socket/conteneur), puis démarrez le service prévu et vérifiez avec ss.
8) Comment savoir si le conflit est IPv4 ou IPv6 ?
Vérifiez les deux : ss -ltnp 'sport = :PORT' et ss -ltnp -6 'sport = :PORT'. Cherchez 0.0.0.0 vs [::], et pour des adresses spécifiques comme 127.0.0.1.
9) Et si le port est possédé par un processus utilisateur et que j’en ai besoin ?
Ne commencez pas par tuer. Identifiez l’utilisateur et la session, coordonnez-vous, et définissez une politique : les ports de production sont réservés. Si vous devez récupérer rapidement le port, arrêtez le processus avec SIGTERM, puis suivez avec une correction durable.
Conclusion : prochaines étapes pour ne plus être réveillé
« Address already in use » n’est pas une énigme. C’est un différend de propriété. Le noyau vous dit qu’il y a déjà un locataire sur ce tuple adresse/port, et votre travail est de déterminer si ce locataire est légitime.
Faites ceci ensuite :
- Standardisez l’usage de
sspour les vérifications de propriété de port et intégrez-le à votre runbook. - Quand vous trouvez un PID, mappez-le toujours à une unité ou un conteneur. Corrigez le système qui le lance, pas seulement le processus.
- Auditez les services socket-activés et documentez-les. Si PID 1 possède un port, c’est probablement intentionnel.
- Créez un registre d’allocation de ports pour votre environnement (même un petit). Faites-le appliquer dans les revues.
- Ajoutez une vérification légère qui valide que les ports critiques sont détenus par les services attendus, avant les déploiements.
Les reboots servent aux mises à jour du noyau et aux rares caprices de pilote. Les conflits de ports méritent mieux : des preuves, une correction propre, et un futur où vous ne redécouvrez pas la même vérité à 2h du matin.