sioaji2012のブログ

普段は組み込み開発でC言語のみです。主にプログラムや勉強日記です

Docker お勉強 03 Dockerfile

もくもくで使わせていただいているDockerfileを理解するためにお勉強です。

以下のページを参考に勉強して、もくもくで使ったDockerfileを例にあげて、覚え書きをこちらに記載します。

FROM: ベースイメージの指定

【解説】 Dockerfile リファレンスの FROM 命令 → 本家

可能であれば、自分のイメージの元として現在の公式レポジトリを使います。
私たちは Debian イメージ を推奨します。
これは、非常にしっかりと管理されており、ディストリビューションの中でも最小(現在は 100 MB 以下)になるよう維持されているからです。
FROM <イメージ>
FROM <イメージ>:<タグ>
FROM <イメージ>@<digest>
  • 以降の命令で使う ベース・イメージ を指定する。
  • コメント以外では FROM を一番始めに書く必要がある。
  • 単一の Dockerfile から複数のイメージを作成するため、複数の FROM を指定できる。
    各 FROM 命令ごとに自動的にコミットし、最新のイメージ ID が出力される。
  • タグ や digest 値はオプション。
    省略した場合、ビルダーはデフォルトの latest (最新)とみなす。
    ビルダーは一致する tag 値が無ければエラーを返す。
  • digestは、イメージのユニークな識別子ID (docker images --digestsで表示確認出来る)
    メージ生成後に変更が加えられなければ、digest 値は変更されていないと考えられる。
    v2 以降の形式を使うイメージで使える。

【もくもく例】

FROM ruby:2.1.10

LABEL: コンテナとイメージに付与できるラベル

【解説】Dockerfile リファレンスの LABEL 命令 → 本家

LABEL 命令はイメージにメタデータを追加します。 
LABEL はキーとバリューのペアです。 
LABEL の値に空白スペースを含む場合はクォートを使いますし、コマンドラインの分割にバックスラッシュを使います。

MAINTAINER 命令は現在非推奨

LABEL <key>=<value> <key>=<value> <key>=<value> ...

【例】

LABEL "com.example.vendor"="ACME Incorporated"
LABEL com.example.label-with-value="foo"
LABEL version="1.0"
LABEL description="This text illustrates \
that label-values can span multiple lines."
  • イメージは複数のラベルを持てます。
    可能であれば1つの LABEL にすることをお勧めします。
    複数のラベルを指定したら、各 LABEL 命令は新しいレイヤを準備しますが、多くのラベルを使えば、それだけレイヤを使います。
    次の例は1つのイメージ・レイヤを使うものです。
LABEL multi.label1="value1" multi.label2="value2" other="value3"

上記の例は、次のようにも書き換えられます。

LABEL multi.label1="value1" \
      multi.label2="value2" \
      other="value3"
  • ラベルには、FROM イメージが使う LABEL も含まれています。
    ラベルのキーが既に存在している時、Docker は特定のキーを持つラベルの値を上書きします。
  • イメージが使っているラベルを確認するには、 docker inspect コマンドを使います。

【もくもく例】

LABEL maintainer="K.SUZUKI / (Twitter: @KuniSioaji)" \
  description="Image to run Redmine simply with sqlite to try/review plugin."

ARG: docker build コマンドで使う変数

【解説】Dockerfile リファレンスの ARG 命令 → 本家

ARG 命令は、構築時に作業者が docker build コマンドで使う変数
ARG <名前>[=<デフォルト値>]
  • --build-arg <変数名>=<値> フラグを定義するものです。
    ユーザが構築時に引数を指定しても Dockerfile で定義されていなければ、構築時に次のようなエラーが出ます。
    One or more build-args were not consumed, failing build.
  • 複数の ARG を指定可能です。
  • ARG 命令のデフォルト値を指定できます。
    構築時に値の指定が無ければ、このデフォルト値を使います。
ARG user1=someuser
ARG buildno=1
  • ARG 変数は Dockerfile で記述した行以降で効果があります。ただし、コマンドライン上で引数の指定が無い場合です。

【例】

FROM busybox
USER ${user:-some_user}
ARG user
USER $user
...
$ docker build --build-arg user=what_user Dockerfile

FROM busybox
USER ${user:-some_user}
          → ここまでは、usr=some_user
