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-11-6)で扱ったテーマを並べる:

Dayテーマキーワード
1-1コンテナとは何かnamespaces / cgroups / VMとの違い / OCI
1-2docker run-d / -p / -v / —rm / -e / -it / logs / exec
1-3イメージとレイヤーCopy-on-Write / レジストリ / タグ
1-4DockerfileFROM / RUN / COPY / CMD vs ENTRYPOINT / ARG vs ENV
1-5ビルドと配布ビルドコンテキスト / マルチステージ / push
1-6ボリュームbind mount / named volume / tmpfs / 権限問題

このレッスンは 「学んだことを全部使って何か1個動かす」 回。読み物ではなく、手を動かす実践。

なぜこの順で学んだか

振り返ると流れがある:

  1. 概念(1-1): 何を扱うのか「世界観」を先に
  2. 動かす(1-2): コマンドを叩いて感覚を掴む
  3. 解剖(1-3): 動かしたものの中身を覗く
  4. 作る(1-4): 自分でイメージを定義する
  5. 配る(1-5): 他のマシンで動かす
  6. 永続化(1-6): データを守る
  7. 統合(1-7): 全部使って何か作る ←今ここ

「使う → 解剖 → 作る」 の順は、新しい技術を学ぶ時の鉄板パターン。最初から「Dockerfile を完璧に書く」を目指すと挫折する。動かして感覚を掴むのが先。


実践課題: Hello World HTTP サーバーをコンテナ化する

0. 作業ディレクトリ

mkdir -p ~/log ~/learn/docker/day07
cd ~/learn/docker/day07
script ~/log/docker_day07.log

1. 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 を先に COPY
  • CGO_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/app

6. 環境変数で挙動を変える

# 停止&削除
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. セッション録画を終了

exit

2. 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.modCOPY し忘れる

マルチステージで 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_NAMEAPP_NAMW で渡してデフォルト動作。docker exec name env で必ず確認する習慣。

物語: Level 1 で身につけた「Linuxの目線」

Level 1 の最大の収穫は、コマンドを覚えたことではない。「コンテナを Linux プロセスとして見られるようになった」 こと。

  • docker run → Linux プロセスを namespaces で隔離して起動
  • docker exec → 動いているプロセスの namespace に新プロセスを差し込む
  • docker stop → SIGTERM を送り、応答なければ SIGKILL
  • docker 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 の数珠つなぎ地獄から解放される瞬間を味わう。