今回はDockerをビルドしていくこととする.
今回から投稿スタイルを,理解した後にまとめるのではなく,結果に至ったまでの過程を記していきたいと考えている.基本的に細かい知識はChatGPTに頼っている.
まずDocker上でWebアプリケーションを立ち上げたい.最低限必要なファイルとして,main.go
, go.mod
, go.sum
の他に,新たにDockerfile
が必要になる.
このDockerfile
とは,アプリを動かすためのイメージの作り方をDockerに教える設計書である.
FROM golang:1.23.0-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -v -o /app/main .
FROM alpine:latest
COPY --from=builder /app/main /main
EXPOSE 8080
CMD ["/main"]
FROM golang:1.23.0-alpine AS builder
はビルド用のベースイメージを指定している.Alpine(軽量なLinuxディストリビューション(Linuxそのもの(カーネル)だけでは動かず,それにツールやライブラリ,設定を組み合わせて使えるようにした完成品)でコンテナのベースイメージとして最もよく使われるOS)上のGo 1.23.0でコンパイル(ソースコードを機械語に変換する処理)する.そしてそのステージ名をbuilder
とする.
WORKDIR /app
は作業ディレクトリを/app
に設定し,移行の相対パスの基準になること.このディレクトリはコンテナ内に作成される.
COPY go.mod go.sum ./ RUN go mod download
は,依存定義ファイルのみ先にコピーさせ,レイヤキャッシュ(Dockerがイメージを作る際に変更のない部分を再利用してビルドを早くする仕組み)を効かせるための定石である.
ここでレイヤキャッシュを説明するためにDockerのイメージの構造から解説する.Dockerイメージはレイヤ(層)の集合体で,各命令ごとに1層ずつ作られる.この時Dockerはレイヤごとにキャッシュを保存する.そして次回同じ命令,ファイル構成なら,そのレイヤを再利用して再ビルドを省略する.
仕組みの流れだが,Dockerは上から順に命令を実行していき,各命令の入力を記録しておき,次回に過去と同じ入力ならその結果を再利用し,違うファイルが一つでもあればそのレイヤ以降は全部再ビルドとなる.
ここからが核心の説明だが,依存関係のダウンロードを,機能のコードの頻繁な変更のたびに毎回行なっていたら時間がかかる.だから依存関係だけ先にコピーしておく.
COPY . .
は,残りのソースをコピーする.機能のコードの変更時はこのレイヤ以降のみ再ビルドになる.
RUN CGO_ENABLED=0 GOOS=linux go build -v -o /app/main .
これは,静的リンク(実行ファイルの中に必要なライブラリを全て埋め込む方式でどんな環境でも単体で動くバイナリ(機械語)を作れる)(cgo(GoがC言語のコードやライブラリを呼び出す機能で,有効にするとCコンパイラやglibcなどの外部環境が必要になる)無効)かつLinuxターゲットでビルドし,-v
は詳細出力,出力ファイルは/app/main
である.
FROM alpine:latest
は実行用の軽量で最新のランタイムイメージ(Dockerではよくマルチステージビルドが使われ,ビルドステージとランタイムステージがある.そのうちの二つ目のアプリを実行するためだけの最小限の環境を持つDockerイメージ)を指定する.
COPY --from=builder /app/main /main
は,ビルドステージから成果物バイナリだけをコピーし,実行イメージを小さく保つもの.ここにもしビルド用ツール(Goコンパイラ)が含まれていたら重くなる.パスに着目するとこれは,builderステージ(ビルドステージ)の/app/main → 現在のイメージ(ランタイムイメージ)の/mainへコピーするということである./app/mainでも良いが,ランタイムイメージでは,ディレクトリ構成を最小限にしたい.触れていなかったが,bilderは一行目で自分で指定した名前である.
EXPOSE 8080
は,コンテナが待ち受ける想定ポートをメタデータ(データについての情報)として宣言している.
CMD ["/main"]
は,コンテナ起動時に実行するデフォルトコマンドを指定するもので,ビルド済みのバイナリを起動する.
さて,Webアプリケーションを立ち上げる準備はできたので立ち上げる.その前に
go mod tidy
これを実行する.これは依存関係の整合性チェック専用コマンドで,import
を変えたときなどに実行すると,ビルド失敗や不要ファイル混入を防げる.
一度ローカルで動作確認をしておく.
go run main.go
そして別のターミナル上で実際にサーバーが立ち上がっているかを確認する.
curl http://localhost:8080/
これの実行結果が
Hello, Gin with Docker! :cocktail:%
出会ったので,いったんローカル上では成功した.
次にDocker環境で立ち上がったかを確認する.Docker起動の手順は以下の通り.
docker build -t Persk_go_version .
docker run Persk_go_version
これで立ち上がるはずである.しかしエラーが発生した.repository name must be lowercase
原因は,Dockerのリポジトリ名が小文字しか使えないというものだった.なのでこれを修正して,
docker build -t persk_go_version .
docker run persk_go_version
一応これで起動したのだが,ERROR: failed to solve: failed to read dockerfile: open /var/lib/docker/tmp/buildkit-mount1335310749/Dockerfile: no such file or directory
このようなエラーが出ていた.これは,このコマンドを実行したターミナルの場所がbackend
でなかったからだ.Dockerファイルの親ディレクトリで実行しないと正常に動かず,以前作成した同名イメージが残っていて,たまたま実行できたみたいだった.
cdコマンドで移動して再度実行すると,成功することができた.
しかしまだ問題があった.先ほどのcurl
コマンドで接続ができない.これはdocker run persk_go_version
これだけだとポートを公開していないことになるからである.よって書き直すと,
docker run -p 8080:8080 persk_go_version
このように変えた.そしてついに完璧に成功することができた.
ちなみに,Dockerfile内のEXPOSE 8080
はメタデータを登録するだけで,実際にホストへポートを開く設定ではないみたいだ.