ARG user
USER $user    → ここからは、usr=what_user (コマンドラインで指定したもの)
...
  • 構築時の変数として、GitHub の鍵やユーザの証明書などの秘密情報を含むのは、推奨される使い方ではありません。

  • ARG や ENV 命令を RUN 命令のための環境変数にも利用できます。
    ENV 命令を使った環境変数の定義は、常に同じ名前の ARG 命令を上書きします。

【例】

FROM ubuntu
ARG CONT_IMG_VER
ENV CONT_IMG_VER v1.0.0
RUN echo $CONT_IMG_VER

or

FROM ubuntu
ARG CONT_IMG_VER
ENV CONT_IMG_VER ${CONT_IMG_VER:-v1.0.0}
RUN echo $CONT_IMG_VER
$ docker build --build-arg CONT_IMG_VER=v2.0.1 Dockerfile

FROM ubuntu
ARG CONT_IMG_VER
ENV CONT_IMG_VER v1.0.0
RUN echo $CONT_IMG_VER →引数を設定した場合、v2.0.1。指定しない場合、v1.0.0
  • 以下のものは、ARG 命令を記載しなくても、ARG 変数で指定出来ます。
    • HTTP_PROXY
    • http_proxy
    • HTTPS_PROXY
    • https_proxy
    • FTP_PROXY
    • ftp_proxy
    • NO_PROXY
    • no_proxy

※これらを使うには、コマンドラインで --build-arg <変数名>=<値> フラグを単に渡すだけです。

構築キャッシュの影響

キャッシュについてはこちらのページをみて少し勉強しました。
Dockerfileを書く時の注意とかコツとかハックとか

ENV 変数は、値が残り続ける。
ARG 変数は、値が残り続けない。
ARG 変数は、構築キャッシュで残り続ける方法として扱える。

ARG 変数の値が以前の値と違う時は、「キャッシュ・ミス」が発生する。
ARG 変数の値を定義していなくても発生します。
特に、すべての RUN 命令は ARG 変数を(環境変数から)暗黙的に使おうとするため、結果としてキャッシュ・ミスを引き起こす。

【例】

--build-arg CONT_IMG_VER=<値> をコマンドライン上で指定

FROM ubuntu
ARG CONT_IMG_VER 
RUN echo $CONT_IMG_VER 
FROM ubuntu
ARG CONT_IMG_VER
RUN echo hello        → キャッシュ・ミス発生

上記の変更は、3行目でキャッシュミス発生。

FROM ubuntu
ARG CONT_IMG_VER
ENV CONT_IMG_VER $CONT_IMG_VER → キャッシュ・ミス発生
RUN echo $CONT_IMG_VER

上記は、ENV コマンドはイメージの中で処理される。キャッシュイメージと値が変わるのでミス発生。

FROM ubuntu
ARG CONT_IMG_VER
ENV CONT_IMG_VER hello → キャッシュ・ミス発生しない
RUN echo $CONT_IMG_VER

上記は、ENV コマンドはイメージの中で処理される。キャッシュイメージは、helloの固定値でミス発生しない。

ENVとの使い分け

Dockerfile ARG入門より

  • ENVでは、コンテナ内で環境変数として変数が定義されます。
    ENVを使用する場合は、CMDやENTRYPOINTによって実行されたコマンドに適している。
  • ARG は、 Dockerfileのbuild時に一時的に変数が定義され、コマンドの実行時に展開されます。
    ARGは、変数を展開した状態でコンテナ内にファイルを配置したい場合に有効です。
  • ARGは変数を展開した状態でコンテナに状態を設定するという特性から、コンテナが管理するファイルには有効ですが、 VOLUMEによって管理されているディレクトリに配置されているファイルに対してはENVを使ったほうが便利な場面が多いです。

【もくもく例】

### get Redmine source
ARG REDMINE_VERSION=3.4-stable

RUN: コマンドの実行

【解説】Dockerfile リファレンスの RUN 命令 → 本家

RUN 命令は既存イメージ上の新しいレイヤで、あらゆるコマンドを実行し、その結果をコミットする命令です。
コミットの結果得られたイメージは、 Dockerfile の次のステップで使われます。
ソース・コントロールのように、イメージの履歴上のあらゆる場所からコンテナを作成可能です。

RUN には2つの形式があります。

RUN <コマンド> 
 ・シェル形式、コマンドを実行する。
 ・Linux 上のデフォルトは /bin/sh -c であり、Windows 上 cmd /S /C。

