メインコンテンツまでスキップ

第03章:“公開の整理”の3大パターンを知る🧠🔀

この章は「ローカルで複数アプリを共存させるとき、URLをどう切る?」がテーマだよ〜😊 結論から言うと、公開の整理はこの3つに集約されます👇✨

  • ① ポートで分けるlocalhost:3000 / localhost:5173 みたいに)
  • ② パスで分けるlocalhost/app1 / localhost/app2 みたいに)
  • ③ サブドメインで分けるapp1.localhost / api.localhost みたいに)

0) まず“URLの部品”だけ超ざっくり理解しよ🧩✨

URLって「住所」なんだけど、分解するとこう👇

http://app1.localhost:8080/api/users?page=1
└──ホスト名──┘ └ポート┘└─パス─┘ └ クエリ ┘
  • ホスト名:どの“家(アプリの入口)”に行く?🏠
  • ポート:その家の“玄関番号”どれ?🚪(同じ家でも玄関が複数ある感じ)
  • パス:家の中の“部屋”どこ?🧭

この章は、ホスト名 / ポート / パスのどれを使って「アプリAとBを分けるか」って話だよ😺


1) ① ポートで分ける(いちばん簡単😆)🔌

イメージ図🗺️

ブラウザ
├─ http://localhost:3000 → app1
└─ http://localhost:5173 → app2

いいところ👍✨

  • すぐできる!リバプロ不要!🚀
  • アプリ側の設定が少ない(“自分は / で動く”でOK)🙂

つらいところ😵‍💫

  • アプリが増えるほど URLが地獄(3000/3001/3002…)🌀
  • ポート衝突が起きがち(別プロジェクトも3000使ってた等)💥
  • フロントとAPIが別ポートだと CORSが出やすい(後で詳しく)😇

1分ハンズオン:同じTypeScriptアプリを2つ起動して“ポートで分ける”🧪🐳

フォルダ構成📁

chapter03-port/
docker-compose.yml
app/
package.json
tsconfig.json
src/server.ts
Dockerfile

app/src/server.ts(超ミニWebサーバ)🌱

import http from "node:http";

const appName = process.env.APP_NAME ?? "app";
const port = Number(process.env.PORT ?? "3000");

const server = http.createServer((req, res) => {
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end(`Hello from ${appName} 👋\npath=${req.url}\n`);
});

server.listen(port, () => {
console.log(`[${appName}] listening on ${port}`);
});

app/package.json 🧩

{
"name": "chapter03-port-app",
"private": true,
"type": "module",
"scripts": {
"dev": "node --watch dist/server.js",
"build": "tsc -p tsconfig.json"
},
"devDependencies": {
"typescript": "^5.9.0"
}
}

TypeScriptの安定最新版は 5.9 系として案内されてるよ(2026-02-12時点)😊 (typescriptlang.org) さらに今日、TypeScript 6.0 Beta も発表された流れ(追い風🌪️) (Microsoft for Developers)

app/tsconfig.json 🛠️

{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"outDir": "dist",
"strict": true
},
"include": ["src"]
}

app/Dockerfile 🐳

FROM node:24-alpine AS build
WORKDIR /app
COPY package.json tsconfig.json ./
RUN npm i
COPY src ./src
RUN npm run build

FROM node:24-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
ENV PORT=3000
EXPOSE 3000
CMD ["node", "dist/server.js"]

Node は v24 が Active LTS(安定枠)で、v25 が Current(新機能枠)という整理(最終更新 2026-02-09)。 (Node.js)

docker-compose.yml(同じビルドを2サービスで使う)🧠✨

services:
app1:
build: ./app
environment:
APP_NAME: app1
PORT: 3000
ports:
- "3000:3000"

app2:
build: ./app
environment:
APP_NAME: app2
PORT: 3000
ports:
- "5173:3000"

起動🚀

docker compose up --build

ブラウザで👇

  • http://localhost:3000 → app1
  • http://localhost:5173 → app2

✅ これが「ポートで分ける」だよ〜!


2) ② パスで分ける(“入口1個+中で振り分け”🚪➡️🏠)🍰

イメージ図🗺️

ブラウザ
└─ http://localhost/
├─ /app1 → app1
└─ /app2 → app2

これをやるには、だいたい リバースプロキシが必要になるよ(入口係🚥) まだ本格的には後の章だけど、感覚だけ先に持つと超ラク😊

いいところ👍✨

  • URLがスッキリ(入口が1個)✨
  • /api を同一ドメイン配下に置けると CORS減りがち🧹(後でやるよ)

