Die Meldung kommt immer dann, wenn du gerade fertig sein wolltest. Die Compose-Datei steht, du tippst docker compose up -d, und statt eines laufenden Dienstes bekommst du das hier:

Error response from daemon: driver failed programming external connectivity on endpoint proxy: Bind for 0.0.0.0:80 failed: port is already allocated

Docker sagt dir damit, dass Tür Nummer 80 auf deinem Server schon jemandem gehört. Wem, sagt es nicht. Genau diese Lücke schließen wir: In drei Schritten findest du den Prozess, der den Port hält, entscheidest, ob er bleiben darf, und bekommst den Port frei. Ohne den ganzen Server neu zu starten und ohne blind etwas abzuschießen.

Port? Die IP-Adresse ist die Hausnummer deines Servers, der Port die Wohnungstür dahinter. Hinter der 80 wohnen Webseiten, hinter der 53 der DNS-Dienst, hinter der 22 SSH. Und wie bei einer echten Wohnung kann hinter jeder Tür nur einer wohnen. Will ein zweiter Dienst dieselbe Nummer, bleibt er draußen.
Kurz gesagt: Ein Befehl zeigt dir, welcher Prozess den Port hält. Danach stoppst du den Dienst, verlegst den Container auf eine andere Nummer oder startest Docker neu, je nachdem, wer es war. Dauer: zehn Minuten. Stufe: Einsteiger. Stand: Docker Engine 28, Ubuntu 24.04, iproute2 6.x, geprüft am 07-09-2026.
Port belegt: 3 Schritte bis er frei ist
Nachsehen, zuordnen, freigeben: der ganze Weg auf einen Blick.

Der Fehler entsteht, bevor dein Dienst überhaupt läuft. Docker bittet den Linux-Kernel um Port 80, der Kernel schaut in seine Liste und sagt Nein. Was in der Liste steht, kannst du dir anzeigen lassen, und genau das ist Schritt 1.

dein Container→ will Port 80 →Linux-Kernel→ schon vergeben an →apache2 (PID 812)

Was du brauchst

  • Einen Linux-Server, auf dem der Fehler auftritt: Debian, Ubuntu, Raspberry Pi OS oder ein LXC auf Proxmox. Du brauchst ein Konto mit sudo-Rechten, sonst zeigt dir der Kernel nicht, wem der Port gehört.
  • Das Werkzeug ss. Es gehört zum Paket iproute2 und ist auf jedem aktuellen Linux vorinstalliert.
  • Docker nur dann, wenn ein Container im Spiel ist. Der Fehler kommt genauso bei Diensten, die ohne Docker laufen.
So liest du diese Anleitung: Alles, was du selbst tun sollst, steht in einem farbigen Kasten. [ per klick ] heißt: Das erledigst du mit der Maus in einer Web-Oberfläche, hier etwa Portainer oder das Proxmox-Dashboard. [ über die shell ] heißt: Den Befehl kopieren, ins Terminal einfügen, Enter. Stehen beide Kästen direkt untereinander, führen sie zum selben Ergebnis. Du gehst EINEN davon, nicht beide.

Ob ss da ist, prüfst du mit einer Zeile:

ss -V
docker --version

Die erste Zeile antwortet mit ss utility, iproute2-6.x, die zweite mit einer Docker-Version. Fehlt Docker, weil dein Dienst ohne Container läuft, ist das in Ordnung.

  • Der belegte Port im Beispiel: 80
  • Der Container, der ihn haben will: proxy im Ordner ~/proxy
  • IP-Adresse des Servers: 192.168.178.20
  • Zweites Beispiel, der Pi-hole-Klassiker: Port 53

Schritt 1: Nachsehen, wer auf dem Port lauscht

Der Kernel führt Buch über jeden Port, an dem gerade ein Prozess wartet. ss liest diese Liste aus. Die Buchstaben dahinter sagen: TCP und UDP anzeigen (-t, -u), nur wartende Ports (-l), den Prozess dazu (-p) und Zahlen statt Namen (-n). Der Filter am Ende sorgt dafür, dass du wirklich nur Port 80 siehst.

sudo ss -tulpn 'sport = :80'

Geklappt hat es, wenn eine Zeile mit LISTEN erscheint und ganz rechts ein Prozessname mit PID steht.

So sieht die Antwort aus, wenn ein Apache den Port hält:

Netid State  Recv-Q Send-Q Local Address:Port Peer Address:Port Process
tcp   LISTEN 0      511          0.0.0.0:80        0.0.0.0:*     users:(("apache2",pid=812,fd=4),("apache2",pid=811,fd=4))