RUN ["実行バイナリ", "パラメータ1", "パラメータ2"] 
 ・exec 形式
  • exec 形式はシェルの文字列を変更できないようにします。
    また、 指定されたシェル実行環境がベース・イメージに含まれなくても RUN コマンドを使えます。
  • デフォルトの shell のシェルを変更するには SHELL コマンドで変更できます。
  • シェル 形式では、RUN 命令を \ (バックスラッシュ)を使い、次の行と連結します。
  • RUN 命令によるキャッシュは自動的に無効化できません。
    RUN apt-get dist-upgrade -y のような命令のキャッシュがあれば、次の構築時に再利用されます。
    RUN 命令でキャッシュを使いたくない場合は、 --no-cache フラグを使います。
    例: docker build --no-cache .

一般的な利用例は apt-get アプリケーションです。
RUN apt-get コマンドは、パッケージをインストールするので、気をつけるべきいくつかの了解事項があります。

RUN apt-get update や dist-upgrade を避けるべきでしょう。
ベース・イメージに含まれる「必須」パッケージの多くが、権限を持たないコンテナの内部で更新されないためです。 もし、ベース・イメージに含まれるパッケージが時代遅れになっていれば、イメージのメンテナに連絡すべきでしょう。
たとえば、 foo という特定のパッケージを知っていて、それを更新する必要があるのであれば、自動的に更新するために apt-get install -y foo を使います。

RUN apt-get updaate と apt-get install は常に同じ RUN 命令文で連結します。以下は実行例です。

RUN apt-get update && apt-get install -y \
    package-bar \
    package-baz \
    package-foo

RUN 命令文で apt-get update だけを使うとキャッシュ問題を引き起こし、その後の apt-get install 命令が失敗します。
例えば、次のように Dockerfile を記述したとします。

FROM ubuntu:14.04
RUN apt-get update
RUN apt-get install -y curl

イメージを構築すると、全てのレイヤは Docker にキャッシュされます。
次に、別のパッケージを追加する apt-get install を編集したとします。

FROM ubuntu:14.04
RUN apt-get update
RUN apt-get install -y curl nginx

Docker は冒頭からファイルを読み込み、命令の変更を認識すると、前のステップで作成したキャッシュを再利用します。しかし、 apt-get update は 決して 実行されず、キャッシュされたバージョンを使います。これは apt-get update が実行されていないためです。そのため、古いバージョンの curl と nginx パッケージを取得する恐れがあります。

そこで Dockerfile でのインストールには RUN apt-get update && apt-get install -y を使うことで、最新バージョンのパッケージを、追加の記述や手動作業なく利用できます。

RUN apt-get update && apt-get install -y \
    package-bar \
    package-baz \
    package-foo=1.3.*

バージョンを指定すると、何がキャッシュされているか気にせずに、特定バージョンを取得した上での構築を強制します。このテクニックは、必要なパッケージの予期しない変更によって引き起こされる失敗を減らします。

以下は 推奨する apt-get の使い方の全てを示すものです。

