業務やプライベートでのハンズオンを通して得た知見を元に、dockerfileの実践的な書き方を記載いたしました。
軽量なdocker imageを作る観点とセキュリティーの観点を踏まえた内容になっております。なにか付け足す点などあればコメントいただければと思います。
#軽量なimageを作る観点
##軽量なimageの使用
Dockerfileでimageを指定する際に、軽量なimageを使用することが進めれている。
FROM golang:1.16-alpine AS build
docker docsでも代表的な軽量なimageのalpineをおすすめしている。
Whenever possible, use current official images as the basis for your images. We recommend the Alpine image as it is tightly controlled and small in size (currently under 5 MB), while still being a full Linux distribution.
###注意点
軽量なimageを使用するのを基本とするのは良いが、
軽量なimageだともともと入っているバイナリやライブラリが少ない。
そのため大量のライブラリをinstallすることになると逆にbuildに時間がかかってしまう。
仕事でPythonコンテナをデプロイする人向けのDockerfile (1): オールマイティ編
パッケージダウンロードは逐次処理なので遅く、処理系が入ったイメージのダウンロードの方が高速です。並列で処理されるし、一度イメージをダウンロードしてしまえば、なんどもビルドして試すときに効率が良いです。また、多くのケースでネイティブのライブラリも最初から入っており、ビルドでトラブルに遭遇することはかなり減るでしょう。
そのためパッケージのinstallが多く必要な場合は、
パッケージの確保がされたimageを使用してMulti stage buildをするのが良い。
最近ではdistrolessというshellのないimageを採用する場合もある。
#comment-3d0c9055614ed5b61bea
軽量Dockerイメージに安易にAlpineを使うのはやめたほうがいいという話
##インストールするpackage listをcacheさせない
Dockerfile内でapk updateなどのコマンドでpackageのリストを更新すると、
packageのリストを/var/cache/apk/などにcacheとして残すようになっている。
Alpine Linux package management
RUN apk update && apk add \
package-bar \
package-baz \
package-foo=1.3.*
このpackageのlistをcacheさせると多少imageが大きくなるので、cacheが残らないようにする
RUN apk add --update-cache --no-cache
package-bar \
package-baz \
package-foo=1.3.*
##Multi stage build
multi stage buildとはimageを複数つくり、片方のimageを、もう片方のimageのディレクトリをCOPYして作成する方法。
imageを2個作成しているので、FROMが2回出てきている。
Goのimageであるbuildの/bin/projectを、scratchのimageの/bin/projectにCOPYをして使用している。
# syntax=docker/dockerfile:1
FROM golang:1.16-alpine AS build
# Install tools required for project
# Run `docker build --no-cache .` to update dependencies
RUN apk add --no-cache git
RUN go get github.com/golang/dep/cmd/dep
# List project dependencies with Gopkg.toml and Gopkg.lock
# These layers are only re-built when Gopkg files are updated
COPY Gopkg.lock Gopkg.toml /go/src/project/
WORKDIR /go/src/project/
# Install library dependencies
RUN dep ensure -vendor-only
# Copy the entire project and build it
# This layer is rebuilt when a file changes in the project directory
COPY . /go/src/project/
RUN go build -o /bin/project
# This results in a single layer image
FROM scratch
COPY --from=build /bin/project /bin/project
ENTRYPOINT ["/bin/project"]
CMD ["--help"]
Goのimageを容量が大きいので、必要なディレクトリをGoのimageで作成して、
軽量なimageのscratchにディレクトリをコピーしている。
つまりmulti stage buildをすることで、使用しないfileやlibraryなどが減り作成されるimageの容量が小さくなる。
今回の例ではscratchという空のdocker imageに、バイナリファイルのみを配置して動作するようにしている。
デプロイに特化(開発環境用のパッケージなどを持たない)していて、Goなどのコンパイル言語で開発している場合に、このようなimage定義をすることができる。
docker docsでもmulti stage buildをおすすめしている。
Multi-stage builds allow you to drastically reduce the size of your final image, without struggling to reduce the number of intermediate layers and files.
##RUNではcommandを&&でつなぐ
Dockerfileでimageを作成する際に、できるだけ容量を小さくするようにすることが大事である。
しかしRUNやCOPYやADDという命令はdocker imageのLayerを増やすことになり、最終的にできるimageの容量がおおきくなってしまう。
docker docsにも「Layerを最小限にしよう」と記載されている
Minimize the number of layers
In older versions of Docker, it was important that you minimized the number of layers in your images to ensure they were performant. The following features were added to reduce this limitation:Only the instructions RUN, COPY, ADD create layers. Other instructions create temporary intermediate images, and do not increase the size of the build.
###RUNを複数回使用した場合
コマンドを毎回、RUNで呼び出している場合、最終的にできるimageのサイズは153MBになる
RUN npm ci
RUN npm cache clean --force
RUN mv /app/node_modules /node_modules
$ docker build -t run_command1_run_command2
REPOSITORY TAG IMAGE ID CREATED SIZE
run_command1_run_command2 latest 2f66932a5335 10 seconds ago 153MB
###RUNを一度だけ使用した場合
コマンドを&&で数珠つなぎにした場合、最終的にできるimageのサイズは146MBになる
RUN npm ci \
&& npm cache clean --force \
&& mv /app/node_modules /node_modules
$ docker build -t run_command1_&&_command2
REPOSITORY TAG IMAGE ID CREATED SIZE
run_command1_&&_command2 latest a6627748ccc8 10 seconds ago 146MB
つまりRUNを多用すると最終的にできるimageの容量が大きくなってしまうので、できるだけ数珠つなぎで実行するのが良い。
###補足
dockerのimageを一度buildすると作成されたLayerはdocker cacheという場所に配置される。
そして再度imageのbuildを行った際に、RUNのcommandの内容が、前回のbuildのRUNのcommandの内容と同じ場合、
前回のimageのbuildで作成されたLayerを使用する。
docker docsにもそのように記載されている
After building the image, all layers are in the Docker cache.
Docker sees the initial and modified instructions as identical and reuses the cache from previous steps.
つまり中間のLayerがあったほうがdocker cacheを利用して、
差分buildを行うことができるので早くbuildのトライアンドエラーができる。
そのため開発環境で最初は開発効率を損なわないようにするために、すべてのステップでRUNでcommandを呼び出し、
build内容が決まってきたら、RUNの回数を減らすために&&を使用するようにするのが良い。
##COPY --chownを使用する
ファイルオーナーの権限変更は、通常chownやchmodで行われる。しかしRUN chown && chmodを使用すると作成されるimageの容量が大きくなる。そこでCOPY --chown=を使用してdocker containerにファイルを配置する時に権限の変更を行う。
###RUN chown && chmodを使用した場合
RUN chown && chmodでLayerが5.84MB大きくなっている。
COPY . .
RUN chown -R node:node /app && \
chmod -R 744 /app && \
chown -R node:node /node_modules && \
chmod -R 777 /node_modules && \
$ docker history run_chown_&&_chmod
IMAGE CREATED CREATED BY SIZE COMMENT
.
.
.
4fc46c072028 2 weeks ago /bin/sh -c chown -R node:node /app && ch… 5.84MB
###COPY --chown=を使用した場合
COPY --chown=node:node . .の場合Layerは474kBしか大きくなっていない。
COPY --chown=node:node . .
$ docker history run_chown_&&_chmod
IMAGE CREATED CREATED BY SIZE COMMENT
.
.
.
32c46c083921 2 weeks ago /bin/sh -c COPY --chown=node:nodedir:… 474kB
##COPYを複数回に分ける
変更がされにくいpackage管理ファイルのCOPYと、変更されやすいアプリケーションコードのCOPYを分けて行う。
COPY requirements.txt /tmp/
RUN pip install --requirement /tmp/requirements.txt
COPY . /tmp/
これにより、特に必要なファイルが変更された場合にのみ各ステップのビルドキャッシュが無効になる。
package管理ファイルは頻繁に変更されることは少ないので、アプリケーションコードのCOPYと分けて行っている。
package管理ファイルが変更されていない場合のbuildではdocker cacheのLayerを使用するためbuildが早くなる。
##.dockerigonore
COPYやADDの対象にしたくないfileを設定する。
.gitigonoreのように、buildに必要ないfileをimageに配置しないようにする。
docker docsでも.dockerigonoreの使用をおすすめしている。
To exclude files not relevant to the build (without restructuring your source repository) use a .dockerignore file. This file supports exclusion patterns similar to .gitignore files. For information on creating one, see the .dockerignore file.
###注意点
.dockerignoreによるbuildの速度低下が発生することがある。
原因としては、.dockerignoreで、指定するファイルをワイルドカードで指定しすぎてパースに時間がかかってしまうため。
# archives
*.zip
*.lzh
*.tar.gz
*.tgz
*.bz2
*.dmg
# OSX
.DS_Store #<= 汎用的なものは記載しない
.Spotlight-V100
.Trashes
.AppleDB
.AppleDesktop
.apdisk
# Rails
.rspec
log
tmp
db/*.sqlite3
db/*.sqlite3-journal
vendor/assets
# Node
node_modules
.dockerignoreでは最低限のfileの記載、ワイルドカードの多用をしないことを意識する。
.rspec
log
tmp
db/*.sqlite3
db/*.sqlite3-journal
vendor/assets
node_modules
#セキュリティーの観点
##rootユーザーでimageを作成しない
dockerfileでUSERの指定を行い、non-rootユーザーの権限を与えるようにする。
USER node
USERの指定がないdockerfileから作られたコンテナに入ると、root権限をもつ状態になる。
そのようになった場合万が一コンテナに侵入された場合にroot権限を与えてしまうことになる。
またそのような事態になった場合に、コンテナを載せいているホストにも侵入される可能性も高まるので、コンテナはnon-rootなuserで起動させなければいけない。
Run Docker as a non-root user
##ADDではなく、基本的にCOPYを使用する
ADDはローカルファイルのCOPYに加えて、インターネットに公開されているURLからコード取得できる機能のついている。
docker docs
The ADD instruction copies new files, directories or remote file URLs from and adds them to the filesystem of the image at the path .
外部のインターネットにアクセスすると悪意のあるURLからfileを取得しかねないので、基本的にはCOPYを使用する。
#その他
##imageのバージョンではlatestを使用しない
下記のようにimageのバージョンをlatestにしてしまうと、時間が経過するにつれてコンテナのimageのバージョンが変更される。
それはバグを生む原因になりうるので、しっかりバージョンを指定して使用するimageを宣言する。
FROM golang:latest
参考資料
##メイン
- Best practices for writing Dockerfiles
- Docker/Kubernetes 実践コンテナ開発入門
- 米シリコンバレーDevOps監修!超Docker完全入門(2020)【優しい図解説とハンズオンLab付き】
##サブ