Docker Composeで一発再現:Nginxを入口にしてGo/Nodeを動かす“最小だけど実戦的”な構成

はじめに

ローカル開発や検証環境で「Nginxを入口にして、バックエンド(GoやNode)を裏で動かす」構成を、毎回手作業で用意するのは地味に面倒です。しかも、環境差(OS、パッケージ、ポート競合、設定ファイルの置き場所)で再現性が崩れると、検証そのものが目的化してしまいます。

そこで役に立つのが Docker Compose です。Nginxとアプリ(Go/Node)をそれぞれコンテナとして分離し、ネットワーク越しに接続するだけで、「入口 → ルーティング → アプリ」の流れをいつでも同じ形で再現できます。これができると、個人開発でもチーム開発でも次のメリットが大きいです。

  • 誰が実行しても同じ挙動(設定や依存がコンテナに閉じる)
  • Nginx設定を触ってすぐ確認(ホストの環境を汚さない)
  • Go/Nodeの差し替えが簡単(裏側を入れ替えて検証できる)
  • “入口の設計”を学びやすい(実務の構成に近い)

この記事では、Compose一発で起動できるように「静的配信 + APIプロキシ」を最小構成で作り、さらに「Go版」「Node版」を切り替えられる形にします。入門記事の延長としても、実務の雰囲気を掴む入口としても使える構成です。


座学

1) “Nginxを入口にする”構成の考え方

アプリを直接外部公開すると、静的配信(HTML/CSS/JS)とAPI(JSON)を同一プロセスで捌くことになりがちです。小規模ならそれでも動きますが、現場では次の理由で入口を分けることが多いです。

  • 静的ファイルは高速に配る役(Nginxが得意)
  • APIはアプリが集中して処理する役(Go/Nodeなど)
  • 設定で振り分けたい/api はアプリ、/ は静的)
  • 将来の拡張に強い(APIだけスケール、入口でTLSなど)

今回の構成はこの考え方をそのままDocker上で再現します。


2) Docker Composeで重要なのは“名前解決”と“責務分離”

Composeでは、同じ docker compose プロジェクト内のサービスは サービス名がDNS名 になります。たとえば api サービスを作ると、Nginxコンテナから http://api:8080 へ到達できます。

この“名前でつながる”性質が、Nginxの proxy_pass と非常に相性が良いです。ホストのIPに依存しないので、どのPCでも同じ設定で動きます。

また、責務分離の基本はこうです。

  • nginx:外部公開(80番)、静的配信、/api の中継
  • api(Go/Node):APIレスポンスを返すだけ

これだけでも、入口とアプリの境界がくっきりして学びやすくなります。


3) “最小だけど実戦的”にするための小技

入門用でも、次の要素を最初から入れておくと後で効きます。

  • 静的配信に try_files(SPAのページ更新で404を防ぐ)
  • プロキシに最低限のヘッダ付与(ログやURL生成に効く)
  • ホットリロード的な体験(設定やソースをマウントする)

今回は、重くしない範囲でこの3つを入れます。


ハンズオン

ここからは “コピペでそのまま動く” を目標に、フォルダ構成→ファイル→起動→動作確認まで一気に作ります。Go/Nodeの両方を用意して、Composeのプロファイルで切り替える形にします。

1) ディレクトリ構成

プロジェクト直下を次のようにします。

compose-nginx-app/
docker-compose.yml
nginx/
nginx.conf
web/
index.html
go-api/
Dockerfile
main.go
node-api/
Dockerfile
package.json
server.js

2) 静的ファイル(web/index.html)

web/index.html を作ります。最低限、画面に「静的配信できた」とAPI呼び出し結果を表示する要素を置くと検証が楽です。

例:

  • ページロード時に /api/health を叩くボタン
  • 結果表示エリア

(HTMLは簡素でOK。後述のAPIが返すJSONが見えれば成功です。)


3) Nginx設定(nginx/nginx.conf)

ポイントは2つです。

  • /web/ を静的配信
  • /api/api サービスにプロキシ

nginx/nginx.conf(例)

events {}http {
server {
listen 80; # 静的配信用
root /usr/share/nginx/html;
index index.html; location / {
# SPA想定:存在しないパスはindex.htmlへ
try_files $uri $uri/ /index.html;
} # APIプロキシ
location /api/ {
proxy_pass http://api:8080/; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}

proxy_pass http://api:8080/;apiComposeのサービス名 です。ここが“再現性の核”になります。


4) Go API(go-api/main.go)

/health を返すだけの最小APIにします。/api/health に中継されるので、Go側は /health で待ちます。

package mainimport (
"encoding/json"
"log"
"net/http"
"os"
)func main() {
mux := http.NewServeMux() mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"ok": true,
"service": "go-api",
"message": "Hello from Go",
"host": os.Getenv("HOSTNAME"),
})
}) addr := ":8080"
log.Println("listening on", addr)
log.Fatal(http.ListenAndServe(addr, mux))
}