RUN apt-get update && apt-get install -y \
    aufs-tools \
    automake \
    build-essential \
    curl \
    dpkg-sig \
    libcap-dev \
    libsqlite3-dev \
    lxc=1.0* \
    mercurial \
    reprepro \
    ruby1.9.1 \
    ruby1.9.1-dev \
    s3cmd=1.1.* \
 && apt-get clean \
 && rm -rf /var/lib/apt/lists/*
  • s3cmd 命令行は、バージョン 1.1.* を指定します。
    従来のイメージが古いバージョンを使っていたとしても、新しいイメージは apt-get update でキャッシュを使わないので、確実に新しいバージョンをインストールします。
  • 各行はパッケージのリストであり、パッケージの重複という間違いをさせないためです。
  • 付け加えると、apt キャッシュをクリーンにし、 /var/lib/apt/lits を削除することで、イメージのサイズを減らします。
  • RUN 命令は apt-get update から開始されるので、 apt-get install でインストールされるパッケージは、常に新鮮なものです。

【もくもく例】

1)
FROM ruby:2.1.10
LABEL maintainer="K.SUZUKI / (Twitter: @KuniSioaji)" \
  description="Image to run Redmine simply with sqlite to try/review plugin."

#
# You can run target version of Redmine.
# If you use this Dockerfile only, try this:
#    $ docker build -t redmine_stable .
#    $ docker run -d -p 3000:3000 redmine_stable
#
# You can change Redmine version with arg
#    $ docker build --rm --build-arg REDMINE_VERSION=master -t redmine_master .
#    $ docker run -d -p 3000:3000 redmine_master
#

2)
### get Redmine source
ARG REDMINE_VERSION=3.4-stable
3)
### Replace shell with bash so we can source files ###
RUN rm /bin/sh && ln -s /bin/bash /bin/sh && echo "REDMINE_VERSION: ${REDMINE_VERSION}"

4)
### install default sys packeges ###
RUN apt-get update && apt-get install -qq -y \
    git vim            \
    sqlite3
5)
RUN cd /tmp && git clone --depth 1 -b ${REDMINE_VERSION} --single-branch https://github.com/redmine/redmine redmine_co
RUN echo "REDMINE_VERSION: ${REDMINE_VERSION}"
WORKDIR /tmp/redmine_co
6)
RUN echo $'test:\n\
  adapter: sqlite3\n\
  database: /tmp/data_co/redmine_test.sqlite3\n\
  encoding: utf8mb4\n\
\n\
development:\n\
  adapter: sqlite3\n\
  database: /tmp/data_co/redmine_development.sqlite3\n\
  encoding: utf8mb4\n'\
>> config/database.yml
7)
RUN gem update bundler
RUN bundle install --without mysql postgresql rmagick test
RUN bundle exec rake db:migrate
RUN bundle exec rake generate_secret_token
CMD bundle exec rails s -p 3000 -b '0.0.0.0'
EXPOSE 3000
  1. ベースのイメージは、ruby2.1.10にしています。
  2. ARG REDMINE_VERSION というところで、デフォルトでRedmine最新バージョンの安定版を指定
  3. Dockerfileのビルドはbashではなくshで実行されるためエラーが出る。
    shをbashへのシンボリックリンクで置き換えることで対応する。
  4. 最低限必要なパッケージを追加
  5. Redmineのソースを /tmp/ ディレクトリにgit cloneする (引数でタグバージョン指定してバージョン変更)
  6. SQLite3用のdatabase.ymlを作成する
  7. bundle installする
    Redmine本体のマイグレーションをする
    port 3000で起動する

WORKDIR: 作業ディレクト

【解説】Dockerfile リファレンスの WORKDIR 命令 → 本家

WORKDIR 命令セットは Dockerfile で RUN 、 CMD 、 ENTRYPOINT 、 COPY 、 ADD 命令実行時の作業ディレクトリ(working directory)を指定します。
もし WORKDIR が存在しなければ、 Dockerfile 命令内で使用しなくてもディレクトリを作成します。

明確さと信頼性のため、常に WORKDIR からの絶対パスを使うべきです。
また、 WORKDIR を RUN cd ... && 何らかの処理 のように増殖する命令の代わり使うことで、より読みやすく、トラブルシュートしやすく、維持しやすくします。
WORKDIR /path/to/workdir
  • 1つの Dockerfile で複数回の利用が可能です。
    パスを指定したら、 WORKDIR 命令は直前に指定した相対パスに切り替えます。

【例】

WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd

この Dockerfile を使えば、最後の pwd コマンドの出力は /a/b/c になります。

WORKDIR 命令は ENV 命令を使った環境変数も展開できます。
環境変数を使うには Dockerfile で明確に定義する必要があります。

【例】

ENV DIRPATH /path
WORKDIR $DIRPATH/$DIRNAME
RUN pwd

この Dockerfile を使えば、最後の pwd コマンドの出力は /path/$DIRNAME になります。

【もくもく例】

RUN cd /tmp && git clone --depth 1 -b ${REDMINE_VERSION} --single-branch https://github.com/redmine/redmine redmine_co
RUN echo "REDMINE_VERSION: ${REDMINE_VERSION}"
WORKDIR /tmp/redmine_co

CMD: docker runの際に起動するコマンド

【解説】Dockerfile リファレンスの CMD 命令 → 本家

Dockerfile で CMD 命令を一度だけ指定できます。
複数の CMD がある場合、最も後ろの CMD のみ有効です。

CMD の主な目的は、 コンテナ実行時のデフォルトを提供します 。 
デフォルトには、実行可能なコマンドが含まれているか、あるいは省略されるかもしれません。
省略時は ENTRYPOINT 命令で同様に指定する必要があります。

RUN と CMD を混同しないでください。 
RUN が実際に行っているのは、コマンドの実行と結果のコミットです。
一方の CMD は構築時には何もしませんが、イメージで実行するコマンドを指定します。

CMD には3つの形式があります。

■exec 形式、推奨する形式
CMD ["実行バイナリ", "パラメータ1", "パラメータ2"] 

■ENTRYPOINT のデフォルト・パラメータ
CMD ["パラメータ1", "パラメータ2"] 

■シェル形式
CMD <コマンド>
  • シェルあるいは exec 形式を使う時、 CMD 命令はイメージで実行するコマンドを指定します。
  • CMD を シェル 形式で使えば、 <コマンド> は /bin/sh -c で実行されます。

【例】

FROM ubuntu
CMD echo "This is a test." | wc -
  • <コマンド>をシェルを使わずに実行 したい場合、コマンドを JSON 配列で記述し、実行可能なフルパスで指定する必要があります。
    配列の形式が CMD では望ましい形式です 。
    あらゆる追加パラメータは個々の配列の文字列として指定する必要があります。

【例】

FROM ubuntu
CMD ["/usr/bin/wc","--help"]
  • もしコンテナで毎回同じものを実行するのであれば、 CMD と ENTRYPOINT の使用を検討ください。
    詳細は ENTRYPOINT をご覧ください。
  • ユーザが docker run で引数を指定した時、これらは CMD で指定したデフォルトを上書きします。

【補足】

CMD 命令は、イメージに含まれるソフトウェアの実行と、その引数のために使うべきです。
CMD は常に CMD [“executable”, “param1”, “param2”…] のような形式で使うべきです。

そのため、イメージがサービス向け(Apache、Rails 等)であれば、 CMD ["apache2","-DFOREGROUND"] のようにすべきでしょう。
実際に、あらゆるサービスのベースとなるイメージで、この命令形式が推奨されます。

大部分の他のケースでは、 
CMD はインタラクティブなシェル(bash、python、perl 等)で使われます。

たとえば、 CMD ["perl", "-de0"] 、 CMD ["python"] 、 CMD [“php”, “-a”] です。
この利用形式が意味するのは、 docker run -it python のように実行すると、それを使いやすいシェル上に落とし込み、すぐに使えるようにします。 
あなたとあなたの想定ユーザが ENTRYPOINT の動作になれていない限り、CMD を ENTRYPOINT と一緒に CMD [“パラメータ”, “パラメータ”] の形式で使うべきではないでしょう。

【もくもく例】

RUN gem update bundler
RUN bundle install --without mysql postgresql rmagick test
RUN bundle exec rake db:migrate
RUN bundle exec rake generate_secret_token
CMD bundle exec rails s -p 3000 -b '0.0.0.0'
EXPOSE 3000

EXPOSE: コンテナ内のプロセスがListenするポート

【解説】 Dockerfile リファレンスの EXPOSE 命令 → 本家

EXPOSE 命令は、コンテナが接続用にリッスンするポートを指定します。
そのため、アプリケーションが一般的に使う、あるいは、伝統的なポートを使うべきです。
例えば、Apache ウェブ・サーバのイメージは、 EXPOSE 80 を使い、
MongoDB を含むイメージは EXPOSE 27017 を使うでしょう。

外部からアクセスするためには、ユーザが docker run 実行時にフラグを指定し、
特定のポートを任意のポートに割り当てられます。
コンテナのリンク機能を使うと、Docker はコンテナがソースをたどれるよう、環境変数を提供します(例: MYSQL_PORT_3306_TCP )。
EXPOSE <port> [<port>...]
  • EXPOSE 命令は、特定のネットワーク・ポートをコンテナが実行時にリッスンすることを Docker に伝えます。
  • EXPOSE があっても、これだけではホストからコンテナにアクセスできるようにしません。
    アクセスするには、 -p フラグを使ってポートの公開範囲を指定するか、 -P フラグで全ての露出ポートを公開する必要があります。
    外部への公開時は他のポート番号も利用可能です。
  • ホストシステム上でポート転送を使うには、 -P フラグを使う をご覧ください。
  • Docker のネットワーク機能は、ネットワーク内でポートを公開しないネットワークを作成可能です。
    詳細な情報は 機能概要 をご覧ください。

Dockerfile コマンドについては、一旦ここまで。