Dev Study
← アプリケーションの土台
レッスン 11 / 11·約 9 分·第7期 アプリケーションの土台

開発サーバと本番 — localhost:3000 と https://example.com の間

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口今ここ
  6. アプリ処理今ここ
  7. DB保存

本コース最終回。違いは段の数であり、動かなくなるのは段が増えた場所だ、という見方で締めます。

まず結論

開発サーバは「1プロセス・平文・自分の機械宛て・ソースを都度変換」という最小構成です。本番はその前にTLSを終端する段や取り次ぎ役が並び、実体も複数になり、ビルド済みのコードが配られます。

違いは機能ではなく段の数です。「開発では動くのに本番で動かない」の多くは増えた段で起きています。まず数えるのは段であって、アプリのコードではありません。

? 読む前に予測

ブラウザ側のコードに `fetch("http://localhost:8080/api")` と書き、本番にデプロイしました。利用者のブラウザは何に接続しに行きますか。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

違うのは機能ではなく、段の数

最後に、ここまで見てきたプロセスが本番ではどう置かれるのかを見ます。

開発中の構成は、たいてい次の4点で最小です。

  1. 1プロセス — アプリの実体は1つだけ
  2. 平文 — http:// のまま。暗号を解く段がない
  3. 自分の機械宛て — 宛先も送り主も同じ機械の中
  4. ソースを都度変換 — 書いたファイルを要求のたびに変換して返す

本番はこの手前に段が積まれます。TLSを終端する段、取り次ぎ役、そして実体が複数。配られるコードもビルド済みです。

重要なのは、アプリのプロセスがやること自体は増えていない点です。第27週『リクエストは何段の手を経るのか — 全体地図』の地図のいちばん奥の段が、本コースの中身でした。

段の数の差(アプリの中身は同じ)
開発: ブラウザ自分の機械
本番: ブラウザ利用者の機械
接続する
開発サーバ1プロセス・平文
TLS終端 / ロードバランサここで暗号を解く
本番だけ、さらに続く
取り次ぎ役(Nginx)どの実体へ渡すか決める
取り次ぐ
アプリ ×Nlisten・パース・文字列を返す
問い合わせる
DB
  • ・左の列(開発)は1段で終わり、右の列(本番)は段が続く
  • ・差は手前の段数
  • ・どの段も自分で応答を作って返せる(第27週)
具体実物

`localhost` は「そのコードを実行している機械」

段の差が分かりやすく出るのが、接続先の書き方です。

localhost は RFC 6761 §6.3 が予約した名前で、名前解決 API はこれを外へ問い合わせずループバックのアドレスを返すことが求められています(SHOULD であって MUST ではありません)。導線『名前がアドレスになる — localhost は DNS に行かない』のとおりです。

ここで効くのは「誰のループバックか」です。localhost は書いた人ではなく、そのコードを実行している機械を指します。開発中は両者が同じですが、本番では利用者の機械になります。

bind の側も同じです。実機で2つのサーバを、片方は 127.0.0.1、もう片方は 0.0.0.0 に bind しました。

$ lsof -nP -iTCP -sTCP:LISTEN | grep -E '3921|3922'(macOS、Node v24.21.0)コマンド出力
node 17295 kazu 12u IPv4 0xaa9386e6cfc7d739 0t0 TCP 127.0.0.1:3921 (LISTEN)
node 17296 kazu 12u IPv4 0x18c10139ee7f1d40 0t0 TCP *:3922 (LISTEN)

NAME 列の 127.0.0.1:3921 と *:3922 が bind 先です。差は同じ機械のLAN側アドレス(192.168.1.3)から叩くと現れます。

$ curl --max-time 3 http://192.168.1.3:3921/ と :3922/(同じ機械から)コマンド出力
$ curl -sv --max-time 3 http://192.168.1.3:3921/ 2>&1 | grep -iE 'refused|Failed'
* connect to 192.168.1.3 port 3921 from 192.168.1.3 port 53175 failed: Connection refused
* Failed to connect to 192.168.1.3 port 3921 after 0 ms: Couldn't connect to server
 
$ curl -s --max-time 3 http://192.168.1.3:3922/
ok from 0.0.0.0:3922

同じ機械の中ですら、127.0.0.1 に bind したサーバは別アドレス宛ての接続を受けません。どのアドレスに bind したかで決まります。

✓ ここまでの確認

`lsof` の listen 先が `127.0.0.1:3921` と出ていました。同じ機械のLAN側アドレス(192.168.1.3)から接続するとどうなりますか。

抽象原理

開発サーバが肩代わりしている段は、本番には無い

一般化します。開発サーバは本番なら別の段が担当する仕事を肩代わりしていることがあります。その段が消える本番で、初めて表に出る差があります。

代表が取り次ぎです。Vite の docs は server.proxy をこう説明しています。

「dev server の」規則だと書いてあるとおり、これは開発サーバの設定で、本番には持ち越されません。

この設定があるとき、ブラウザは開発サーバ自身へ要求を出し、取り次ぎは向こう側で起きます。宛先は1つのオリジンです。本番で相対パス /api のままなら同じオリジンに届き、取り次ぐ段(Nginx など)が無ければ 404 などになります。API を別オリジンに置いて直接呼ぶと、ブラウザ側のアクセス制御がそこで初めて効きます。開発で出なかったのは、別オリジンへの要求になっていなかったからです。

