3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Dockerイメージの最適化

Posted at

こんにちは、
アプリケーションエンジニアとして働いてる、キムです。
今回は自分が作成、運営中のシステムのDockerイメージの最適化、イメージの軽量化のコツについていくつか方法を共有したいと思います。

確か、この方法以外により良い方法もあると思いますので、なにかあればコメントとかで残していただけると嬉しいです。

入る前に

アプリケーションをDocker Containerで作って使う時、ちゃんと気にしないと作成されたDockerイメージの容量が考えたことより大きくてサーバーにデプロイするときや、共有が難しくなるかもしれません。

しかし、下記の流れをみてDockerイメージ容量に影響を与える部分をどうやって回避するのかを理解できれば、より軽量のイメージを作られると思います。

今日はその方法の中、自分が経験し適用してみたコツを共有しようと思います。

やってみましょう!

それでは比較のために何も気にせずDocker Imageを作ってみましょう。
自分は今回Nuxtでプロジェクトを作ってみました。

$ yarn create nuxt-app qiita_test
yarn create v1.22.17
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...

success Installed "create-nuxt-app@4.0.0" with binaries:
      - create-nuxt-app

create-nuxt-app v4.0.0
✨  Generating Nuxt.js project in qiita_test
? Project name: qiita_test
? Programming language: JavaScript
? Package manager: Yarn
? UI framework: None
? Nuxt.js modules: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Linting tools: ESLint, Prettier
? Testing framework: Jest
? Rendering mode: Universal (SSR / SSG)
? Deployment target: Server (Node.js hosting)
? Development tools:
? Continuous integration: GitHub Actions (GitHub only)
? What is your GitHub username? seongjoo kim
? Version control system: Git

これでPJが作成されました。次はDockerfileを作成して下記のように書いてみましょう。
本当に基本的な、何も考えず基本的な処理を行うDockerfileになります。

FROM node:16

WORKDIR /app
COPY . /app
RUN yarn global add webserver.local
RUN yarn install & yarn build

EXPOSE 3000
CMD webserver.local -d ./.nuxt

コード作成が終わったら下記のコマンドでイメージを作ってみましょう。

$ docker image build . --tag=demo-docker

このコマンドで、イメージが作成されたら内容を確認してみましょう。

$ docker images demo-docker
REPOSITORY    TAG       IMAGE ID       CREATED          SIZE
demo-docker   latest    c6d2943f9cf0   18 seconds ago   1.13GB

なんと…1.13GB…!これはすごいですね。

Docker Imageの軽量化

このすごい容量をどうすれば軽量化できますかね。
この問題を解決するに自分は2つの方法でアプローチしてみました。

1.より軽いベースイメージを選択すること

Dockerイメージは基本ベースイメージを元にし、その上にアプリやパッケージを積み上げていく方法になります。
このベースイメージを我々が普段知っているサーバーOS(CentOS,Ubuntu)をベースとして作ると、Docker環境での不要なパッケージや、ファイルも含まれていて自然に容量が高くなります。
(一般的にサーバーにOSをインストールすると、かなり容量を食いますね?Dockerを使うかそうかも結局サーバーにOSにをインストールすることと同じ概念です。)

なので、この問題点を解決するためのDockerのためのOSがつくられて、その中に代表的なものがAlpineでございます。

AlpineはDockerのためのLinuxのDistributeであります。
(一般的なOSをDocker上で軽く使いたく、カスタマイジングされたものになります。)

それでは、上記のDockerfileをAlpineをベースに修正してBuildし直してみましょう

FROM node:16-alpine

WORKDIR /app
COPY . /app
RUN yarn global add webserver.local
RUN yarn install & yarn build

EXPOSE 3000
CMD webserver.local -d ./.nuxt

どうでしょうか?!

REPOSITORY    TAG       IMAGE ID       CREATED          SIZE
demo-docker   latest    1ce499968dde   15 seconds ago   369MB

