時間がない人へのまとめ

この記事では、Docker Composeのポート競合やネットワーク競合を自動で検出・解決するコマンド「gopose」を作った話をします。

目次

まずgoposeの紹介

今回作ったコマンドは、gopose というコマンドで、Docker Composeのポート競合やネットワーク競合を自動で検出し、compose.override.yml を生成して解決するものです。

このコマンドの動作イメージは以下のような感じです。

$ gopose up

  SERVICE   ORIGINAL   RESOLVED   REASON
  web       3000       8001       port in use
  api       5432       8002       port in use

  NETWORK   ORIGINAL         RESOLVED
  default   172.20.0.0/24    10.20.0.0/24

Override file generated: compose.override.yml
Run `docker compose up` to start services.

上記のように、Docker Composeのプロジェクトディレクトリで gopose up を実行するだけで、既に使用中のポートやネットワークの競合を検出し、安全な代替ポート・サブネットを割り当ててくれます。

元の compose.yml は一切変更せず、compose.override.yml として差分を出力するので安心です。

インストール方法

Go環境があれば一発でインストールできます。

go install github.com/harakeishi/gopose@latest

バイナリを直接ダウンロードすることもできます。

# macOS (arm64)
curl -L https://github.com/harakeishi/gopose/releases/latest/download/gopose_darwin_arm64.tar.gz | tar xz
sudo mv gopose /usr/local/bin/

# Linux (amd64)
curl -L https://github.com/harakeishi/gopose/releases/latest/download/gopose_linux_amd64.tar.gz | tar xz
sudo mv gopose /usr/local/bin/

基本的な使い方

コマンド 説明
gopose up 競合を検出して compose.override.yml を生成
gopose up --dry-run ファイル生成なしでプレビュー
gopose status 現在の競合状態を表示

また、-f オプションでcomposeファイルを指定したり、--port-range でポートの割り当て範囲を指定することもできます。

# カスタムcomposeファイルを指定
gopose up -f custom-compose.yml

# ポートの割り当て範囲を指定
gopose up --port-range 9000-9999

goposeの内部について

goposeの内部でやっていることは大きく分けると下記の流れになります。

  1. compose.yml をパースしてサービスのポートマッピングとネットワーク設定を取得する
  2. システム上で使用中のポートをスキャンする
  3. 定義されたポートと使用中のポートを突き合わせて競合を検出する
  4. Dockerのネットワークサブネットの競合も同様に検出する
  5. 競合があればそれぞれに対して空いているポート・サブネットを割り当てる
  6. 結果を compose.override.yml として出力する

ポイントは、元の compose.yml には一切手を加えないという点です。Docker Composeにはoverrideファイルの仕組みがあり、compose.override.yml が存在すると自動的にマージされます。この仕組みを利用して、非破壊的にポートの上書きを行っています。

ちょっと寄り道(Docker Composeのポート競合について)

複数のDocker Composeプロジェクトを同時に開発していると、ポート競合は避けて通れない問題です。

例えば、プロジェクトAで 3000:3000 を使っていて、プロジェクトBでも 3000:3000 を使おうとすると、docker compose up したときに Bind for 0.0.0.0:3000 failed: port is already allocated というエラーになります。

これまでの対処法は大きく2つでした。

  1. 手動でポートを変えるcompose.yml を編集して空いているポートに変更する。しかし、どのポートが空いているか調べるのが面倒だし、compose.yml を直接変更するのでgitの差分が出てしまう。
  2. 都度プロジェクトを落とす — 使わないプロジェクトを docker compose down してからもう一方を起動する。これだと切り替えのたびにコンテナの再構築が必要になる場合がある。

goposeはこの面倒を自動化します。

さらに、ネットワークのサブネット競合も同様の問題が起きます。Docker Composeはデフォルトでブリッジネットワークを作成しますが、複数プロジェクトでサブネットが被ると通信に問題が出ることがあります。goposeはこれも自動で検出・解決します。

設定ファイル

プロジェクトやホームディレクトリに .gopose.yaml を置くことで、ポートの割り当て範囲や予約ポートを設定できます。

port:
  range:
    start: 8000
    end: 9999
  reserved: [8080, 8443]  # 割り当て対象外にするポート

resolver:
  strategy: "minimal_change"

reserved に指定したポートは、たとえ空いていても割り当て対象から除外されます。開発環境で固定的に使うポートがある場合に便利です。

コマンドを公開してから

goposeは、自分自身が複数のDocker Composeプロジェクトを同時に扱う中で感じていた不便さから生まれたコマンドです。

whrisのときの経験を活かして、今回は最初からいくつかのことに気をつけて開発を進めました。

この経験を通して得た知見

whrisでの反省を活かせた点

経緯

whrisを公開したときに得た知見(whrisの記事)を活かして、goposeでは最初から以下のことを意識しました。

  • ネーミングの確認 — 既存コマンドとの名前被りがないかを事前にチェック
  • テストの整備 — 最初からテストを書きながら開発
  • CIの準備 — 早い段階でGitHub Actionsを設定

何が良かったか

whrisのときは公開後に名前の被りを指摘されたり、テストなしでmainにマージして動かないものをリリースしてしまったりと冷や汗をかきました。goposeではそういった問題を最初から防げたのは大きかったです。

過去の失敗から学んで改善できるのは、こういった個人開発の良いところだと思います。

非破壊的な設計にこだわった点

経緯

ポート競合を解決するツールを作ろうとしたとき、一番シンプルなのは compose.yml を直接書き換える方法でした。

何が問題だったか

しかし、ユーザーの compose.yml を直接変更してしまうと、gitの差分が出たり、意図しない変更が混入するリスクがあります。また、チームで開発している場合、個人の環境差異がcomposeファイルに反映されてしまうのは望ましくありません。

対応

Docker Composeのoverrideファイルの仕組みを活用し、compose.override.yml として出力する設計にしました。これにより、元のファイルは一切変更せず、.gitignore にoverrideファイルを追加しておけばgitの差分も出ません。

この「非破壊的」というのはgoposeの設計上の一番のこだわりポイントです。

おわりに

whrisを公開してから約4年、今回またGoでCLIツールを作りました。前回の経験で得た知見を活かせた部分も多く、個人開発を続けることの意味を改めて感じました。

goposeはまだまだ改善の余地があると思います。スター、Issue、PRお待ちしてます!!