Dockerのマルチステージビルドが便利だった話

はじめに

最近,バックエンドにGo,フロントエンドにTypeScriptを採用したWebアプリケーションを作っているのですが,これをDockerコンテナ化するときに,どのようなDockerfileを書けばいいのか,少し迷ってしまいました.
どういう方法でDockerコンテナ化したのかを記録しておこうと思います.

Dockerのマルチステージビルドとは

前述の通り,バックエンドとフロントエンドでは別の言語を使用しているため,別々のランタイムを準備してビルドを行う必要があります.愚直にやるのであれば,Ubuntu等のイメージにGoとNode.jsのランタイムを入れてビルドするのがいいのでしょうが,これらはビルド時以外には使用しません.Goは最終的にバイナリファイルを1個吐き出すだけですし,TypeScriptのフロントエンドはビルドして静的ファイルとなります.従って,最終的なイメージには,使用しないランタイムが含まれてしまい,無駄が生じてしまいます.それに,使用するのであれば,最初からGoやNode.jsのイメージを使ってしまったほうがDockerfileもシンプルになります.この方法を実現するのがマルチステージビルドです.
docs.docker.com

Dockerfileの書き方

Dockerfileの一例はこのようになります.

FROM golang:latest AS build-backend
WORKDIR /go/src
COPY main.go main.go
COPY go.mod go.mod
COPY go.sum go.sum
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM node:lts AS build-frontend
WORKDIR /root
COPY ./frontend/package.json .
RUN npm i
COPY ./frontend/src/ ./src/
COPY ./frontend/public/ ./public/
COPY ./frontend/tsconfig.json .
COPY ./frontend/yarn.lock .
RUN npm run build

FROM alpine:latest
LABEL maintainer="maintainer@example.com"
WORKDIR /root/
COPY  --from=build-backend /go/src/app .
COPY --from=build-frontend /root/build/ ./frontend/build/
EXPOSE 8080
CMD [ "./app" ]

ディレクトリの構造は,アプリケーションのルートディレクトリにmain.go,frontendディレクトリの中にTypeScriptのプロジェクトという構成です.
FROMから次のFROMの直前までが一つのステージとなります.ステージにはFROM image AS nameというようにして名前をつけることができます.これはオプションで指定するものですが,後述のファイルのコピーの際に便利になるので,名前をつけたほうがいいと思います.
各ステージでは,依存関係のインストール,アプリケーションのビルド等を行い,成果物を生成します.
一番最後のステージはアプリケーションの実行用のイメージとなります.ここでは,前のステージで生成された成果物をコピーしています.コピーはCOPY [src] [dist]というように書きますが,--from=[src]というオプションが使用できます.ここで,COPY --from=build-backend /go/src/app .というように指定することで,build-backendステージの/go/src/appをコピーしてくることができます.このようにすることで,ランタイムは含めずに,成果物のみを含んだイメージを生成することができます.

おわりに

マルチステージビルドという機能自体は,dotnetのアプリケーションをDockerコンテナ化する方法を調べたときに見かけて知ってはいましたが,今回すぐには思い出せませんでした.
docs.microsoft.com
マルチステージビルドという方法があることを思い出して,使ってみたところ,かなりいい感じだったので良かったです.