なんと!1.13GBから、369MBになりました。
でも、私はこれで満足できません!!
もう少し減らしてみましょう!

2.Multi-stage Build

これは、Buildする時、Build処理だけ必要な作業をわけてBuildを行い、最終Containerイメージには追加しない方法になります。
つまり、Buildステップを分けて、不要なものを結果物には含まれないように作る方法になります。

Dockerfileを下記のように修正してみましょう

FROM node:16-alpine AS build
WORKDIR /app
COPY . /app
RUN yarn install & yarn build

FROM node:16-alpine
WORKDIR /app
RUN yarn global add webserver.local
COPY --from=build /app/.nuxt ./.nuxt
EXPOSE 3000
CMD webserver.local -d ./.nuxt

Buildのステップを2つに分けました。
Build環境セットアップ及びBuild後、実際Buildされた結果だけウェブサーバーで使うようにします。

Build結果は下記のようになります。
更に減りましたね!

$ docker images demo-docker
REPOSITORY    TAG       IMAGE ID       CREATED          SIZE
demo-docker   latest    fe177f7962d8   37 seconds ago   119MB

ここまで来たら、なにかもう少し物足りないな…と思うと思います。
それはウェブサーバーをなぜwebserver.local を使ってるのかです。

我々にはもう少し軽く、早いNginxApacheがあるのにです!

つぎはこれはNginxに書き換えてみましょう

FROM node:16-alpine AS build
WORKDIR /app
COPY . /app
RUN yarn install & yarn build

FROM nginx:stable-alpine
COPY --from=build /app/.nuxt /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

じゃぁ、最後の結果を見ましょう
どうでしょうか!?

$ docker images demo-docker
REPOSITORY    TAG       IMAGE ID       CREATED          SIZE
demo-docker   latest    3d3251e6baf7   56 seconds ago   23.8MB

最初に始まるときに1.19GBだったことを覚えてますでしょうか?
今まで、修正してきたことを見売ればわかりますが、本当に小さな修正がここまで容量の差分を作り出します。

おまけ.1

普段は、このAlpine Linux Dockerイメージで始まることをおすすめします。
その理由としては、上記見たとおりにイメージのサイズが小さいからです。では、なぜ小さいでしょうか?
それは、Cライブラリーとしてmuslを使ってるし、いろんなUnixツールを持っているbusyboxをベースとしてるからです。

しかし、Pythonアプリケーションを作る時にはAlpineをおすすめしません。
その理由はUsing Alpine can make Python Docker builds 50× slowerこちらを読んでみてください。
この記事をざっくり略してみると、

  • PyPIに上げたPythonライブラリーは普段Wheelフォーマットを使いますが、Alpine LinuxはWheelフォーマットをサポートしてないので、ソースコード(.tar.gz)をわざとダウンロードして直接コンパイルする場合があるからです。
  • Wheelフォーマットをサポートしてない理由はmuslライブラリーがGNU Cライブラリー(glibc)としてコンパイルされたWheelバイナリーをサポートしてないからです。なのでPyPIをつかって.whlとして作られたパッケージではなく、ソースコード(.tar.gz)をダウンロードすることです。
  • WheelフォーマットはPythonでインストールなしで直接実行できるメリットがあります。その理由はWheelフォーマットそのものが、zip圧縮から拡張子だけ変えたものであり、PythonはJavaの.jarのようにzipファイルをすぐに実行できるからです。

なので、Apline Linuxを使うのであれば、すべてのPythonパッケージからCコードをコンパイルしなきゃいけないので、イメージをBuildするには時間がかなりかかります。

FROM python:3.8-alpine
RUN apk --update add gcc build-base freetype-dev libpng-dev openblas-dev
RUN pip install --no-cache-dir matplotlib pandas

このようなイメージをBuildするには自分の環境だと30分ぐらいかかりました。
ついでに、Alpine LinuxをベースとしたPythonイメージは容量もそこそこあるし、いくつかの脆弱性も発見されてるいます。