Go用Dockerfile(go-api/Dockerfile):

FROM golang:1.22-alpine AS build
WORKDIR /app
COPY main.go .
RUN go build -o server main.goFROM alpine:3.20
WORKDIR /app
COPY --from=build /app/server /app/server
EXPOSE 8080
CMD ["/app/server"]

5) Node API(node-api/server.js)

Nodeも同じく /health を返すだけにします。

node-api/package.json

{
"name": "node-api",
"version": "1.0.0",
"type": "module",
"dependencies": {
"express": "^4.19.2"
}
}

node-api/server.js

import express from "express";const app = express();app.get("/health", (req, res) => {
res.json({
ok: true,
service: "node-api",
message: "Hello from Node",
host: process.env.HOSTNAME
});
});app.listen(8080, () => {
console.log("listening on :8080");
});

Node用Dockerfile(node-api/Dockerfile):

FROM node:20-alpine
WORKDIR /app
COPY package.json ./
RUN npm install --omit=dev
COPY server.js ./
EXPOSE 8080
CMD ["node", "server.js"]

6) Docker Compose(docker-compose.yml)

ここが“一発再現”の本体です。

  • nginx は 80番を公開
  • api は Go または Node をプロファイルで切り替え
  • nginx から api:8080 で到達可能
services:
nginx:
image: nginx:1.27-alpine
ports:
- "8088:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./web:/usr/share/nginx/html:ro
depends_on:
- api # Go版
api:
profiles: ["go"]
build:
context: ./go-api
expose:
- "8080" # Node版(同名サービスにするため、別ファイルに分ける方法もあるが、
# ここでは "override" ではなくプロファイル切替を採用する)
api-node:
profiles: ["node"]
build:
context: ./node-api
expose:
- "8080"

ただし、このままだと nginx が参照するのは api 固定です。そこで プロファイルに応じてNginxが見る宛先を揃えるために、構成を少し整理します。おすすめは次のどちらかです。

A案(最も分かりやすい):Composeファイルを2つに分割

  • docker-compose.yml(共通:nginx + web)
  • docker-compose.go.yml(api=go)
  • docker-compose.node.yml(api=node)

B案(1ファイルで済ませる):サービス名を合わせるためにextends/overrideを使う
入門記事としては A案 の方が読みやすく事故りません。ここではA案でいきます。

共通(docker-compose.yml)

services:
nginx:
image: nginx:1.27-alpine
ports:
- "8088:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./web:/usr/share/nginx/html:ro
depends_on:
- api

Go用(docker-compose.go.yml)

services:
api:
build:
context: ./go-api
expose:
- "8080"

Node用(docker-compose.node.yml)

services:
api:
build:
context: ./node-api
expose:
- "8080"

これで Nginxは常に api を見ればよい になり、切り替えが綺麗に決まります。


7) 起動コマンド(Go版 / Node版)

Go版

docker compose -f docker-compose.yml -f docker-compose.go.yml up --build

Node版

docker compose -f docker-compose.yml -f docker-compose.node.yml up --build

アクセスは共通で:

  • 静的:http://localhost:8088/
  • API:http://localhost:8088/api/health

8) 動作確認のポイント

うまくいくと、/api/health のレスポンスが以下のように変わります。

  • Go版:"service": "go-api"
  • Node版:"service": "node-api"

つまり「入口(Nginx)は同じ、裏側(実装言語)だけ差し替え可能」が実現できています。これがCompose再現の強さです。


9) よくある詰まりどころ

  • 502 Bad Gateway
    → アプリが起動してない / proxy_pass の宛先が違う / APIが8080で待ってない
  • 静的が404
    web/ のマウント先が違う / root が違う
  • 設定を変えたのに反映されない
    nginx.conf をマウントしているか確認、必要なら docker compose restart nginx

この3つは、入門段階でも最短で切り分けられるようにしておくと安心です。


まとめ

Docker Composeを使うと、Nginx + Go/Nodeの「入口→中継→アプリ」を、環境差なしで一発再現できます。今回の構成は最小ながらも、現場の考え方に近い要素をちゃんと含んでいます。

  • Nginxは 静的配信APIプロキシ を担当
  • Go/Nodeは APIの実装だけ を担当
  • Composeは サービス名で名前解決 し、設定の再現性を担保
  • 2つのComposeファイルを重ねる方式で 実装の差し替え が簡単

このテンプレを持っておくと、Nginx設定の学習、APIの置き換え、検証環境の共有が一気に楽になります。


投稿日

カテゴリー:

投稿者:

タグ:

コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です