Apple container はコンテナ毎にIPを持つ
この記事でやること
Apple 純正のコンテナCLI container の特色(コンテナごとに独立したIPを持つ)を使って、複数のアプリを全部 :80(や :3000)のまま同時に動かす手順をまとめます。docker run -p 3001:3000、-p 3002:3000…とホストのポートを取り合うあの作業から解放されます。
何が問題だったか(Docker Desktop の場合)
Docker Desktop(や Colima)は、コンテナのネットワークがホストの単一 localhost 越しに見えます。なので複数のアプリを動かすと:
docker run -d -p 3000:3000 frontend # localhost:3000
docker run -d -p 3001:3000 admin # localhost:3001 ← ずらす
docker run -d -p 3002:3000 storybook # localhost:3002 ← さらにずらす
とホスト側ポートを手作業でずらすことになり、.env やドキュメントに「ローカルは 3001」みたいなローカル専用の差分が増えていきます。
Apple container は「コンテナ毎にIP」
Apple container は各コンテナを軽量VMとして起動し、vmnet で1つずつ独自IPを割り当てます。Docker Desktop との違いを図にするとこうです。
Docker Desktop は全部 localhost なので 3000 が埋まると 3001・3002 …とホスト側ポートをずらして見分ける必要があります。Apple container は各コンテナが別IPなので、全員 :80 のまま -p なしで衝突しません。
実際に確認してみます。
# 3つとも同じ :80 のまま起動(-p は付けない)
container run -d --name web nginx:alpine
container run -d --name api nginx:alpine
container run -d --name admin nginx:alpine
container ls
NAME IMAGE STATE IP
web nginx:alpine running 192.168.64.29/24
api nginx:alpine running 192.168.64.30/24
admin nginx:alpine running 192.168.64.31/24
それぞれ別IPが振られています。IP は container ls の IP 列で見えるほか、スクリプトでは container ls --format json を jq で抜けます。
container ls --format json | jq -r '.[] | select(.configuration.id=="web") | .status.networks[0].ipv4Address'
# → 192.168.64.29/24
そのままホストから各IPの :80 に到達できます(-p 不要)。
curl -s -o /dev/null -w "%{http_code}\n" http://192.168.64.29/ # web → 200
curl -s -o /dev/null -w "%{http_code}\n" http://192.168.64.30/ # api → 200
curl -s -o /dev/null -w "%{http_code}\n" http://192.168.64.31/ # admin → 200
3 つとも :80 のまま、衝突なし。「ポート番号をずらす」という発想自体が要らなくなります。
名前でアクセスしたい(web.test のように)
IP 直打ちは味気ないので名前を付けたいところです。
ポイントは、自分で「名前→IP の対応」を用意しないといけないこと(コンテナへの到達自体は IP 直で標準で動くので、足すのは名前だけ)。一番手軽なのは /etc/hosts に1行書く方法です。名前→IP さえ与えれば普通に届くのは curl --resolve でも確認できます。
IP=$(container ls --format json | jq -r '.[] | select(.configuration.id=="web") | .status.networks[0].ipv4Address' | cut -d/ -f1)
curl --resolve "web.test:80:$IP" http://web.test/ # → 200(名前→IPを与えれば届く)
一番手軽なのは /etc/hosts に手動で1行(192.168.64.29 web.test)足すことですが、IP はコンテナを作り直すと変わるので、起動中のコンテナから自動で /etc/hosts を更新するのが実用的です。
/etc/hosts を「マーカーブロック」で sync / reset する
この「名前レイヤ」は Apple container 標準には無いので、自前で足します。コツは /etc/hosts の中に自分用のブロックをマーカーで囲むこと。そうすれば「稼働中コンテナから一括反映(sync)」と「自分の追記だけ全消し(reset)」をワンコマンドにできます。~/.zshrc に次の2関数を入れておくと便利です。
# 稼働中コンテナを <名前>.test として /etc/hosts に反映
ctr-hosts-sync() {
local b='# >>> apple-container >>>' e='# <<< apple-container <<<'
local entries
entries=$(container ls --format json | jq -r \
'.[] | select((.configuration.id|test("buildkit"))|not)
| "\(.status.networks[0].ipv4Address|split("/")[0]) \(.configuration.id).test"')
sudo sed -i '' "/$b/,/$e/d" /etc/hosts # 既存ブロックを削除
printf '%s\n%s\n%s\n' "$b" "$entries" "$e" | sudo tee -a /etc/hosts >/dev/null
echo "synced:"; echo "$entries"
}
# 自分のブロックだけ消す(他の行は触らない)
ctr-hosts-reset() {
sudo sed -i '' '/# >>> apple-container >>>/,/# <<< apple-container <<</d' /etc/hosts
echo "reset done"
}
ctr-hosts-sync # コンテナ起動後に1発 → web.test / api.test … が引けるように
ctr-hosts-reset # シュッと全消し(apple-container ブロックだけ)
-
マーカー行で囲むので、
resetはlocalhost等の既存行に触れず自分の追記分だけ消えます。 -
syncは「消してから足す」ので何度叩いても重複しません(IP が変わったら sync し直すだけ)。 -
buildkitなどの裏方コンテナは除外しています。
使いどころ
コンテナ毎IP(+必要なら名前)が効くユースケース例:
-
PR / ブランチごとのプレビューを同時起動:
pr-123.testpr-124.testを全部:3000のまま並べる。ポート割当の管理が不要。 -
OAuth / Cookie / CORS の検証:
app.testapi.testadmin.testを別ホスト名・別IPで立て、localhostでは再現しづらい SameSite Cookie・redirect URI・CORS の挙動をローカルで確認。 -
複数スタックを素の設定のまま:Rails も Next も Storybook も全部
:3000で起動。.envをローカル用にいじらなくて済む(※ dev サーバが0.0.0.0で listen している前提。127.0.0.1固定だとコンテナIP経由では届かないので、bind アドレスだけ0.0.0.0にする)。 - 送信元IP依存の機能:rate limit・IP allowlist などを、別IPのコンテナから叩いて確認(Docker Desktop の NAT 越しだと送信元が潰れがち)。
まとめ
- Apple container はコンテナごとに独自IPを持つので、
-pなし・ポート衝突なしで複数アプリを同じポートのまま同時起動できる。これが Docker Desktop との明確な差。 -
名前での自動解決は標準には無い。
container system dns createはコンテナ名を登録しないので、名前運用は/etc/hostsか簡単な自動化スクリプトで足す。 - 「localhost にポートを全部ぶら下げて番号で見分ける」世界から、「コンテナごとにIP(と名前)を持つ小さなネットワーク」へ、という発想の転換が一番のうまみです。
Discussion