LoginSignup
827
905

More than 1 year has passed since last update.

Dockerfileのベストプラクティス

Last updated at Posted at 2021-06-04

業務やプライベートでのハンズオンを通して得た知見を元に、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

cacheが残る方法
RUN apk update && apk add \
    package-bar \
    package-baz \
    package-foo=1.3.*

このpackageのlistをcacheさせると多少imageが大きくなるので、cacheが残らないようにする

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で、指定するファイルをワイルドカードで指定しすぎてパースに時間がかかってしまうため。

.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の記載、ワイルドカードの多用をしないことを意識する。

.dockerignore(良いパターン)
.rspec
log
tmp
db/*.sqlite3
db/*.sqlite3-journal
vendor/assets
node_modules

.dockerignore アンチパターン

セキュリティーの観点

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

参考資料

メイン

サブ

827
905
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
827
905