docker compose up -d が port is already allocated で落ち、続けて打った docker compose exec が service "app" is not running を返す。この記事では2つのメッセージの関係を原文から読み、ホストポートを掴んでいる相手を特定して直すまでを、Docker Desktop + WSL2 環境の実際の出力で追います。非対象は Linux ネイティブの Docker Engine、Kubernetes、本番向けのポート設計です。
1. 検証環境と、この記事が受け取るエラー文字列
- Windows 11
- WSL2(Ubuntu)
- Docker Desktop 4.55.0(Docker Engine 29.1.3)
nginx:1.27-alpine
コマンドは断りがない限り WSL 側のシェルで実行します。Windows 側で実行するものには PowerShell と明記します。
この記事が扱うのは、次の文字列で検索して来た場合です。同じ原因でもコマンドによって前半が変わり、共通するのは末尾の port is already allocated だけになります。
Bind for 0.0.0.0:8080 failed: port is already allocated
service "app" is not running
エラー本文の前半(failed to set up container networking: の部分)は Engine のバージョンで文言が変わります。この記事の原文はすべて Engine 29.1.3 で採取したものなので、手元の出力と前半が違う場合は末尾の port is already allocated で読み合わせてください。
2. ホストポート 8080 を2つのプロジェクトで取り合って再現する
再現に必要なのは、同じホストポートを公開する compose プロジェクト2つだけです。Windows のターミナルで wsl を実行して Ubuntu に入り、作業ディレクトリを作成します。
# Windows側から始める場合のみ実行
# wsl
mkdir -p ~/projects/docker-port-is-already-allocated-demo/site-a
mkdir -p ~/projects/docker-port-is-already-allocated-demo/site-b
cd ~/projects/docker-port-is-already-allocated-demo
code .
site-a/compose.yml を作成します。こちらが先にポートを掴む側です。
services:
web:
image: nginx:1.27-alpine
ports:
- "8080:80"
site-b/compose.yml を作成します。重ねるのはホスト側のポートだけです。
services:
app:
image: nginx:1.27-alpine
ports:
- "8080:80"
先に site-a を起動します。
cd ~/projects/docker-port-is-already-allocated-demo/site-a
docker compose up -d
初回はイメージの取得が入ります。取得後の出力が次の6行です。
Network site-a_default Creating
Network site-a_default Created
Container site-a-web-1 Creating
Container site-a-web-1 Created
Container site-a-web-1 Starting
Container site-a-web-1 Started
続けて site-b を起動すると衝突します。
cd ~/projects/docker-port-is-already-allocated-demo/site-b
docker compose up -d
Network site-b_default Creating
Network site-b_default Created
Container site-b-app-1 Creating
Container site-b-app-1 Created
Container site-b-app-1 Starting
Error response from daemon: failed to set up container networking: driver failed programming external connectivity on endpoint site-b-app-1 (5dac0f8c582949c2637d1c2b988137e3567260d8a05602c41aba695197238fc2): Bind for 0.0.0.0:8080 failed: port is already allocated
終了コードは 1 です。docker run で同じ衝突を起こすと、docker: の接頭辞と末尾のヘルプ行が付き、終了コードは 125 になります。
docker: Error response from daemon: failed to set up container networking: driver failed programming external connectivity on endpoint musing_jackson (0440fa512d551909272d17b78e7d13330288f34c1969fade373a522c493a1c80): Bind for 0.0.0.0:8080 failed: port is already allocated
Run 'docker run --help' for more information
docker run -d の場合はコンテナ ID が1行目に出てからエラーが続きます。ID が出ていても、そのコンテナは起動前の状態です。
3. 原文のどこが原因で、どこが巻き添えか
1行のメッセージに4つの情報が入っています。読む順序は末尾からです。
Error response from daemon: failed to set up container networking: driver failed programming external connectivity on endpoint site-b-app-1 (5dac0f8c...): Bind for 0.0.0.0:8080 failed: port is already allocated
Bind for 0.0.0.0:8080 failed: port is already allocated— ここだけが原因です。0.0.0.0:8080はホスト側の待ち受けアドレスとポートで、コンテナ側の80はこの行に出てきません。compose.ymlの"8080:80"のうち、問題になっているのは左の数字だけになりますendpoint site-b-app-1— 名指しされているのは起動に失敗した側です。8080 を掴んでいる相手はこの行に出てきません。掴んでいる側の名前は5章のdocker ps --filter publishで出します(5dac0f8c...)— endpoint ID で、起動のたびに変わります。検索エンジンに貼るときは削ってくださいfailed to set up container networking:/driver failed programming external connectivity— bind 失敗を包んだ結果側の説明で、独立した原因ではありません
先頭の 0.0.0.0 は「すべてのインターフェースで待ち受ける」指定ですが、これは自分の compose.yml の写しではありません。site-b を "127.0.0.1:8080:80" とホスト側 IP 付きに書き換えて衝突させても、原文の IP は変わりませんでした。
Error response from daemon: failed to set up container networking: driver failed programming external connectivity on endpoint site-b-app-1 (95071a2b5e852f88eb44d226a4af7a02365081bd15446e99b884d5dc5ef04147): Bind for 0.0.0.0:8080 failed: port is already allocated
掴んでいる site-a 側が 0.0.0.0 で公開している以上、ループバックだけに絞っても衝突は避けられません。照合に使えるのはポート番号だけです。
4. service "app" is not running は原因ではなく結果
起動に失敗したあと、そのまま次のコマンドを打つと別のメッセージに変わります。
docker compose exec app nginx -v
service "app" is not running
このメッセージは「ポートが埋まっている」とは一言も言いません。しかも docker compose ps が返すのは見出しだけです。
docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
ログも空のまま終了コード 0 で返ります。コンテナのプロセスが一度も起動しておらず、出力が存在しないためです。
docker compose logs app
コンテナ自体は消えたわけではなく、-a を付けると Created で残っているのが見えます。
docker compose ps -a
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
site-b-app-1 nginx:1.27-alpine "/docker-entrypoint.…" app 1 second ago Created
失敗の原文が残っているのは、このコンテナの中です。up のスクロールを流してしまっても、ここから読み直せます。
docker inspect site-b-app-1 --format '{{.State.Status}} / {{.State.Error}}'
created / failed to set up container networking: driver failed programming external connectivity on endpoint site-b-app-1 (6e31cce94f99a8986f7dcd962e03f5c309656a5e0c179bb1250f4d07add87fbf): Bind for 0.0.0.0:8080 failed: port is already allocated
直す順序はポート側が先です。service "app" is not running が報告しているのは「コンテナが起動していない」という状態で、起動を止めているのは 8080 の bind 失敗のほうにあります。このメッセージが消えるのは、ポートが空いた時点です。
なお、up の側にポート衝突の原文が無く no space left on device が出ている場合は、起動を止めている原因が別にあります。Docker のデータ領域が満杯だとコンテナを作成できず、その後の exec が同じ service "app" is not running を返すためです。その切り分けは Dockerで「no space left on device」を解決する(WSL2でDockerのディスクとマウント先を見分ける) にあります。
5. 8080 を掴んでいるコンテナを特定する
公開ポートで絞り込むと、掴んでいる側が1行で出ます。
docker ps --filter publish=8080
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a99315b4195a nginx:1.27-alpine "/docker-entrypoint.…" 27 seconds ago Up 26 seconds 0.0.0.0:8080->80/tcp, [::]:8080->80/tcp site-a-web-1
NAMES の site-a-web-1 が相手で、PORTS の 0.0.0.0:8080->80/tcp が原文の 0.0.0.0:8080 と一致する。この一致が取れれば、掴んでいる側の特定はここで終わりです。
一方、OS 側のコマンドからコンテナ名へは辿り着けません。Windows の PowerShell で見ると、待ち受けているのは Docker Desktop 本体です。
> netstat -ano | findstr :8080
TCP 0.0.0.0:8080 0.0.0.0:0 LISTENING 16776
TCP [::]:8080 [::]:0 LISTENING 16776
TCP [::1]:8080 [::]:0 LISTENING 15556
> tasklist /FI "PID eq 16776"
Image Name PID Session Name Session# Mem Usage
========================= ======== ================ =========== ============
com.docker.backend.exe 16776 Console 2 225,708 K
PID 16776 は com.docker.backend.exe、つまり Docker Desktop のバックエンドです。この PID を taskkill しないでください。 Docker Desktop ごと止まり、掴んでいるコンテナは残ります([::1] 側の 15556 は wslrelay.exe で、これも WSL の中継プロセスです)。
WSL 側から ss で見た場合も、Docker が公開したポートについては Process 列が空になります。
$ ss -ltnp 'sport = :8080'
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0 4096 *:8080 *:*
同じ WSL の中で自分が起動したプロセスなら、Process 列に名前と PID が出ます。
$ ss -ltnp 'sport = :9099'
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0 5 0.0.0.0:9099 0.0.0.0:* users:(("python3",pid=13857,fd=3))
ホストのポートを自分のプロセスで代理受けしてからコンテナへ転送するのが、Docker Desktop の仕組みです。そのため netstat と ss が名指しできるのは代理側までで、コンテナ名は docker ps からしか出ません。netstat の出番は、docker ps --filter publish=8080 が空だったとき(9章)です。
なお、WSL の中で動いている自分のプロセスが同じ番号を使っていても、この衝突は起きません。0.0.0.0:9099 を待ち受ける python3 を動かしたまま docker run -p 9099:80 を実行しても、起動は成功です。Docker Desktop の Docker Engine は別のディストリビューションで動くため、ポートの取り合いになりません。port is already allocated を見て WSL 側のプロセスを探すのは空振りです。
6. ホストポートを変えるか、掴んでいる側を止めるか
どちらでも直ります。site-a が動き続ける必要があるかどうかで選んでください。
対処A: site-b のホストポートを変える
site-b/compose.yml を更新し、左の数字だけ 8081 にします。
services:
app:
image: nginx:1.27-alpine
ports:
- "8081:80"
site-b で起動し直します。
cd ~/projects/docker-port-is-already-allocated-demo/site-b
docker compose up -d
Container site-b-app-1 Recreate
Container site-b-app-1 Recreated
Container site-b-app-1 Starting
Container site-b-app-1 Started
表示された Recreated は、compose.yml の変更で設定が変わり、失敗した残骸が作り直されたことを表す行です。公開ポートを確認します。
docker compose ps
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8081/
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
site-b-app-1 nginx:1.27-alpine "/docker-entrypoint.…" app 3 seconds ago Up 2 seconds 0.0.0.0:8081->80/tcp, [::]:8081->80/tcp
200
合格条件は、PORTS の 0.0.0.0:8081->80/tcp と curl の 200 の2つです。
対処B: 掴んでいる site-a を止める
site-a のほうが不要なら、そちらを停止します。
cd ~/projects/docker-port-is-already-allocated-demo/site-a
docker compose stop
Container site-a-web-1 Stopping
Container site-a-web-1 Stopped
停止したコンテナはポートを掴みません。docker ps --filter publish=8080 が空になったのを確認してから、site-b を起動し直してください。
cd ~/projects/docker-port-is-already-allocated-demo/site-b
docker compose down
docker compose up -d
down を挟む理由は8章です。compose.yml を書き換えない対処Bでは、up -d だけでは残骸が作り直されません。
7. ports の右側をいくら変えても衝突は消えない
ポートを変える対処で結果を分けるのは、"8080:80" のどちらを書き換えるかです。右側だけを変えても、同じエラーが同じ数字で出続けます。
services:
app:
image: nginx:1.27-alpine
ports:
- "8080:8081"
Error response from daemon: failed to set up container networking: driver failed programming external connectivity on endpoint site-b-app-1 (4ce1e9811d9068c3bbb77f71a43cf1be16df53f0fb9e7d4f66b740c9810fcaa7): Bind for 0.0.0.0:8080 failed: port is already allocated
原文の 0.0.0.0:8080 が変わっていません。右側はコンテナの中で待ち受けているポートを指すので、ホスト側の奪い合いには関係しないためです。
左右をまとめて入れ替えると、今度はエラーを出さずに壊れます。"8081:8080" にすると起動は成功し、docker compose ps の見た目も正常です。
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
site-b-app-1 nginx:1.27-alpine "/docker-entrypoint.…" app 3 seconds ago Up 2 seconds 0.0.0.0:8081->8080/tcp, [::]:8081->8080/tcp
しかし転送先の 8080 では nginx が待っていないため、接続だけが失敗します。
$ curl -v http://localhost:8081/
* Trying [::1]:8081...
* Connected to localhost (::1) port 8081
> GET / HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/8.5.0
> Accept: */*
>
* Recv failure: Connection reset by peer
* Closing connection
curl の終了コードは 56 です。左右の意味は Docker初心者向け: compose.yml の中身を1項目ずつ読む入門 の ports の節で扱っています。
注意したいのは、ports の指定が正しくても Recv failure: Connection reset by peer が出る点です。コンテナ内のアプリが 127.0.0.1 で待っている場合と、アプリが起動していない場合が同じ文言になり、docker compose ps の表示でも区別できません。3つの見分け方は Dockerで「localhostに繋がらない」を解決する(WSL2でコンテナの待受アドレスを見分ける) にあります。
8. ポートを空けたのに繋がらない——Created で残ったコンテナの罠
落とし穴があるのは、対処Bを選んだ場合だけです。site-a を止めて 8080 を空け、site-b で up -d を打ち直すと、Compose は成功したように見えます。
Container site-b-app-1 Starting
Container site-b-app-1 Started
終了コードは 0 です。ところが docker compose ps の PORTS を見ると、公開ポートが消えています。
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
site-b-app-1 nginx:1.27-alpine "/docker-entrypoint.…" app 7 seconds ago Up 2 seconds 80/tcp
80/tcp はコンテナ側の公開表示だけで、ホストへの割り当てが付いていない形です。docker port は何も返さず、接続も通りません。
$ docker port site-b-app-1
$ curl http://localhost:8080/
curl: (7) Failed to connect to localhost port 8080 after 0 ms: Couldn't connect to server
docker compose restart でも同じ結果になり、しかもこちらは 8080 がまだ埋まっている状態でさえ Started と表示して終了コード 0 を返します。
inspect で見えるのは、設定としてのポート指定は残っているのに、実際の割り当てとネットワーク接続だけが空になっている状態です。
docker inspect site-b-app-1 --format '{{json .HostConfig.PortBindings}}'
docker inspect site-b-app-1 --format '{{json .NetworkSettings.Ports}}'
docker inspect site-b-app-1 --format '{{json .NetworkSettings.Networks}}'
{"80/tcp":[{"HostIp":"","HostPort":"8080"}]}
{"80/tcp":[]}
{}
最後の {} は、このコンテナがどのネットワークにも繋がっていないことを示します。正常なコンテナで同じ場所を見ると、プロジェクトのネットワーク名と IP アドレスが入った JSON です(長いので抜粋)。
{"site-a_default":{ ... ,"Gateway":"172.21.0.1","IPAddress":"172.21.0.2", ... }}
影響は公開ポートだけにとどまりません。ネットワーク未接続のまま Up になっているため、コンテナからの外向き通信も落ちます。
docker exec site-b-app-1 wget -qO- --timeout=3 http://example.com
wget: bad address 'example.com'
コンテナの中では nginx が正常に応答するので、プロセスの起動そのものは成功です。
docker exec site-b-app-1 wget -qO- --timeout=3 http://127.0.0.1/
<!DOCTYPE html>
<html>
<head>
この検証では、start も restart も NetworkSettings.Networks を {} のままにし、作り直したときだけ復旧しました。created で残ったコンテナを起こす経路でネットワークの再接続が行われていないことになりますが、Engine 内部の実装まで確かめたわけではないので、ここでは観測された範囲にとどめます。復旧の手順は作り直しです。
docker compose down
docker compose up -d
down を避けたい場合の代替が --force-recreate です。
docker compose up -d --force-recreate
Container site-b-app-1 Recreated
Container site-b-app-1 Starting
Container site-b-app-1 Started
どちらの場合も、docker compose ps の PORTS に 0.0.0.0:8080->80/tcp が戻っていることを確認してください。Up という表示は、公開ポートが有効であることを保証しません。
対処A(compose.yml のポートを書き換える)でこの問題が起きないのは、設定が変わると Compose が自動的に Recreated へ倒すからです。書き換えずにポートを空けた対処Bだけが、残骸をそのまま起こしにいきます。
9. 「ポートが使えない」の3系統を原文で見分ける
docker ps --filter publish=<ポート> が空なら、原因はコンテナの衝突ではありません。Docker Desktop 環境では、原因ごとにメッセージが別物です。
flowchart TD
A["起動が失敗した"] --> B{"メッセージの末尾"}
B -->|"port is already allocated"| F["docker ps --filter publish で相手を出す"]
B -->|"Only one usage of each socket address"| G["netstat -ano で PID を引く"]
B -->|"forwards/expose returned unexpected status 500"| H["netsh で確保済み範囲を確認する"]
F --> I{"該当するコンテナは"}
I -->|"出た"| C["そのコンテナが掴んでいる"]
I -->|"空だった"| J["この3例では確定できない"]
G --> D["Windows 側のプロセスが掴んでいる"]
H --> E["Windows の予約ポート範囲に入っている"]
port is already allocated の枝で docker ps --filter publish が空だった場合、この記事で再現した3例には当てはまりません。疑う範囲は、Docker Desktop の再起動、docker context の切り替わり、別の Docker 環境の併用などです。
Windows 側のプロセスが掴んでいる場合は、port is already allocated にはなりません。Docker 以外のプロセスに 8083 を握らせた状態で docker run -d -p 8083:80 を打つと、Windows のソケットエラーがそのまま出ます。
docker: Error response from daemon: ports are not available: exposing port TCP 0.0.0.0:8083 -> 127.0.0.1:0: listen tcp 0.0.0.0:8083: bind: Only one usage of each socket address (protocol/network address/port) is normally permitted.
netstat -ano が本来の使い方になるのは、この系統だけです。出てきた PID が Docker Desktop 以外なら、そのプロセスを止めるか、ホストポートをずらしてください。
予約ポート範囲に入っている場合の文言は、さらに別物です。Hyper-V や WSL が起動時に確保した範囲へ -p で入り込むと、誰も待ち受けていないのに bind できません。
docker: Error response from daemon: ports are not available: exposing port TCP 0.0.0.0:49900 -> 127.0.0.1:0: /forwards/expose returned unexpected status: 500
このとき 49900 を LISTEN しているプロセスは存在せず、netstat -ano | findstr :49900 の結果も空です。確保済みの範囲は PowerShell で確認できます。
> netsh interface ipv4 show excludedportrange protocol=tcp
Protocol tcp Port Exclusion Ranges
Start Port End Port
---------- --------
49894 49993
50000 50059 *
50060 50159
50160 50259
50260 50359
50460 50559
61870 61969
* - Administered port exclusions.
49900 が入るのは先頭の 49894-49993 です。ただし表に出た範囲がすべて等しく塞がるわけではなく、* 付きの 50000-50059 に入る 50000 では同じ手順がそのまま通りました。範囲の一覧は再起動のたびに変わるため、この記事の数字ではなく手元の出力と突き合わせてください。今回確保されていたのはいずれも 49894 以上で、8080 や 8081 のような番号は含まれていません。順序としては、ports のホスト側に動的ポート域の番号を選んだうえで ports are not available が出たとき、はじめてこの表を見る形です。
10. 片付けと、対処
検証用のコンテナとネットワークを削除します。このデモの2プロジェクトはどちらも nginx を起動するだけなので、消えて困るものはありません。
cd ~/projects/docker-port-is-already-allocated-demo/site-a
docker compose down
cd ~/projects/docker-port-is-already-allocated-demo/site-b
docker compose down
8章の down や --force-recreate を自分の構成へ読み替えるときは、先にデータの置き場所を確認してください。-v を付けなければ named volume は残りますが、コンテナの中だけに書いたデータは作り直しで消えます。
症状ごとの対処は次の6件です。
- 原文の数字と名前の対応が分からない → 3章
service "app" is not runningしか出ていない → 4章(先に直すのはポート側)- 掴んでいる相手が分からない → 5章
- ポートを変えたのに同じエラーが出る → 7章(左の数字を変える)
- ポートを空けたのに繋がらない → 8章(
downしてからup -d) docker ps --filter publishが空 → 9章
compose.yml の各キーの読み方は Docker初心者向け: compose.yml の中身を1項目ずつ読む入門、イメージ側の設定と起動設定の切り分けは Docker初心者向け: compose.yml と Dockerfile の違いを最小構成で理解する にあります。
バインドマウント越しにファイルを編集できなくなる症状は Dockerで「Permission denied」を解決する(WSL2のバインドマウントとUIDの食い違い) で扱っています。