Goマイクロサービスをコンテナで動かす理由
main.go をビルドして単一バイナリで動かす。その単一バイナリをDocker イメージにパッケージ化し、複数の独立したコンテナで複数のマイクロサービスを管理する。これがゴールです。
静的リンクされるため、Go バイナリはランタイム環境なしで動く。Docker イメージは小さく、本番環境と開発環境の差異がなくなる。
Dockerで管理する利点
- 依存関係の統一: Dockerfile に明記した環境が本番と同じ
- スケール管理: ポート・ボリューム・ネットワークを宣言的に定義
- 開発・テスト効率化: Docker Compose で立ち上げと破棄を自動化
本記事の到達点
APIサーバー1つ + ワーカーサービス1つの2コンテナシステムを Docker Compose で起動。サービス間の HTTP 通信を実装します。
Go のマルチステージビルドで軽量イメージを作る
Docker の「What will you learn」セクション にもある マルチステージビルドは、Goバイナリの サイズ削減に必須です。
ビルドステージと実行ステージを分ける。ビルドステージは Go のコンパイラと全ソースコードを含めますが、実行ステージはバイナリだけをコピー。イメージサイズが 800MB → 20MB 程度に圧縮できます。
基本的な Dockerfile(multi-stage)
# ビルドステージ
FROM golang:1.23 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
# 実行ステージ
FROM alpine:3.20
WORKDIR /root/
COPY --from=builder /app/main .
CMD ["./main"]
重要なポイント:
CGO_ENABLED=0: C言語ライブラリへの依存をなくす(alpine で動作保証)-installsuffix cgo: C 関連のシンボルを除去--from=builder: 前のステージからバイナリだけを抽出
このビルドを実行すると、イメージは20-30MBに納まります。
ビルドと実行の流れ
# ローカルビルド
$ docker build -t api-server:1.0 .
# 検証: イメージサイズ確認
$ docker images api-server:1.0
api-server 1.0 xxxxx 25MB
# 実行テスト
$ docker run -p 8080:8080 api-server:1.0
Docker Compose で複数サービスを連携させる
2つのマイクロサービスを定義します。
- api-server:
localhost:8080でリッスン - worker: api-server からのリクエストを受け取り処理
docker-compose.yml で両者をネットワークで接続。サービス間通信はサービス名をホスト名として使えます。
docker-compose.yml の例
version: '3.8'
services:
api-server:
build: ./api-server
container_name: api-server
ports:
- "8080:8080"
environment:
- WORKER_URL=http://worker:9090
networks:
- microservice-net
depends_on:
- worker
worker:
build: ./worker
container_name: worker
ports:
- "9090:9090"
networks:
- microservice-net
networks:
microservice-net:
driver: bridge
重要な指定:
| 項目 | 役割 |
|---|---|
depends_on |
起動順序を指定。worker が先に起動 |
WORKER_URL |
環境変数で worker のアドレスを api-server に通知 |
networks |
同じネットワーク上のサービス同士が通信可能 |
API サーバーの実装:サービス間通信
Go の net/http パッケージの ListenAndServe と Handler パターンを使います。
api-server/main.go
package main
import (
"io"
"log"
"net/http"
"os"
)
var workerURL string
func init() {
workerURL = os.Getenv("WORKER_URL")
if workerURL == "" {
workerURL = "http://localhost:9090"
}
}
func handleProcess(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "POST only", http.StatusMethodNotAllowed)
return
}
// worker へリクエスト送信
resp, err := http.Post(
workerURL+"/do",
"application/json",
r.Body,
)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Printf("worker request error: %v", err)
return
}
defer resp.Body.Close()
// worker からのレスポンスをクライアントへ返す
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
}
func main() {
http.HandleFunc("/process", handleProcess)
log.Println("api-server listening on :8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
ポイント:
WORKER_URLを環境変数から読み込む(Docker Compose でhttp://worker:9090が渡される)http.Post()で worker にリクエスト転送- ステータスコードとレスポンスボディをクライアントへ返す
実行結果
$ curl -X POST http://localhost:8080/process \
-H 'Content-Type: application/json' \
-d '{"data": "test"}'
{"status": "done", "result": "processed"}
ワーカーサービスの実装
API サーバーから POST リクエストを受け取り、処理を実行。レスポンスを返します。
worker/main.go
package main
import (
"encoding/json"
"log"
"net/http"
"time"
)
type Request struct {
Data string `json:"data"`
}
type Response struct {
Status string `json:"status"`
Result string `json:"result"`
}
func handleDo(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "POST only", http.StatusMethodNotAllowed)
return
}
var req Request
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// 処理(シミュレーション)
time.Sleep(100 * time.Millisecond)
resp := Response{
Status: "done",
Result: "processed: " + req.Data,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
func main() {
http.HandleFunc("/do", handleDo)
log.Println("worker listening on :9090")
if err := http.ListenAndServe(":9090", nil); err != nil {
log.Fatal(err)
}
}
Docker Compose で起動・停止
起動
$ docker compose up -d
Creating api-server ... done
Creating worker ... done
ログ確認
$ docker compose logs -f
api-server | api-server listening on :8080
worker | worker listening on :9090
通信テスト
$ curl -X POST http://localhost:8080/process \
-H 'Content-Type: application/json' \
-d '{"data": "hello"}'
{"status":"done","result":"processed: hello"}
api-server がリクエストを受け、worker に転送。worker が処理を返し、api-server がクライアントに返す。すべてがコンテナ内のネットワークを通じて動作します。
停止と削除
$ docker compose down
ネットワーク設定とサービス検出
Docker Compose の内部ネットワーク(bridge)では、各サービスはサービス名をホスト名として解決できます。
| 仕組み | 説明 |
|---|---|
| Service Discovery | Docker の組み込み DNS が http://worker:9090 を自動解決 |
| Network Isolation | 同じネットワーク上のコンテナ同士のみ通信可能。外部からは接続不可 |
| Port Mapping | ホスト側のポートと コンテナ側のポートを分離 |
api-server と worker は同一ネットワーク上にあるため、環境変数 WORKER_URL=http://worker:9090 で直接通信できます。
本番環境への展開を考える
Docker Compose はローカル開発とテスト環境向け。本番環境では Kubernetes や Docker Swarm で運用されるのが一般的。
ただし、本記事で作ったマルチステージビルド + コンテナネットワークの基本は変わりません。イメージを Docker Hub や ECR にプッシュ、本番環境で pull して起動するだけです。
まとめ
- マルチステージビルドで Go バイナリを 20-30MB に圧縮。イメージサイズはコンテナ管理の第一歩
- Docker Compose で複数サービスの起動順序とネットワークを宣言。
depends_onと環境変数で依存関係を管理 - Go の
net/httpHandler パターンでサービス間通信。HTTP クライアント・サーバーの基本に変わりなし - Docker の bridge ネットワークが Service Discovery を提供。サービス名で直接通信可能
- 開発環境と本番環境で同じイメージを使うため、差異による不具合が減る

