こんにちは、株式会社アルファドライブ @takano-hi です。AlphaDrive で Web フロントエンドを中心に設計・実装などを担当しています。 今回は、弊社のローカル開発環境で localhost:xxxx といったポート番号が増え続ける問題を解決した話をまとめてみました。
背景
弊社では Next.js を利用したフロントエンドアプリケーションを複数開発しており、今後も増える見込みがあります。 通常 Next.js アプリケーションは、ローカル開発環境の起動時に localhost:3000 にホストされます。 これが複数になると、2つ目以降はそのままではポート 3000 番が占有されていることによるエラーが発生するため、別のポート番号を指定する等の方法で回避することが多いと思います。 さらに、それぞれのプロジェクトごとに Storybook を用意したりすると、必要なポートはさらに増えていきます。 弊社では下記のような状況になっていました。
サービス名 | URL |
---|---|
サービスA | http://localhost:3000 |
サービスB | http://localhost:3001 |
サービスC | http://localhost:3002 |
サービスAのStorybook | http://localhost:6006 |
サービスBのStorybook | http://localhost:6007 |
サービスCのStorybook | http://localhost:6008 |
認証サービス | http://localhost:18080 |
開発用メールサーバー | http://localhost:1080 |
こうなってくると、どのサービスのポート番号が何番だったか覚えていられません。 そこで、前職で採用されていた仕組みを参考に、各サービスの前段に nginx を挟むことでポート番号ではなくサービス名で名前解決できるようにする仕組みを導入しました。 下記にてその手順を説明していきます。
1. docker-compose.yml にプロキシ用サービスを追加
弊社ではレポジトリが monorepo 構成で、 docker-compose を利用して開発環境を管理しているので、まずは docker-compose.yml にプロキシ用のサービスを追加していきます。
version: "3.8" services: service-a: build: ./service-a command: npm run dev service-b: build: ./service-b command: npm run dev service-c: build: ./service-c command: npm run dev # ここから下を追加 dev-proxy: build: ./dev-proxy ports: - 80:80 depends_on: - service-a - service-b - service-c
※ 上記の docker-compose.yml は分かりやすさのため実際より簡略化してあります。
※ service-a, service-b, service-c それぞれのコンテナの中では、 npm run dev
の中で turborepo を利用して、アプリケーション本体と Storybook が同時に起動するような構成になっています。
2. プロキシ用 nginx を設定する
次にプロキシ用 nginx の設定ファイルを書いていきます。
# dev-proxy/nginx.conf user nginx; worker_processes auto; error_log /var/log/nginx/error.log debug; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '[$time_local] $host "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; server { listen 80; server_name service-a.localhost; # ホスト名が http://service-a.localhost であれば service-a コンテナの3000番にプロキシする location / { proxy_pass http://service-a:3000; # docker-compose で管理するコンテナ群はサービス名をホスト名として名前解決できる proxy_set_header Host $host; } # Next.js 12 以降で HMR のために WebSocket 通信を利用するので、WebSocket をプロキシするための設定を加える location = /_next/webpack-hmr { proxy_pass http://service-a:3000/_next/webpack-hmr; include /etc/nginx/conf.d/webpack-hmr.conf; } } server { listen 80; server_name service-a-storybook.localhost; # ホスト名が http://service-a-storybook.localhost であれば service-a コンテナの 6006 番にプロキシする location / { proxy_pass http://service-a:6006; proxy_set_header Host $host; } # Storybook の HMR 向け WebSocket 通信のプロキシ設定 location = /storybook-server-channel { proxy_pass http://frontends:6006/storybook-server-channel; include /etc/nginx/conf.d/webpack-hmr.conf; } } # 以上の server ブロックを各サービス分記述する }
HMR のための WebSocket 通信向け設定は全て同じなので、別ファイルに切り出して使い回しています。
# dev-proxy/conf.d/webpack-hmr.conf proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade";
ここまで書けたら、これらが nginx の挙動に反映されるように Dockerfile を書いていきます。
FROM nginx:1.25.2-bookworm COPY ./nginx.conf /etc/nginx/nginx.conf COPY ./conf.d/ /etc/nginx/conf.d/ EXPOSE 80
これで設定ファイルが nginx に反映されます。 試しに dev-proxy をビルドして動作を確認してみましょう。
$ docker compose build dev-proxy
$ docker compose up -d
$ open http://service-a.localhost
するとポート番号なしで、サブドメインにサービス名を足すことでアプリケーションにアクセスすることができるようになりました。
余談
今回の記事では <service-name>.localhost
というホスト名でご説明しましたが、実際には <repository-name>-<service-name>.localhost
のように、レポジトリ名をホスト名に含めるのがオススメです。
職場の環境・体制によっては、複数レポジトリを同時に開発することもあると思いますが、この時、レポジトリの間でサービス名が重複してしまった場合にも衝突を回避することができます。
3. ポータルページを用意する
ここまでで、ポート番号を覚えていなくてもサービス名さえ覚えていれば即座に開発環境にアクセスできる状態になったので当初の問題は解決しました。ですが、さらに踏み込んでサービス名すら覚えていなかった場合を考慮して、ポータルページを用意してみましょう。 今回は「http://localhost にアクセスすれば、各サービスの開発環境へのリンク一覧が表示される」といった仕様で考えてみます。
まずはポータルページとなる静的 HTML ファイルを用意します。
<!-- dev-proxy/services.html --> <html lang="ja"> <head> <title>local development portal</title> <script src="https://cdn.tailwindcss.com"></script> </head> <body> <div class="p-5"> <h1 class="text-3xl font-bold mb-5">local development portal</h1> <ul id="service-list" class="list-disc list-inside"></ul> </div> <script> var menuItems = [ { url: "http://service-a.localhost", label: "service-a" }, { url: "http://service-a-storybook.localhost", label: "service-a-storybook" }, { url: "http://service-b.localhost", label: "service-b" }, { url: "http://service-b-storybook.localhost", label: "service-b-storybook" }, { url: "http://service-c.localhost", label: "service-c" }, { url: "http://service-c-storybook.localhost", label: "service-c-storybook" }, { url: "http://auth.localhost", label: "auth" }, { url: "http://maildev.localhost", label: "maildev" }, ]; var serviceList = document.getElementById("service-list"); menuItems.forEach((menuItem) => { var listItem = document.createElement("li"); listItem.classList.add("list-disc", "list-inside"); var link = document.createElement("a"); link.classList.add("text-blue-600", "hover:underline"); link.setAttribute("href", menuItem.url); link.textContent = menuItem.label; listItem.appendChild(link); serviceList.appendChild(listItem); }); </script> </body> </html>
作成したファイルをブラウザで開くと、次のようなシンプルなリストが表示されます。
これを http://localhost で表示されるように nginx.conf と Dockerfile に追記していきます。
# dev-proxy/Dockerfile FROM nginx:1.25.2-bookworm WORKDIR /app COPY ./nginx.conf /etc/nginx/nginx.conf COPY ./conf.d/ /etc/nginx/conf.d/ # 下記を追記: service.html をコンテナ内の /etc/nginx/html/services.html に配置する COPY ./services.html /etc/nginx/html/services.html EXPOSE 80
# dev-proxy/nginx.conf user nginx; worker_processes auto; error_log /var/log/nginx/error.log debug; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '[$time_local] $host "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; ... # http ブロックの末尾に下記を追記 server { listen 80 default_server; server_name _; location = / { index services.html; } } }
ここまで完了したら、dev-proxy をビルドし直すと挙動に反映されます。
$ docker compose down
$ docker compose build dev-proxy
$ docker compose up -d
※ ただし、このポータルページは localhost:80 を占有してしまうため、複数レポジトリで開発する際にはポート番号を変更するなどの工夫が必要です。
まとめ
プロダクトの規模が大きくなるに従ってアプリケーションの数が増え、ポート番号が増え続けてしまって覚えられないという問題の対処法について解説しました。 より発展的には、ポータルページで各サービスの health check の結果を元に起動中のサービスのみ表示するようにするなど、工夫の方向性は色々ありそうです。 この調子で開発環境を快適にして、日々の開発を爆速にしていきましょう!