Die Spalte ganz rechts ist die Antwort auf deine Frage: apache2 ist der Name des Prozesses, pid=812 seine Nummer. Beides brauchst du gleich. Steht dort nichts und die Zeile endet nach 0.0.0.0:*, hast du das sudo vergessen. Ohne Root-Rechte darf ss fremde Prozesse nicht benennen, und dann rätst du weiter.

Wenn du lsof lieber magst, kommt dasselbe dabei heraus. Es listet alle Prozesse, die eine Netzwerkadresse mit diesem Port geöffnet haben, in Spalten mit COMMAND, PID und USER:

sudo lsof -nP -i :80

Die Zeile mit (LISTEN) am Ende nennt den Prozess. -n und -P verhindern nur, dass lsof Adressen und Portnummern in Namen übersetzt; das macht die Ausgabe schneller und lesbarer.

Ein Wort zur Ausgabe, weil sie am Anfang verwirrt: 0.0.0.0:80 heißt, der Prozess nimmt Port 80 auf allen Adressen des Servers an. 127.0.0.53%lo:53 heißt, er lauscht nur intern auf dieser einen Adresse. Das zweite Muster begegnet dir gleich noch einmal.

Schritt 2: Den Namen einordnen

Was du in der rechten Spalte gefunden hast, gehört in eine von drei Schubladen. Von der Schublade hängt ab, wie du den Port freibekommst.

Ein Systemdienst. Namen wie apache2, nginx, lighttpd, cupsd oder systemd-resolve stehen für Dienste, die systemd beim Hochfahren startet. Der Apache ist der Klassiker: Debian und Ubuntu starten ihn direkt nach apt install, und wer ihn mal für einen Test installiert hat, hat ihn seither auf Port 80 sitzen. Prüfen kannst du das so:

systemctl status apache2

Steht in der Zeile Active: ein active (running) und in der Zeile Loaded: ein enabled, läuft der Dienst und kommt nach jedem Neustart wieder.

Ein Container. Steht rechts docker-proxy, hält ein anderer Container den Port. Welcher, verrät ss nicht, das weiß nur Docker selbst.

docker-proxy? Der Pförtner, den Docker für jeden veröffentlichten Port auf dem Server anstellt. Er nimmt an der Haustür entgegen, was für den Container bestimmt ist, und reicht es nach innen durch. Für den Kernel ist er der Bewohner der Tür, nicht der Container.
docker ps --filter publish=80 --format "table {{.Names}}\t{{.Ports}}"

Die Tabelle zeigt den Container, der Port 80 veröffentlicht, mit Namen und Zuordnung wie 0.0.0.0:80->80/tcp. Bleibt die Tabelle leer, obwohl ss einen docker-proxy zeigt, lies weiter bei „Der Geist von Docker“.

Etwas von Hand Gestartetes. Namen wie python3, node oder java mit deinem Benutzernamen dahinter bedeuten meist: Jemand hat in einer zweiten SSH-Sitzung oder in einem tmux-Fenster etwas gestartet und vergessen. Oft warst das du selbst vor drei Wochen.

Am Ende dieses Schritts kannst du den Satz sagen: „Port 80 gehört apache2, einem Systemdienst.“ Erst dann geht es weiter, denn die Schublade entscheidet über den Handgriff.

Schritt 3: Den Port freigeben

Ein Backup gibt es hier nicht zu machen, dafür eine Notiz: Schreib dir auf, was du gleich abschaltest. In drei Monaten fragst du dich sonst, warum der Apache nicht mehr läuft.

Ein Systemdienst, den du nicht brauchst

stop allein reicht nicht, der Dienst käme beim nächsten Neustart zurück. Laut systemctl-Handbuch stoppt disable den Dienst nicht von selbst; erst der Zusatz --now tut beides in einem Rutsch: jetzt beenden und nie wieder automatisch starten.

sudo systemctl disable --now apache2
sudo ss -tulpn 'sport = :80'

Die zweite Zeile antwortet jetzt nur noch mit der Kopfzeile und keiner Treffer-Zeile darunter. Dann ist der Port frei.

Ich würde nur abschalten, was ich beim Namen kenne. Bei einem Apache, den niemand bestellt hat, fällt das leicht. Bei systemd-resolve nicht, und weil das der häufigste Fall im Homelab ist, bekommt er einen eigenen Abschnitt.

systemd-resolved auf Port 53: der Pi-hole-Klassiker

Wer Pi-hole oder AdGuard Home als Container auf Ubuntu startet, bekommt fast immer diese Meldung:

Error starting userland proxy: listen tcp4 0.0.0.0:53: bind: address already in use

Die Pi-hole-Doku erklärt, warum: Ubuntu seit 17.10 und Fedora seit 33 bringen systemd-resolved mit, das auf der internen Adresse 127.0.0.53 einen kleinen DNS-Zwischenspeicher auf Port 53 betreibt. Im Pi-hole-Forum schrieb der Nutzer Zopyrus, er sei sich „zu 99,9 Prozent sicher“, dass es dieser Dienst ist, und der Faden läuft seit Jahren. Du bist also in guter Gesellschaft.