結論を言うと、Pythonアプリケーションを使うときにはDebian Busterをベースとしたpython:3.8-busterまたは、3.8-slim-busterなどを使うことをおすすめします。
※参考:The best Docker base image for your Python application (August 2021)

おまけ.2 その他のTIPS

イメージレイアの数を減らしましょう

過去、Dockerのバージョンではイメージレイア数がパフォーマンスに影響をかけたと言われてます。
しかし、もう大丈夫です。

それでも、Dockerのレイアー数を減らすことは最適化的な面では役に立つと思います。
レイアーはRUN,ADD,COPYコマンドだけで作られるので、下記のように複数の別れてるコマンドをChainingしてつなげてみましょう
レイアー数を少なくすることでDockerイメージ、Containerのパフォーマンスに影響があるわけではないですが、Dockerfileの可読性や、メンテナンス面では役に立つと思います。

RUN apt-get update
RUN apt-get -y install git
RUN apt-get -y install locales
RUN apt-get -y install gcc
↓
RUN apt-get update && apt-get install -y \
    gcc \
    git \
    locales

このように作成することで、既存4つのレイアーが作られたことを一つのレイアーとして減らせます。
そして、これをやりながら、インストールするパッケージの順番をアルファベット順に書き換えたりすることで、可読性も高く、重複インストールを防止できる効果もあります。

アプリケーションコードをコピーする部分は下へ

下記のサンプルDockerfileにはCOPYが2回実行されます。
1回目は依存性パッケージが明示されたファイル、2回目はアプリケーションコードが保存されてるディレクトリーであります。

FROM python:3.8-slim-buster

WORKDIR /app

COPY requirements.txt /app
COPY django_project /app

RUN pip install -r requirement.txt

CMD ["pip", "freeze"]

通常、依存パッケージは頻繁に変わらないため、最初のCOPY命令で作成されたレイヤーはキャッシュされます。
しかし、2番目のCOPYはビルドするたびに変わることができるため、キャッシュが頻繁に初期化され、次に実行されるRUNコマンドで実行される依存パッケージのインストールが毎回実行される可能性が高いです。
このようになると、不必要にビルド時間が増えて、やや依存性パッケージバージョンをlatest*にしておけば、予期せずパッケージバージョンが上がることがります。

したがって、アプリケーションコードコピーコマンドは、頻繁に変更されないステートメントの後に来ると、イメージの構築時間を短縮するのに有利です。

プログラミング言語ごと、パッケージマネージャが提供するLockファイルを使うこと

前の説明で依存パッケージを指定したファイルとしてrequirements.txtを使用しました。 このファイルは、Pythonの公式パッケージマネージャであるPIPで使用される慣行的に呼ばれるファイルです。

Python開発環境に基づいてみると、PIPはパッケージ固有の相互依存関係への関係を管理するのに不十分な面があります。 だから最近では、より進化したLockingシステムを備えたPipenvPoetryを使用することをお勧めします。

これらのツールを使用して生成されたロックファイル(Pipfileなど)に基づいてパッケージをインストールできるようにすることで、上記説明したキャッシュレイヤの利点を得ることができ、予期しないパッケージバージョンの更新を防ぐことができます。

FROM python:3.8-slim-buster

WORKDIR /tmp
RUN pip install pipenv

COPY Pipfile /tmp/Pipfile
COPY Pipfile.lock /tmp/Pipfile.lock

RUN pipenv install --system --deploy

CMD ["pip", "freeze"]

終わり

いかがでしょうか?
もう、Dockerはウェブアプリケーションの開発では当たり前な時期になりました。
Dockerfileの修正ははじめにアプリケーションを立ち上げるとき以外はなかなか手を入れづらいとおもいます。
しかし、このような技を身に着けておくと今後メンテナンスしやすいベースを作り出すと思います。
参考になったら嬉しいです。

参考資料

3
4
0

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
3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?