つらいところ😵‍💫(ここ大事⚠️)

  • アプリ側が「自分は /app1 配下で動く」前提になってないと事故りやすい

    • SPAのルーティング
    • 静的ファイルの相対パス
  • リバプロで「/app1 を付けたまま渡す?外して渡す?」問題が出る🤯

例:Caddyだと“パスの先頭を外して渡す”がやりやすい🍞

handle_path は “マッチしたパスを strip してくれる” 仕組みとして用意されてるよ。 (Caddy Web Server)

http://localhost/app1/hello
↓(/app1 を外して)
app1 コンテナには /hello で届く

3) ③ サブドメインで分ける(本番っぽくて強い💪)🏷️

イメージ図🗺️

ブラウザ
├─ http://app1.localhost → app1
├─ http://app2.localhost → app2
└─ http://api.localhost → api

いいところ👍✨

  • 本番の構成に近い(ホスト名で分かれるのは現場あるある)🎯
  • 多くのWebアプリが「自分は / で動く」前提なので、パス方式より事故りにくい🙂
  • CookieやOAuthなど“ドメインっぽい話”をする時に整理しやすい🍪

つらいところ😵‍💫

  • “名前解決(どのIPに向く?)”が絡む(次の章でやるやつ)🧠
  • ローカルHTTPSや証明書の話と相性が強い(後半でやる🔒)

でもね、ここで朗報🎉 .localhost はローカル用途として扱われる流れが強く、*.localhost をローカルへ向ける挙動が一般的に期待できる(仕様・実装の背景あり)。 (RFC エディタ) DockerのTraefikガイドでも nginx.localhost の例で “そのままブラウザで開ける” 前提で説明されてるよ。 (Docker Documentation)


4) 3パターン比較まとめ(迷ったらここを見る😺)📌

パターンURL例難易度向いてるつらいポイント
ポートlocalhost:3000とにかく今すぐ動かすポート増殖・衝突・CORS
パスlocalhost/app1★★★入口1個に統一したい“/app1配下”対応が必要
サブドメインapp1.localhost★★本番っぽくしたい名前解決/HTTPSが絡む

5) ざっくり判断のコツ(設計超入門向け)🧭✨

  • 今日だけ動けばOK:まず ポート方式で勝つ🏆😆
  • 複数アプリが当たり前になるサブドメイン方式が安定しやすい🏷️
  • /api を同一ドメインに寄せたいパス方式が候補(ただしアプリ側が対応必要)🍰

「あとで変えればいいや〜」は、だいたい後で泣くので😂 この章で “自分のチーム(未来の自分)に優しいURL” を選ぶ意識を持つのが勝ちだよ✅✨


6) よくあるミス辞典😇🧯(第3章ver)

  • ポート方式

    • 「3000が既に使われてた」→ まず衝突を疑う💥
  • パス方式

    • 「画面は出るけどリンク遷移で404」→ SPAのベースパス未対応👻
    • 「/app1 を付けたら502」→ リバプロが“どこに流すか”間違い🚑
  • サブドメイン方式

    • 「app1.localhost が開かない」→ 名前解決/ブラウザ/OSの挙動確認(次章で丁寧に)🪟🔍

7) AIに聞くと爆速になる質問テンプレ🤖✨

コピペして、プロジェクト名だけ変えると超便利だよ👇😺

  • ポート方式

    • 「docker composeで同じDockerfileから app1/app2 を起動して、ホスト側のポートを 3000 と 5173 に分けたい。最小構成のcomposeを書いて」
  • パス方式

    • /app1/app2 を入口1つで振り分けたい。Caddyで handle_path を使ったCaddyfile例と、composeへの組み込み例を作って」 (Caddy Web Server)
  • サブドメイン方式

    • app1.localhostapi.localhost をリバプロで振り分けたい。Traefikのラベル例でcomposeを書いて」 (Docker Documentation)

8) ミニ課題(10〜20分)🎒✨

課題A:ポート方式を“3アプリ”に増やす🔌➕

  • app3 を追加して http://localhost:8787 で開けるようにしてみよう😺
  • ヒント:composeでサービス増やして、ports だけ変える!

課題B:あなたの開発スタイルだと、どれを採用する?✍️

次の質問に1行ずつ答えてみてね👇

  • アプリは今後いくつ増えそう?📈
  • OAuthやCookieログイン、扱いそう?🍪
  • “本番っぽさ”どれくらい必要?🎯

この回答が、そのまま次の章(ルーティング/ローカルドメイン)で効いてくるよ〜🧠✨


必要なら、次の返答で「第3章の続きとして」 ✅ パス方式 / サブドメイン方式を **“最小のCaddy or Traefik構成で実際に動かす”**ところまで、コピペ教材として仕上げちゃうよ🚀😺