1-7. Level 1 総復習 - Go アプリをコンテナ化する
所要時間: 45-60分(実践メイン) ゴール: Level 1 で学んだことを全部使って、自作 Go アプリをコンテナ化・ビルド・起動・停止・再起動できる コミット内容: 完成した
~/learn/docker/day07/を Git で管理 → 後でリポジトリに push する素材
このレッスンのゴール
- 自作 Go アプリを Dockerfile からビルドし、ローカルで起動・停止できる
- マルチステージビルドで本番イメージを最小化する判断ができる
- 環境変数で挙動を切り替える 12 Factor 的設計を体感する
- 永続化、ヘルスチェック、リスタートを 1 セッションで通せる
- Level 1 の全要素(1-1〜1-6)を地続きに使える
なぜ学ぶか(実務悩みベース)
- 知識は単発で覚えるより「1個作る」と全部繋がる
- 「コンテナ化してください」と言われた時に30分で雛形が書ける状態にしたい
- Compose に進む前に、まず1コンテナで完璧に動く経験を積む
前章とのつながり
1-1 の世界観 →1-2 の実行 →1-3 の解剖 →1-4 の作成 →1-5 の最適化と配布 →1-6 の永続化、すべてを1つのアプリに統合する回。
大前提: ここまでで身についたこと
Level 1(1-1 〜 1-6)で扱ったテーマを並べる:
| Day | テーマ | キーワード |
|---|---|---|
| 1-1 | コンテナとは何か | namespaces / cgroups / VMとの違い / OCI |
| 1-2 | docker run | -d / -p / -v / —rm / -e / -it / logs / exec |
| 1-3 | イメージとレイヤー | Copy-on-Write / レジストリ / タグ |
| 1-4 | Dockerfile | FROM / RUN / COPY / CMD vs ENTRYPOINT / ARG vs ENV |
| 1-5 | ビルドと配布 | ビルドコンテキスト / マルチステージ / push |
| 1-6 | ボリューム | bind mount / named volume / tmpfs / 権限問題 |
このレッスンは 「学んだことを全部使って何か1個動かす」 回。読み物ではなく、手を動かす実践。
なぜこの順で学んだか
振り返ると流れがある:
- 概念(1-1): 何を扱うのか「世界観」を先に
- 動かす(1-2): コマンドを叩いて感覚を掴む
- 解剖(1-3): 動かしたものの中身を覗く
- 作る(1-4): 自分でイメージを定義する
- 配る(1-5): 他のマシンで動かす
- 永続化(1-6): データを守る
- 統合(1-7): 全部使って何か作る ←今ここ
「使う → 解剖 → 作る」 の順は、新しい技術を学ぶ時の鉄板パターン。最初から「Dockerfile を完璧に書く」を目指すと挫折する。動かして感覚を掴むのが先。
実践課題: Hello World HTTP サーバーをコンテナ化する
0. 作業ディレクトリ
mkdir -p ~/log ~/learn/docker/day07
cd ~/learn/docker/day07
script ~/log/docker_day07.log1. Go アプリを書く
cat > main.go <<'EOF'
package main
import (
"fmt"
"log"
"net/http"
"os"
"time"
)
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
name := os.Getenv("APP_NAME")
if name == "" {
name = "hello-go"
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s from %s", r.Method, r.URL.Path, r.RemoteAddr)
fmt.Fprintf(w, "Hello from %s at %s\n", name, time.Now().Format(time.RFC3339))
})
http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "ok")
})
log.Printf("listening on :%s", port)
if err := http.ListenAndServe(":"+port, nil); err != nil {
log.Fatal(err)
}
}
EOF
cat > go.mod <<'EOF'
module hello
go 1.22
EOFこのアプリの意図
- 環境変数で挙動を変える (
PORT,APP_NAME) → ENVの章の復習- ログを stdout に出す (
log.Printf) →docker logsで見るため- ヘルスチェックエンドポイント (
/healthz) → 本番運用の定番「コンテナ化前提のアプリ」の最小要件を備えている。
2. Dockerfile を書く(マルチステージ)
cat > Dockerfile <<'EOF'
# ===== ビルドステージ =====
FROM golang:1.22-alpine AS builder
WORKDIR /src
# 依存定義を先に → キャッシュ最適化
COPY go.mod ./
RUN go mod download
# ソースをコピーしてビルド
COPY main.go ./
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /out/app main.go
# ===== ランタイムステージ =====
FROM alpine:3.19
# セキュリティ: 非rootユーザーで動かす
RUN addgroup -S app && adduser -S app -G app
WORKDIR /app
COPY --from=builder /out/app /app/app
# 環境変数のデフォルト
ENV PORT=8080
ENV APP_NAME=hello-go
# 非rootユーザーへ切替
USER app
# ドキュメント目的
EXPOSE 8080
# 起動コマンド(exec 形式)
CMD ["/app/app"]
EOFここに詰め込んだ「Level 1 で学んだこと」
- マルチステージ: golang ベースでビルド → alpine で実行(軽量化)
- キャッシュ最適化:
go.modを先に COPYCGO_ENABLED=0: 静的バイナリを作って alpine でも動くように-ldflags="-s -w": バイナリのサイズ削減- 非rootユーザー:
addgroup+USERでセキュリティ強化- 環境変数のデフォルト: ENV で
- EXPOSE 8080: ドキュメント
- exec 形式の CMD: JSON配列でシグナル制御
3. .dockerignore を書く
cat > .dockerignore <<'EOF'
.git
*.log
.DS_Store
README.md
.vscode
.idea
EOF今回はそれほど除外するものは無いが、習慣として書く
本番プロジェクトでは
node_modules、__pycache__、target/、dist/、.envなどを必ず入れる。
4. ビルド
docker build -t hello-go:v1 .
# サイズ確認
docker images hello-go
# REPOSITORY TAG IMAGE ID CREATED SIZE
# hello-go v1 abc123def 10 seconds ago ~10MBサイズの確認ポイント
マルチステージにより、最終イメージは 10MB前後。これがもし
golang:1.22のままだったら 350MB を超えていた。学んだことが効いている瞬間。
5. 起動して動作確認
# 起動
docker run -d --name myapp -p 8080:8080 hello-go:v1
# 動作確認
curl http://localhost:8080/
curl http://localhost:8080/healthz
# ログ
docker logs myapp
# コンテナの中を覗く(USERでapp ユーザーに切り替わってる)
docker exec myapp whoami
# → app(非rootが動いている証拠)
docker exec myapp ps aux
# → PID 1 が /app/app6. 環境変数で挙動を変える
# 停止&削除
docker stop myapp && docker rm myapp
# 別ポート、別名で起動
docker run -d --name myapp \
-p 9090:9090 \
-e PORT=9090 \
-e APP_NAME=production-greeter \
hello-go:v1
curl http://localhost:9090/
# Hello from production-greeter at ...
docker logs myapp環境変数の威力
イメージは 1個 なのに、起動時の設定で挙動が変わる。これが 12factor の “Config” 原則。
7. グレースフル停止を確認
# ログを別タブで追従
docker logs -f myapp &
# 停止(SIGTERM → 10秒待ち → SIGKILL の順)
docker stop myapp
# ps で停止確認
docker ps -a | grep myapp
# STATUS: Exited (0)
# ↑ 終了コード 0 = 正常終了
Exited (0)の意味終了コード 0 = 正常終了。
Exited (137)だと SIGKILL(強制終了)、Exited (1)以上だとエラー。トラブル時に最初に見る場所。
8. データ永続化を入れる(簡易版)
このアプリは状態を持たないが、ログをホストに永続化してみる:
# まず古いコンテナを削除
docker rm -f myapp
# bind mount でホストの logs/ にコンテナの /var/log を見せる
# (ただし、このGo アプリは log を stdout に出すだけなので、Docker のログを見る方が普通)
# → 練習として named volume を試す
docker volume create myapp_data
docker run -d --name myapp \
-p 8080:8080 \
-v myapp_data:/data \
hello-go:v1
# ボリュームの中身を確認(空のはず)
docker run --rm -v myapp_data:/data alpine ls -la /data
# コンテナ削除 → ボリュームは生きる
docker rm -f myapp
docker volume ls | grep myapp_data今回のアプリではボリュームは「使い方の確認」だけ
ファイルを書き込まないアプリだとボリュームの恩恵を実感しにくい。次の Level 2 で docker-compose を使って postgres + アプリ の構成を作る時に、本領を発揮する。
9. もう一度起動できることを確認
# 新しいコンテナで同じイメージを起動
docker run -d --name myapp \
-p 8080:8080 \
-v myapp_data:/data \
hello-go:v1
# 動作確認
curl http://localhost:8080/
# 「コンテナを消しても、もう一度起動できる」状態にした
docker rm -f myapp
docker run -d --name myapp -p 8080:8080 hello-go:v1
curl http://localhost:8080/これが「コンテナの強み」
イメージさえあれば、何度コンテナを消して作り直しても、全く同じ状態のアプリ が起動する。これが「環境差異の根絶」「使い捨てサーバー」の本質。
10. 後片付け
# コンテナ削除
docker rm -f myapp
# 念のため確認
docker ps -a | grep myapp # 何も出ない
# ボリューム削除
docker volume rm myapp_data
# イメージは取っておく(後で push 練習にも使える)
docker images | grep hello-go
# 全部消したいなら
# docker rmi hello-go:v1振り返り: Level 1 で身に付いたこと
Level 1 完了の証
ここまでで以下ができるはず:
- コンテナ = 隔離プロセス を自分の言葉で説明できる
docker runの主要オプションを暗記で打てるdocker ps / logs / exec / stop / rmが呼吸のように出る- イメージとコンテナの違い を即答できる
- レイヤーと Copy-on-Write の仕組みを説明できる
- Dockerfile を自分で書ける
- CMD / ENTRYPOINT / COPY / ADD / ARG / ENV を間違えない
- マルチステージビルド で軽量イメージを作れる
- ビルドコンテキスト の挙動が分かる
docker pushでレジストリに配布する流れを知っている- 3種のボリューム を使い分けられる
- コンテナのエフェメラル性 を腹落ちしている
これが「Docker入門完了」のチェックリスト。求人票の「Dockerが使える」レベルにほぼ到達。
「Docker使えます」の解像度
採用面接で「Docker使えます」と言って通用する基準:
- 使えます(実は怪しい):
docker runを見様見真似で打てる- 使えます(最低限): Dockerfile を書いてビルド・起動できる ← Level 1 完了でここ
- 得意です: Compose / マルチステージ / ベストプラクティスを語れる ← Level 2 で目指す
- 詳しいです: Kubernetes / セキュリティ / マルチアーキ対応もできる ← Level 3 以降
Level 2 への橋渡し
ここまでは 「1つのコンテナを動かす」 世界。実務では:
[ Web Server ] ←→ [ App Server ] ←→ [ Database ]
nginx Go アプリ postgres
のように 複数のコンテナを連携させる 構成が普通。これを docker run でやろうとすると:
# ネットワーク作って…
docker network create mynet
# DB を起動して…
docker run -d --name db --network mynet -v pgdata:/var/lib/postgresql/data -e POSTGRES_PASSWORD=... postgres:16
# アプリを起動して…
docker run -d --name app --network mynet -p 8080:8080 -e DATABASE_URL=postgres://... hello-go:v1
# nginx を起動して…
docker run -d --name web --network mynet -p 80:80 -v $(pwd)/nginx.conf:/etc/nginx/nginx.conf:ro nginx長い。さらに「停止する時の順序」「設定変更したら全部再起動」を手でやるのは現実的でない。
Level 2: docker-compose の世界
これを 1つの YAML ファイル で記述できるのが
docker-compose。services: db: { image: postgres:16, ... } app: { build: ., ... } web: { image: nginx, ... }起動:
docker compose up -dの1コマンド。詳細は次の章で。
締め: 振り返り(10分)
1. セッション録画を終了
exit2. Level 1 全体の振り返り(このノートに追記)
- Level 1 で一番手応えのあった瞬間:
- 一番ハマったところ:
- まだモヤモヤしているところ(次回のFAQに):
- Compose 以前に、もう少し練習したいこと:
- Level 2 で楽しみなテーマ:
3. リポジトリ化(学習の証跡)
cd ~/learn/docker/day07
git init
git add main.go go.mod Dockerfile .dockerignore
git commit -m "feat: hello-go コンテナ化(Level 1 復習)"
# README を書いてみる
cat > README.md <<'EOF'
# hello-go
Level 1 復習で作った Go の HTTP サーバー+Docker。
## ビルド
\`\`\`bash
docker build -t hello-go:v1 .
\`\`\`
## 起動
\`\`\`bash
docker run -d --name myapp -p 8080:8080 hello-go:v1
curl http://localhost:8080/
\`\`\`
EOF
git add README.md
git commit -m "docs: README 追加"やらかし事例: 総復習でも落とし穴
事例1:
go.modをCOPYし忘れるマルチステージで
COPY go.mod ./を忘れてgo mod downloadが空振り。依存ファイルは「必ず最初にCOPY」の鉄則。
事例2:
CGO_ENABLED=1のままビルドして scratch で動かないGo の static バイナリには
CGO_ENABLED=0が必要。scratch ベースで起動しない時はまずここを疑う。
事例3: ポート公開忘れで
curl: Connection refusedアプリは 8080 で listen しているが
-p 8080:8080を書き忘れ。Dockerfile のEXPOSEはあくまでドキュメント、実際の公開は-p。
事例4: 環境変数の typo に気づかず動かない
APP_NAMEをAPP_NAMWで渡してデフォルト動作。docker exec name envで必ず確認する習慣。
物語: Level 1 で身につけた「Linuxの目線」
Level 1 の最大の収穫は、コマンドを覚えたことではない。「コンテナを Linux プロセスとして見られるようになった」 こと。
docker run→ Linux プロセスを namespaces で隔離して起動docker exec→ 動いているプロセスの namespace に新プロセスを差し込むdocker stop→ SIGTERM を送り、応答なければ SIGKILLdocker rm→ 停止プロセスのメタデータと書込層を破棄docker build→ レイヤーを順に積み上げてイメージ tarball を作るdocker push→ tarball をレジストリに HTTP で配送
「魔法のコマンド」が「Linux カーネルの応用」として腑に落ちると、Compose / Kubernetes / クラウド と進むときも 同じ目線で読める。これが Level 2 以降の地盤になる。
自己評価チェックリスト
統合スキル(Level 1 卒業判定)
- Go の Hello World を書いた
- マルチステージの Dockerfile を書いた
- イメージサイズが 10-20MB に収まった
-
docker runで起動してcurlで確認できた -
docker logsでログが見えた -
docker execで中に入ってwhoamiで非rootを確認した - 環境変数で挙動を変えられた
- コンテナを削除 → 同じイメージで再起動できた
- Git でコミットした
説明スキル
- 「Docker は何が嬉しいの?」を1分で語れる
- Dockerfile のベストプラクティスを5つ挙げられる
- トラブル時(起動しない、ログが見えない、ポートが繋がらない)の初動が分かる
メタ認知
- Level 1 で一番手応えがあった瞬間を言語化した
- まだモヤッとしている領域を3つ書き出した(→ 後で復習)
詰まった時のチートシート
| やりたいこと | コマンド |
|---|---|
| ビルド | docker build -t name:tag . |
| 起動 | docker run -d --name X -p 8080:8080 image |
| 環境変数付き起動 | docker run -e KEY=VAL ... |
| ログ | docker logs -f name |
| 中に入る | docker exec -it name sh |
| 停止 | docker stop name |
| 削除 | docker rm name |
| 強制削除 | docker rm -f name |
| 一括掃除 | docker container prune |
| イメージサイズ | docker images |
| イメージ削除 | docker rmi image:tag |
「実務OK」基準(Level 1 卒業判定)
- 自分のアプリを「コンテナ化してください」と言われたら、30分で雛形が書ける
- Dockerfile のベストプラクティスを5つ以上挙げられる
- コンテナのトラブル(起動しない、ログが見えない、ポートが繋がらない)の初動が分かる
- 「Docker は何が嬉しいの?」を1分で説明できる
ここを越えたら、Level 2 の 複数コンテナ連携(docker-compose) に進む準備が整った。
次のレッスン
2-1. Docker Compose 基礎 - 複数コンテナのオーケストレーション へ。
複数コンテナを YAML で定義する世界。Web + アプリ + DB の3層を1コマンドで起動・停止できるようになる。docker run の数珠つなぎ地獄から解放される瞬間を味わう。