具体実物

ビルドの時点で焼き付くもの

もう1つ挙動が変わるのが、ビルド時に値が固定される仕組みです。Vite の docs はこう書いています。

開発時は「グローバル変数として定義」、ビルド時は「静的に置換」。本コースの『ビルドとバンドル — 書いたコードと届くコードが違う理由』で見たずれです。

$ VITE_API_URL=http://localhost:8080 npx vite build の出力を検査(vite 8.3.0、macOS)コマンド出力
dist/assets/index-W_jZTbjh.js 0.82 kB │ gzip: 0.47 kB
 
$ grep -o 'http://localhost:8080' dist/assets/*.js
http://localhost:8080
$ grep -c 'import.meta.env' dist/assets/*.js
0

import.meta.env の参照は残らず、代わりに開発時の接続先が文字列で入っています。値が違えばファイル名も変わります。

同じソースを env の値だけ変えて3回ビルドした結果(実機)表
回 VITE_API_URL 出力ファイル名
1 http://localhost:8080 index-W_jZTbjh.js
2 https://api.example.com index-0j6iANU8.js
3 http://localhost:8080 index-W_jZTbjh.js

3回目で1回目と同じ名前に戻っています。名前は内容から決まるので、env の値1つの違いが名前に出ます。

そして値はビルド時に固定されます。予測の fetch("http://localhost:8080/api") は、この形で本番に紛れ込みます。

✓ ここまでの確認

開発サーバの `server.proxy` で `/api` を取り次いでいました。本番では設定が使われないため、API を別オリジン(例: `https://api.example.com`)に置いて直接呼ぶ形にしました。何が変わりますか。

⇄ 2つを見比べる
具体例 A

開発サーバのプロキシ — 宛先が1つに見えていた

開発サーバが `/api` を取り次ぐ間、ブラウザの宛先は開発サーバ1つです。取り次ぎは向こう側で起きるので境目が見えません。本番で API を別オリジンに置くと、そこへ直接要求を出します。

ブラウザが接続する先(本番は API が別オリジン)表
環境 ブラウザの接続先 取り次ぎの段
開発 開発サーバ(1つ) 開発サーバの中
本番 別のオリジンへ直接 無い
具体例 B

TLS終端 — 暗号を解く場所が分かれた(第33週)

開発では `http://` のまま1プロセスに届くので、暗号を解く段がありません。本番では前段で終端され、そこから先は平文です。終端した場所がエンドポイントです。

アプリのプロセスが受け取る形表
環境 ブラウザから見た形 アプリの入口
開発 http:// 平文
本番 https:// 終端の位置で決まる

開発サーバのプロキシと、TLS終端。「本番で初めて差が出る」理由として共通する構造は何でしょうか。

開発では1つだった段が、本番では何段に分かれているか数えてみてください。

この章のまとめ

  • 開発は「1プロセス・平文・自分の機械宛て・ソースを都度変換」、本番はその手前に段が積まれる。アプリのプロセスがやること自体は変わらない。
  • localhost は「そのコードを実行している機械」を指す。ブラウザ側に書けば利用者の機械になる。
  • bind 先が 127.0.0.1 か 0.0.0.0 かで受ける接続が変わる。同じ機械の別アドレスからでも拒否される(実機で確認)。
  • 開発サーバは本番に無い段(server.proxy など)を肩代わりすることがある。開発で出なかったのは、越える境目が無かったから。
  • ビルド時に静的置換される値は固定される。本番で環境変数を変えても配られたファイルは変わらない。
  • 開発で動いて本番で動かないとき、まず数えるのは段。どの段まで届き、どの段が応答を作ったか(第27週)。

この先の学びにどう繋がるか

手前の段は第3期です。取り次ぎ役の設定は第32週『Nginx設定を書く — proxy_pass とスラッシュ』、暗号を解く位置は第33週『TLS終端 — どこで暗号を解くのか』、なぜ取り次ぐのかは第28週『リバースプロキシ — なぜ自分で返さず取り次ぐのか』。

問題演習なら サーバーのしくみ 群の サーバーのしくみ V(リバースプロキシとアプリ連携)、ブラウザのアクセス制御 群の ブラウザのアクセス制御 II(CORS) です。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

環境:

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 3正解: 0

「開発では動くのに本番で動かない」という報告を受けたとき、最初に見るべきものはどれですか。

用語集(4語)
bind
ソケットに待ち受けるアドレスとポートを結び付けること。`127.0.0.1` ならループバック宛てだけ、`0.0.0.0` なら IPv4 の全アドレス宛てを受ける。
ループバックアドレス
自分自身を指すアドレス(IPv4 は 127.0.0.1、IPv6 は ::1)。W3C Secure Contexts は `127.0.0.0/8` と `::1/128` を potentially trustworthy として扱う。
開発サーバ (dev server)
開発中に起動する1プロセスのサーバ。取り次ぎ規則(`server.proxy` など)を持てる。本番の配信経路とは別物。
ビルド時の静的置換
ソース中の参照を、ビルド時点で値そのものに書き換えること。Vite の `import.meta.env.VITE_*` がこれにあたる。