Stub-Resolver? Der kleine Zettelkasten am Empfang: Wer eine Adresse wissen will, fragt erst dort, und nur wenn der Zettel fehlt, geht die Frage weiter nach draußen. Pi-hole will genau diesen Empfangstresen selbst besetzen.

Den ganzen Dienst abzuschalten wäre der Holzhammer. Die Pi-hole-Doku warnt, dass dann die Namensauflösung in VPNs bricht und netplan nicht mehr funktioniert. Du schaltest deshalb nur den Zettelkasten ab, den Rest lässt du laufen. Die Befehle stammen wörtlich aus der Pi-hole-Doku; der zweite tauscht die Datei /etc/resolv.conf aus, kopier sie dir vorher weg.

sudo cp /etc/resolv.conf ~/resolv.conf.alt
sudo sh -c 'mkdir -p /etc/systemd/resolved.conf.d && printf "[Resolve]\nDNSStubListener=no\n" | tee /etc/systemd/resolved.conf.d/no-stub.conf'
sudo sh -c 'rm -f /etc/resolv.conf && ln -s /run/systemd/resolve/resolv.conf /etc/resolv.conf'
sudo systemctl restart systemd-resolved
sudo ss -tulpn 'sport = :53'
ping -c 1 debian.org

Die ss-Zeile zeigt keinen Treffer mehr, und der Ping bekommt eine Antwort. Beides zusammen heißt: Port 53 ist frei, und dein Server kann trotzdem noch Namen auflösen.

Die Zeile mit DNSStubListener=no ist der eigentliche Schalter. Laut Handbuch zu resolved.conf steht er standardmäßig auf yes und lässt den Zwischenspeicher auf 127.0.0.53, Port 53, für TCP und UDP lauschen. Die Datei landet als Ergänzung im Ordner resolved.conf.d, damit ein Systemupdate deine Änderung nicht überschreibt.

Ein anderer Container hält den Port

Hier würde ich nichts abschießen. Beide Container sollen ja laufen, also bekommt der Neue eine andere Hausnummer. Dazu änderst du in der Compose-Datei die linke Zahl der Port-Zeile, denn links steht der Port auf dem Server, rechts der im Container.

cd ~/proxy
nano compose.yaml

Suche die Zeile - "80:80" und mache daraus - "8080:80". Speichern mit Strg+O und Enter, schließen mit Strg+X.

docker compose up -d
docker ps --filter publish=8080 --format "table {{.Names}}\t{{.Ports}}"

Der Container startet ohne Fehlermeldung, und die Tabelle zeigt ihn mit 0.0.0.0:8080->80/tcp. Im Browser erreichst du ihn jetzt unter http://192.168.178.20:8080.

Auf 80 und 443 gibt es nur einen Gewinner. Sollen mehrere Dienste ohne Portnummer erreichbar sein, ist das die Aufgabe eines Reverse Proxys, der als Einziger diese beiden Türen besetzt und alles andere weiterreicht.

Der Geist von Docker

Manchmal zeigt ss einen docker-proxy, aber docker ps findet keinen Container dazu. Das ist ein alter Bekannter: Im Docker-Projekt liegt seit 2016 der Issue #26527 mit genau diesem Bild, der Pförtner blieb nach dem Löschen des Containers an der Tür stehen. Der Abhilfe, die dort funktionierte, kann ich nichts hinzufügen: Docker einmal durchstarten.

sudo systemctl restart docker
sudo ss -tulpn 'sport = :80'

Alle Container starten dabei kurz neu, das dauert ein paar Sekunden. Danach zeigt ss keinen Treffer mehr auf Port 80.

Ein Prozess, den jemand von Hand gestartet hat

Hier hilft die PID aus Schritt 1. kill schickt dem Prozess standardmäßig das Signal TERM, eine höfliche Bitte, sich zu beenden und aufzuräumen. Das Handbuch rät, erst danach zu -9 zu greifen, denn dieses Signal kann der Prozess nicht abfangen und räumt nichts mehr auf.

kill 4711
sudo ss -tulpn 'sport = :80'

Zeigt ss den Prozess nach ein paar Sekunden immer noch, dann kill -9 4711. Gehört der Prozess einem anderen Benutzer, brauchst du sudo davor.

Im Netz liest du oft fuser -k 80/tcp als Abkürzung. Ich würde sie meiden: Laut Handbuch schickt fuser -k ohne weitere Angabe sofort das Signal KILL, also den Holzhammer, ohne zu fragen. Zum Nachsehen ist sudo fuser -v 80/tcp dagegen ein guter dritter Weg, falls das Paket psmisc installiert ist.

Damit es nicht wieder passiert

Der Port ist frei, der Container läuft. Drei Einstellungen sorgen dafür, dass du diesen Beitrag nicht in vier Wochen wieder brauchst.

Ports nur dort veröffentlichen, wo sie hin sollen. Ein Port ohne Adresse, also "8080:80", landet auf 0.0.0.0, und die Docker-Doku nennt das selbst „standardmäßig unsicher“: Der Dienst ist dann für jeden erreichbar, der deinen Server sehen kann. Für alles, was nur der Reverse Proxy auf demselben Server ansprechen soll, schreibst du "127.0.0.1:8080:80". So liegt der Dienst hinter der eigenen Haustür und kann keinem anderen Gerät im Netz in die Quere kommen.

Eine Portliste führen. Docker führt keine zentrale Liste, welche Nummern vergeben sind; das erste Zusammentreffen passiert meist beim dritten Container. Ein Befehl zeigt dir den Stand, und den Stand schreibst du in eine Textdatei neben deine Compose-Ordner.

docker ps --format "table {{.Names}}\t{{.Ports}}"

Jede Zeile ein Container, rechts seine Ports. Was hier steht, ist vergeben.

Nach jedem apt install einmal nachsehen. Debian und Ubuntu starten installierte Dienste sofort und dauerhaft, ohne zu fragen. Ein sudo ss -tulpn direkt nach der Installation zeigt dir, welche Tür du gerade dazubekommen hast. Brauchst du sie nicht, schließt du sie gleich mit disable --now, solange du noch weißt, woher sie kam.

80 und 443 gehören dem Reverse Proxy. Jeder andere Dienst bekommt eine hohe Nummer, so wie Uptime Kuma auf 3001 oder Paperless auf 8000. Dann kann kein neuer Container mehr mit dem Proxy zusammenstoßen, und die Adressen im Browser bleiben trotzdem ohne Portnummer.

Wo es erfahrungsgemäß klemmt

grep findet den falschen Port. Viele Anleitungen empfehlen ss -tulpn | grep :80. Das trifft auch :8080 und :8000, und dann suchst du den Schuldigen am falschen Ort. Der Filter 'sport = :80' aus dem ss-Handbuch trifft genau eine Nummer.

Docker Desktop zeigt nichts. Auf Windows und Mac laufen die Container in einer eigenen kleinen virtuellen Maschine. Im Docker-Forum beschreibt ein Nutzer, dass netstat auf seinem Windows nichts auf dem Port fand, während Docker weiter „port is already allocated“ meldete. Der Konflikt lag in der VM, nicht auf dem Host. Geraten wurde dort zu docker system prune, das verwaiste Reste alter Container aufräumt.

Der Port ist nur für IPv6 belegt. Zeigt ss eine Zeile mit [::]:80 und keine mit 0.0.0.0:80, hält ein Prozess den Port nur auf IPv6. Docker will beides, und die Meldung sieht gleich aus. Der Weg ist derselbe wie oben, nur dass du nach dem Namen in der IPv6-Zeile suchst.

Der Pi-hole-Fix hält nicht. Im Pi-hole-Forum meldete sich Ende 2024 jemand, bei dem die Meldung trotz DNSStubListener=no blieb. Was du daraus mitnimmst: Nach der Änderung erst wieder mit ss nachsehen, ob Port 53 wirklich leer ist, und dann den Container starten. Der Befehl kostet zwei Sekunden, das Raten kostet einen Abend.

Der Port ist frei, und du weißt jetzt, warum

Ein Befehl statt eines Neustarts: So würde ich diesen Fehler immer angehen. Der Neustart des Servers löst das Problem nur bei einem Geisterprozess, und selbst da reicht der Neustart von Docker. In allen anderen Fällen kommt der Dienst nach dem Hochfahren wieder, und du stehst mit derselben Meldung da.

Was du abschaltest, kennst du beim Namen, sonst verlegst du lieber den Neuen. Das ist die ganze Regel. Sie kostet dich einmal zehn Minuten und erspart dir das Gefühl, mit geschlossenen Augen an Kabeln zu ziehen.

Welcher Dienst hat bei dir zuletzt einen Port blockiert, und hättest du auf ihn getippt? Mein erster Verdächtiger wäre immer der Apache aus einem vergessenen Test. Schreib deinen Fund in die Kommentare, das hilft dem Nächsten mit derselben Meldung.

Wenn Pi-hole der Auslöser war: In einem eigenen LXC auf Proxmox hat Pi-hole Port 53 für sich allein, und die ganze Geschichte mit systemd-resolved kommt gar nicht erst auf.

Quellen