LoginSignup
19
34

More than 3 years have passed since last update.

[Docker入門]勉強して得られたDockerの知見を色々まとめてみた(Windows・Python)

Posted at

社内で専門のインフラ関係の担当の方がいらっしゃって、環境などはよしなにしてくださっていたのですが、流石にそろそろDocker周りを自分でも勉強しておかないとまずいだろう・・ということで、最近Dockerにやっと入門したので、色々と学んだことを備忘録も兼ねてまとめたり試したりしていこうと思います。

経歴的にインフラ周りなど馴染みの薄いお仕事を長くしてきていたので、知識的に荒い(浅い)ところなどは色々ご容赦ください。
私も初心者ですし、あまりインフラとかに馴染みのない方向けの記事となります。

Windows環境(Docker for Windows)でLinuxやPython関係を動かしたりを中心に進めていきます。

この記事で触れること

  • コンテナなどの概要やメリット
  • Docker for Windowsのインストール・設定
  • コンテナとイメージ関係の基礎
  • Docker Hubやタグについて
  • Dockerfileの基礎
  • 実際にUbuntuにPythonなどを絡めたイメージのビルド

この記事で触れないこと

記事が長くなりすぎるので主に以下は触れません。
※機会があれば別の記事で対応します・・

  • ネットワークやファイルシステム周り
  • Docker Compose周り
  • SwarmやKubernetes関係など
  • サーバーへのデプロイ周りなど

そもそもコンテナって?Dockerを使うと何が嬉しいの?

コンテナーとは、ホストOS上に論理的な区画(コンテナー)を作り、アプリケーションを動作させるのに必要なライブラリやアプリケーションなどを1つにまとめ、あたかも個別のサーバーのように使うことができるようにしたものです。ホストOSのリソースを論理的に分離し、複数のコンテナーで共有して使います。
コンテナーとは? Kubernetesとは? 導入や運用、ユースケースを解説

これだけだとイメージが付きづらいので、画像を踏まえてかみ砕いていきます。

例として、Linuxのサーバーを使ったDjangoやFlaskなどでのwebサイトを考えます。
開発をしていく場合、ローカルのWindowsなどのPCで本番に合わせるためにLinuxを動かす必要があり、そこにPythonやらDjangoなどのライブラリやらwebサイトのコードを載せたりが必要になってきます。

このWindows上でLinuxなどを動かす技術が仮想環境(仮想マシン)などと呼ばれ、昔からVirtualBoxやVMWareといったツールやサービスなとを使うことで利用することができていました。

Windows PCの中に、もう一つパソコンが入っているようなイメージでしょうか。
これはこれで本番環境に近い形の環境がローカルのPCに用意できて、便利に使うことができます。

ただ、メモリやCPUなどを各仮想環境に割り振っていく必要があったりと、大元のWindows PCのリソースがどんどん持っていかれてしまってリソースの無駄が多かったり、仮想環境の起動時間が長かったりと、辛い面がありました。

例えがちょっと微妙ですが、複数のデザインツール(Photoshop, Illustratorなど)を使いたいときに例えてみます。
当たり前ですが、これらのツールを使うときには「PCを起動」→「Photoshop起動」→「Illustratorを起動」→「...」みたいな感じで、一度だけPCを起動すれば済みますし、ソフトの起動も連続して行えます。

一方で、VirtualBoxなどのケースで先ほどのツールで例えると、「PCを起動」→「別のPCを起動」→「Photoshopを起動」→「別のPCを起動」→「Illustratorを起動」みたいなことが必要でした。
この場合、通常のケースと比べると各OSが起動する時間を待たないといけなかったり、OSに必要なメモリなども持っていかれてしまったりで無駄が多くなってしまって辛いところです。

コンテナ(Docker)技術を使うと、前述のOSを起動してから複数のソフトを連続して起動するように、無駄無く環境を起動することができます(厳密にはコンテナ関係もDocker以外にも色々ありますが、今回は触れません)。

また、まるでソフトの1つのようにLinuxやそれに乗っかるPython、使うライブラリ、webサイトのコードを1つのコンテナーとして起動することができます。
無駄無くソフトを起動するような感覚で使えるため、VirtualBoxなどで仮想環境を起動するよりも起動時間が大分短くなり、且つ本番に近い形でWindows上で他のOSの隔離された環境を扱うことができるようになります。

大雑把に図にすると以下のようになります。

temp0119_2.png

その他にも、複数人でのローカル環境の統一がやりやすいとか、CI/CDとかで快適とか、デプロイが楽になるとか色々メリットがあります。

複数人でアプリケーションを開発する場合、開発者のマシンの数だけ開発環境を立てる必要があります(VDIでというのは無視)。まあ、大変ですよね
...
これが dockerのサービス/デーモンさえ動いていれば、1つの共通 Docker Imageを挿すだけで全く同じ環境が、開発者全員に行き渡ります。
...
開発で動いたものがまるっきりそのまま本番、しかもCIもその開発で使っていたImageなので、如何に安定性・リスク回避からも有利であることがわかると思います。
2018年なぜ私達はコンテナ/Dockerを使うのか

環境構築や共有なども楽になる

また、1つ1つのコンテナーを構築するのはDockerfileと呼ばれるファイルのコード(テキスト)で作られるため、構築の手順の再現が容易という点もメリットとして挙げられます(Infrastructure as Codeなどと呼ばれます)。
今までVirtualBoxなどで環境を作る際、手順や必要なライブラリのインストールなど、結構辛いものがありました(順番が違うとか、共有するにしてもファイルサイズが巨大など)。

Dockerでは、必要なOSやライブラリ、コードなどの指定をテキストファイルで行われるため、構築は楽ですし構築したものの共有・配布なども楽になっています。

Docker for Windowsのインストール

さっそくWindowsにDockerをインストールしていきます。

まず前提として、Windows10が必要になります(Windows7もサポートが切れたので多くの方がWindows10だと思いますが、Win7だとDockerが使えません)。

Hyper-VとContainersの有効化

Windowsの設定でHyper-VとContainersの機能を有効にする必要があります。
「Windowsの機能」から設定ができます。

スタートメニューで「アプリと機能」と検索します。

image.png

開かれる設定画面の右の方にある「プログラムと機能」を選択します。

image.png

左メニューにある、「Windowsの機能の有効化または無効化」を選択します。

image.png

開かれるWindowで、CotnainersとHyper-Vにチェックを入れます。

image.png

OKを押したら一度PCを再起動します。

Docker Desktop for Windowsのダウンロードとインストール

Docker Desktop for Windowsのページからインストーラーのダウンロードなどを行います。
Dockerのアカウントが必要になります(後述しますが、ダウンロード時以外でもアカウントが必要になってきます)。
アカウントの作成が終わったら、ダウンロードしてそちらのインストーラーを使ってインストールしていきます(特に迷うことなく、そのままインストーラーの指示通りに進めていけばとりあえず動きます)。
ファイルが1GB近い、結構大きなファイルなので注意してください。

インストールが終わったら、一度サインアウトもしくは再起動します。

再起動後、Dockerは自動的に起動します。

右下の常駐アプリ表示の部分をクリックすると、クジラのアイコンが表示されていることか確認できます。

image.png

コマンドラインを開いて、dockerが動くか確認するため、バージョンを表示するコマンドを実行してみます。

$ docker version
Client: Docker Engine - Community
 Version:           19.03.5
 API version:       1.40
 Go version:        go1.12.12
 Git commit:        633a0ea
 Built:             Wed Nov 13 07:22:37 2019
 OS/Arch:           windows/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          19.03.5
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.12.12
  Git commit:       633a0ea
  Built:            Wed Nov 13 07:29:19 2019
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          v1.2.10
  GitCommit:        b34a5c8af56e510852c35414db4c1f4fa6172339
 runc:
  Version:          1.0.0-rc8+dev
  GitCommit:        3e425f80a8c931f88e6d94a8c831b9d5aa481657
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683

問題なく表示されました。

Linuxを扱うための切り替えを行う

Docker for Windowsでは、WindowsのPCでWindowsのDocker環境を使うか、Windows用のPCでLinuxの環境を使うかが切り替えができます。
今回はWindows上でLinuxを動かしたいので、Linuxの方に切り替えます。

左下の常駐アプリのクジラのアイコンを右クリックします。

image.png

表示されるメニューで、Switch to Linux containers...を選択します(初めて表示する際などには少し時間がかかったりするようです。その場合には右クリック後少し放置します)。
もしSwitch to Windows containers...という表示になっていたら、もうLinux環境に切り替わっているので特に操作は不要です。

警告が出ますが、特に問題ないのでSwitchを選択します。

image.png

数秒待つと、Linux環境が有効化されます。

イメージってなに?

コンテナはイメージと呼ばれるものを使って起動されます。
停止している仮想マシン(もしくは停止しているコンテナ)のようなものとか、テンプレートのようなものに近いイメージになります。
どんな風なコンテナなの?という情報やデータが色々詰まっています。

公開されているイメージをダウンロードしてみる

自分でイメージを作ることもできますが、まずは他人が公開しているものを落としてきてみます。
Githubなどでリポジトリのファイルを落としてくるように、他人が公開しているものを落としてきたり、自分で公開したりすることができます。

コマンドもgitのコマンドのようなフォーマットになっています。
例えば、Ubuntuのイメージを落としてくるには以下のようなimage pullのコマンドで対応できます。

$ docker image pull ubuntu:latest
latest: Pulling from library/ubuntu
5c939e3a4d10: Pull complete
c63719cdbe7a: Pull complete
19a861ea6baf: Pull complete
651c9d2d6c4f: Pull complete
Digest: sha256:8d31dad0c58f552e890d68bbfb735588b6b820a46e459672d96e585871acc110
Status: Downloaded newer image for ubuntu:latest
docker.io/library/ubuntu:latest

ubuntuの箇所に落としたいイメージの名前、latestの部分はタグと呼ばれバージョンの指定などに使います。
(バージョン番号などの他に、必ずしも最新版を意味するものではありませんが、安定版の最新版などとしてのlatestという単語が良く使われます。)

ローカルに存在するイメージは、Linuxのlsコマンド(ディレクトリのファイル一覧などを表示するコマンド)と同じような感覚でイメージの一覧を確認することができます。

$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              latest              ccc6e87d482b        4 days ago          64.2MB

落としてきたイメージを起動してみる

pullしてきたUbuntuのイメージを使ってコンテナを起動してみます。
docker container runコマンドでコンテナを起動できます。

$ docker container run -it ubuntu:latest bash

ubuntu:latest部分は起動したいイメージ(と対象のタグ)を指定します。

末尾のbash部分は、Ubuntuで起動したいプログラムを指定します。
VirtualBoxなどでサーバー用Linuxを起動するとコンソールの黒い画面になりますが、それと同じように今回はUbuntu上でコマンドを実行できるようにしたいので、bashを指定しています。

-itの引数を指定すると、Windows上のコマンドプロンプトに対して起動したUbuntu上のbashを割り当てる指定となります(Ubuntu以外のLinuxだとbash以外が必要になったりする点にはご留意ください)。
割り当てる、と書くとなんだか分かりづらいですが、Linuxにログインした際のターミナルにWindows上のコマンドプロンプトが切り替わる、といった具合です。

コマンドプロンプトを見てみると、Ubuntuのrootユーザー(root@75ae40e15c7f:/#といったユーザー表示)に切り替わっているのが分かります(私は最初これに触れた際に、最初はなんだか結構不思議な感覚になりました)。

image.png

この状態では、Windows上で動かしているにも関わらず実際にUbuntu用のコマンドが実行できます。lsコマンドを実行してみると、Ubuntuで見慣れたディレクトリたちが表示されることが分かります。

root@75ae40e15c7f:/# ls
bin   dev  home  lib64  mnt  proc  run   srv  tmp  var
boot  etc  lib   media  opt  root  sbin  sys  usr

まるでUbuntuを起動してログインしたような感覚です。

また、コンテナのメリットとして、起動が速いという点も前述しましたが、私の環境で上記のコマンドでUbuntuのコンテナが立ち上がってbashの入力ができるようになるまでほんの数秒で移行できています。VirtualBoxやVMWareで起動するよりかはやっぱり大分速い・・・という印象です。
普段開発環境で毎日起動することを考えると、この速さはいいですね。

なお、-it引数は、-i-tの引数の組み合わせのようで、それぞれで以下のような設定のようです。

-iは、Keep STDIN open even if not attached
標準入力を開き続ける。

-tは、Allocate a pseudo-TTY
疑似ttyを割りあてる。

標準入力を開き続け、そこを操作出来るようにする。
→手元の環境で、docker内入力ができるようにする
docker exec -itって実際は何をしてるの?【90日目】

元のWindowsのコマンドプロンプトに戻る

Ubuntu上のbashから元のWindowsのコマンドプロンプトに戻りたい場合にはexitとコマンドを実行するか、Ctrl + P → Ctrl + Qとキーボードで入力すると戻れます。
exitコマンドを実行した場合には、条件によってはコンテナ自体が停止します(それしかプロセスが動いていない場合など?)。Ctrl + P → Ctrl + Qとキーボードを入力して戻った場合には、Ubuntuのコンテナは動いたままとなります。

サーバからログアウトする感覚でexitコマンドを実行すると、起動したコンテナも停止してしまうので注意してください。
Dockerコンテナの作成、起動〜停止まで

起動中のコンテナの一覧を確認する

イメージのlsコマンドと同様に、コンテナのlsコマンド(docker container ls)で起動中のコンテナの一覧を表示することができます。
試しにUbuntuのコンテナを起動して、exitではなくCtrl + P → Ctrl + Qのショートカット(起動したままbashを抜ける)を使って、そののちにlsコマンドで確認してみます。

$ docker container run -it ubuntu:latest bash

※Ctrl + P → Ctrl + Qを入力してWindowsのコマンドプロンプトに戻る。

$ docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED              STATUS              PORTS               NAMES
e8f5c7aadcb5        ubuntu:latest       "bash"              About a minute ago   Up About a minute                       xenodochial_hodgkin

先ほど起動したUbuntuのコンテナ情報が表示されました。

起動中のコンテナを停止する

停止するにはdocker container stop <コンテナ識別用のパラメーター>を指定します。

前述のdocker container lsコマンドで表示された、CONTAINER IDもしくはNAMESが指定できます。
NAMESの値は、コンテナ起動時に指定しなかった場合は自動で何らかの値が設定されます。

たとえば、先ほど起動したコンテナに対してIDを指定して停止する場合には以下のようになり、

$ docker container stop e8f5c7aadcb5

NAMESを指定して停止する場合には以下のようになります。

$ docker container stop xenodochial_hodgkin

停止処理後再度lsコマンドで起動中のコンテナを確認してみると、ヘッダーのみの表示で、起動中のコンテナが無くなっていることが確認できます。

$ docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

コンテナに名前を付けて起動する

特に名前を指定しなくてもコンテナを起動することはできますが、特定の名前を明示しておくとスクリプトなどで制御する際に便利です。
名前を指定するには--name <コンテナ名>と指定します。例えば、ubuntutestという名前をコンテナに指定して起動したい場合は以下のようになります。

$ docker container run --name ubuntutest -it ubuntu:latest bash

Ctrl + P, Ctrl + Qでコンテナを起動したままWindowsのコマンドプロンプトに戻って、lsコマンドで確認すると、NAMESの値に引数で指定したubuntutestという名前が設定されていることが分かります。

$ docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED              STATUS              PORTS               NAMES
9c1141a99d07        ubuntu:latest       "bash"              About a minute ago   Up About a minute                       ubuntutest

一旦、上記のコンテナは停止させて次に進みます。

$ docker container stop 9c1141a99d07

停止しているコンテナのリストを表示する

通常のdocker container lsコマンドでは起動しているコンテナしか表示されません。

$ docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

しかし、停止したコンテナはローカルから削除されるわけではありません。
-aのオプションを加えると停止済みのコンテナも表示することができます。

$ docker container ls -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                      PORTS               NAMES
9c1141a99d07        ubuntu:latest       "bash"              10 hours ago        Exited (0) 43 seconds ago                       ubuntutest
e8f5c7aadcb5        ubuntu:latest       "bash"              11 hours ago        Exited (0) 11 hours ago                         xenodochial_hodgkin
39c583048c7f        ubuntu:latest       "bash"              23 hours ago        Exited (0) 23 hours ago                         nervous_swanson
...

コンテナを取り除く

停止しているコンテナに対して、docker container rm <対象のコンテナのIDや名前>というフォーマットのコマンドで対象のコンテナを取り除くことができます。

$ docker container rm ubuntutest

lsコマンドで確認してみると、一番上にあったubuntutestという名前のコンテナ(ID=9c1141a99d07)が消えていることが確認できます。

CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                    PORTS               NAMES
e8f5c7aadcb5        ubuntu:latest       "bash"              11 hours ago        Exited (0) 11 hours ago                       xenodochial_hodgkin
39c583048c7f        ubuntu:latest       "bash"              23 hours ago        Exited (0) 23 hours ago                       nervous_swanson
...

なお、起動しているコンテナに関しては基本的に削除は実行できず、一度停止させる必要があります。試しにrunning_ubuntuという名前でコンテナを起動し、Ctrl + P → Ctrl + Qで起動したままコマンドプロンプトに戻り、削除を試してみます。

$ docker container run --name running_ubuntu -it ubuntu:latest bash

※Ctrl + P → Ctrl + Qを押す。

$ docker container rm running_ubuntu

以下のように怒られます。

Error response from daemon: You cannot remove a running container 95586a709576d33521072c1b40c2d84e5c3881a6d6468b485958e62f6d2a3440. Stop the container before attempting removal or force remove

-fオプションを追加すると一応強制的に取り除くことができます。ただ、基本的には停止 → 削除の流れに沿う形での対応が好ましそうな印象があります。

$ docker container rm -f running_ubuntu

同一名を指定してコンテナで複数回起動するとどうなるのか

名前(--name引数)を指定せずにコンテナを起動すると毎回別のコンテナが起動します。
では、名前を指定しつつ、複数回同じ名前で起動しようとするとどうなるのでしょうか?
same_name_ubuntuという名前で、コンテナを起動してみます。

$ docker container run --name same_name_ubuntu -it ubuntu:latest bash

Ctrl + P, Ctrl + Qで起動したままbashからWindowsのコマンドプロンプトに戻ります。
もう一度同じ名前を指定してコンテナを起動してみます。

$ docker container run --name same_name_ubuntu -it ubuntu:latest bash

そうすると以下のように、別のコンテナが立ち上がることは無くエラーで弾かれ、名前はユニークになっている必要があることが分かります。

docker: Error response from daemon: Conflict. The container name "/same_name_ubuntu" is already in use by container "9abe70cdbc98350f18b780880fd3b2ba23d0202b1cdb464c290dfe929924188b". You have to remove (or rename) that container to be able to reuse that name.
See 'docker run --help'.

停止しているコンテナを再開させる

停止しているコンテナを再度スタートさせるには、docker start <コンテナのIDもしくは名前>というフォーマットでコマンドを実行します。docker container runでも無いですし、docker container startでもなく、docker startというフォーマットになります。

一度試しに動いているコンテナを停止させます :

$ docker container stop same_name_ubuntu

スタートさせます :

$ docker start same_name_ubuntu

lsコマンドを実行すると、ちゃんと起動しているのが確認できます。

$ docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
9abe70cdbc98        ubuntu:latest       "bash"              35 minutes ago      Up 4 minutes                            same_name_ubuntu

なお、このコマンドではコンテナが起動するだけでbashとかに切り替わってくれたりはしません。

起動しているコンテナに対してもう一度bashで入るには?

Ctrl + P, Ctrl + Qとかで起動したままbashを抜けた場合や、docker startコマンドでコンテナを起動した後に、再度bashに切り替えたり、もしくは他の任意のコンテナ内のアプリを起動したい場合には、docker container execコマンドで実行できます。

$ docker container exec -it same_name_ubuntu bash

コマンドプロンプトの表示がroot@9abe70cdbc98:/#といったような表示になり、bashに切り替えられたのが確認できます。

イメージを削除する

イメージもコンテナと同様の感覚で削除できます。
CONTAINER IDの代わりにIMAGE IDという値がlsコマンドで確認できるため、そちらを指定します。

$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              latest              ccc6e87d482b        6 days ago          64.2MB

ただし、該当のイメージをいずれかのコンテナで利用していると弾かれます。

$ docker image rm ccc6e87d482b
Error response from daemon: conflict: unable to delete ccc6e87d482b (cannot be forced) - image is being used by running container 9abe70cdbc98

起動しているコンテナで使われていて削除できないよ、といったエラー内容ですが、コンテナを停止させてもエラーで怒られます。
試しに起動しているコンテナをlsコマンドで確認してから、そのコンテナを停止させてからもう一度削除コマンドを実行してみます。

$ docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
9abe70cdbc98        ubuntu:latest       "bash"              14 hours ago        Up 14 hours                             same_name_ubuntu
$ docker container stop 9abe70cdbc98
$ docker image rm ccc6e87d482b
Error response from daemon: conflict: unable to delete ccc6e87d482b (must be forced) - image is being used by stopped container 75ae40e15c7f

エラー内容がimage is being used by stopped containerとなり、停止しているコンテナで使われていても弾かれることが分かります。

コンテナと同様、-fオプションを付ければ一応は強制的に削除ができます。ただし、コンテナ側などに悪影響が出たりするので、実行時には要注意です。

$ docker image rm -f ccc6e87d482b
Untagged: ubuntu:latest
Untagged: ubuntu@sha256:8d31dad0c58f552e890d68bbfb735588b6b820a46e459672d96e585871acc110
Deleted: sha256:ccc6e87d482b79dd1645affd958479139486e47191dfe7a997c862d89cd8b4c0

なお、ID以外にも<イメージ名>:<タグ名>の指定で特定のイメージを削除することも可能です。

$ docker image rm ubuntu:latest

大元のイメージを削除してしまったりと、影響を受けたコンテナをこのまま残しておくのも微妙なので、次の節で一括で停止中のコンテナを削除しておきます。

停止中のコンテナを一括で削除する

利用は気を付ける必要がある(消したくないコンテナまで消してしまったりなど)感じではありますが、docker container pruneコマンドで停止中のコンテナを一括で削除できます。

lsコマンドで確認してみると、色々な停止中のコンテナが残っていることが分かります。

$ docker container ls -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                      PORTS               NAMES
9abe70cdbc98        ccc6e87d482b        "bash"              15 hours ago        Exited (0) 23 minutes ago                       same_name_ubuntu
e8f5c7aadcb5        ccc6e87d482b        "bash"              26 hours ago        Exited (0) 26 hours ago                         xenodochial_hodgkin
39c583048c7f        ccc6e87d482b        "bash"              38 hours ago        Exited (0) 38 hours ago                         nervous_swanson
...

pruneコマンドを実行してみます。

$ docker container prune

警告が出るのでyと入力してEnterを押します。

WARNING! This will remove all stopped containers.
Are you sure you want to continue? [y/N]
Deleted Containers:
9abe70cdbc98350f18b780880fd3b2ba23d0202b1cdb464c290dfe929924188b
e8f5c7aadcb54cbddb1b9857ffc6c9ae16d389023f094fd8fb52385ad1195c47
39c583048c7f63c425c5bcb1e88cbf10fa5a6e69f736e1f3db235678cbac089e
...

Total reclaimed space: 33B

lsコマンドで確認すると、ちゃんと削除されていることが分かります。

$ docker container ls -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

コンテナは停止させても内部のデータが削除されるわけではない

docker container stopでコンテナを停止させても、追加した内部のファイルなどのデータはそのまま残ります。
試しに、Ubuntuのコンテナを追加して、bashからファイルを追加し、停止したのちに再度起動してファイルが残っていることを確認してみます。

前節まででコンテナなどを一通り削除していたので、もう一度docker container runコマンドでコンテナを追加・起動します。今回はfile_persistency_testという前を付けました。
なお、イメージも削除済みですが、docker container runコマンドで、指定したイメージがローカルて見つからなければ同時にpullしてくれます。便利。

$ docker container run --name file_persistency_test -it ubuntu:latest bash
Unable to find image 'ubuntu:latest' locally
latest: Pulling from library/ubuntu
5c939e3a4d10: Pull complete
c63719cdbe7a: Pull complete
19a861ea6baf: Pull complete
651c9d2d6c4f: Pull complete
Digest: sha256:8d31dad0c58f552e890d68bbfb735588b6b820a46e459672d96e585871acc110
Status: Downloaded newer image for ubuntu:latest

Ubuntuのbashに切り替わるので、cdコマンドでtmpフォルダに移動して、確認用のファイルを保存してみます。

root@d289b867492e:/# cd tmp

test_file.txtというファイル名で書き込みます。

root@d289b867492e:/tmp# echo "吾輩は猫である" >> test_file.txt

catコマンドで、書き込んだ内容を確認してみます。

root@d289b867492e:/tmp# cat test_file.txt
吾輩は猫である

無事書き込めているようなので、一旦コンテナを停止させて再度起動します。
exitでそのままbashを抜けてstopさせてもいいのですが、起動プロセスの都合で止まらないことがあるので普通にCtrl + P, Ctrl + Qで抜けてからコマンドでstopさせます。

$ docker container stop file_persistency_test

もう一度起動します。

$ docker start file_persistency_test

bashに切り替えます。

$ docker container exec -it file_persistency_test bash

tmpフォルダに移動して、ファイルが残っていることを確認します。

root@d289b867492e:/# cd tmp
root@d289b867492e:/tmp# ls
test_file.txt
root@d289b867492e:/tmp# cat test_file.txt
吾輩は猫である

無事表示されました。コンテナを停止してもファイルが残ることが確認できました。

Docker Hubについて

説明無くイメージのpullなどを行っていましたが、これらのイメージはDocker HubというDocker公式のサーバーから落とされてきています。

Docker Hub

image.png

後で触れますが、他人のイメージをpullしたりする以外にも、自分でイメージをアップして公開することもできます。

Docker Hub以外のサーバーでもイメージが公開されていて、落としてきて利用することもできますが、基本的にはDocker Hubがメインとなります。
特殊なケースを除いて、基本的にはDocker Hubを使うことになります。

イメージを検索する

必要になったイメージを探したいときには、Googleでdocker hub <対象のイメージのキーワード>みたいな感じで探せば見つかると思います。

例えば、MongoDBのイメージを探したいときにはdocker hub mongodbといった具合です。

image.png

pull用のコマンドや、Tagsのタブを見ると各タグの情報やそのタグでのpullのコマンドを調べることができます。

image.png

その他にも、docker searchコマンドを使うことで、コマンドプロンプト上で検索することもできます。

$ docker search mongo
NAME                                DESCRIPTION                                     STARS               OFFICIAL            AUTOMATED
mongo                               MongoDB document databases provide high avai…   6519                [OK]
mongo-express                       Web-based MongoDB admin interface, written w…   600                 [OK]
tutum/mongodb                       MongoDB Docker image – listens in port 27017…   229                                     [OK]
bitnami/mongodb                     Bitnami MongoDB Docker Image                    108                                     [OK]
mongoclient/mongoclient             Official docker image for Mongoclient, featu…   79                                      [OK]
mongooseim/mongooseim               Small docker image for MongooseIM - robust a…   19
frodenas/mongodb                    A Docker Image for MongoDB                      18                                      [OK]
cvallance/mongo-k8s-sidecar         Kubernetes side car to setup and maintain a …   14                                      [OK]

STARSの数が多い、人気のイメージのリポジトリから降順で25件程度表示されます。

もしそれ以上の件数を確認したい場合には、--limitのオプションを指定することで、最大100件まで表示することができます。

$ docker search --limit 50 mongo

公式と非公式のイメージ(リポジトリ)について

先ほどのdocker searchコマンドでは、検索結果にOFFICIALというカラムがありました。

NAME                      ...     OFFICIAL            AUTOMATED
mongo                     ...     [OK]
mongo-express             ...     [OK]
tutum/mongodb             ...                         [OK]
bitnami/mongodb           ...                         [OK]
mongoclient/mongoclient   ...                         [OK]

[OK]と付いているリポジトリと空になっているリポジトリがあることが分かります。
これは、Docker側で公式として扱われているリポジトリかどうかの判定として利用することができます。
[OK]と付いていないリポジトリは、個人だったり公式とは関係無い第三者によるリポジトリとなります。

それらは便利なケースも多くある一方で、ものによってはアップデートが止まっていたり、脆弱性が放置されていたり、バグが残ったままになっていたりするかもしれません。
利用する場合には自己責任となります。
もちろん公式でもそういった脆弱性が見つかることもあり、公式だからといって安全というものでもないですが、そういった際にちゃんとアップデートがされるかどうかなどで差が出る可能性があります。

公式のリポジトリだけ検索する

--filter "is-official=true"と引数を加えると、公式リポジトリのみに検索を制限したりすることができます。

$ docker search mongo --filter "is-official=true"
NAME                DESCRIPTION                                     STARS               OFFICIAL            AUTOMATED
mongo               MongoDB document databases provide high avai…   6519                [OK]
mongo-express       Web-based MongoDB admin interface, written w…   600                 [OK]

イメージの詳細をinspectコマンドで確認する

docker image inspect <イメージ名:タグ もしくはID>のフォーマットのコマンドで、対象のイメージの詳細を確認することができます。

表示される情報は、設定されているタグやユニーク判定用のハッシュ値、生成日時や設定値、実行されるコマンド、OS、サイズ、ネットワーク情報、マウント(ファイルシステム)など様々です。

$ docker image inspect ubuntu:latest
[
    {
        "Id": "sha256:ccc6e87d482b79dd1645affd958479139486e47191dfe7a997c862d89cd8b4c0",
        "RepoTags": [
            "ubuntu:latest"
        ],
        "RepoDigests": [
            "ubuntu@sha256:8d31dad0c58f552e890d68bbfb735588b6b820a46e459672d96e585871acc110"
        ],
        "Parent": "",
        "Comment": "",
        "Created": "2020-01-16T01:20:34.28326882Z",
        "Container": "8fadb7780c5426c7a294cc9fdb50294da831d550a3ecaf8382737d6b141b3054",
        "ContainerConfig": {
            "Hostname": "8fadb7780c54",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
            ],
            "Cmd": [
                "/bin/sh",
                "-c",
                "#(nop) ",
                "CMD [\"/bin/bash\"]"
            ],
            "ArgsEscaped": true,
            "Image": "sha256:d73776eba985c9fbd43227fbf32eeb85baf88775dc0c27db7edb0c8ddeb72b26",
            "Volumes": null,
            "WorkingDir": "",
            "Entrypoint": null,
            "OnBuild": null,
            "Labels": {}
        },
        "DockerVersion": "18.06.1-ce",
        "Author": "",
        "Config": {
            "Hostname": "",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
            ],
            "Cmd": [
                "/bin/bash"
            ],
            "ArgsEscaped": true,
            "Image": "sha256:d73776eba985c9fbd43227fbf32eeb85baf88775dc0c27db7edb0c8ddeb72b26",
            "Volumes": null,
            "WorkingDir": "",
            "Entrypoint": null,
            "OnBuild": null,
            "Labels": null
        },
        "Architecture": "amd64",
        "Os": "linux",
        "Size": 64194748,
        "VirtualSize": 64194748,
        "GraphDriver": {
            "Data": {
                "LowerDir": "/var/lib/docker/overlay2/f03c69f5d7637556772ae1b2c650e1872f8a574db88e45f3911315729db0a59c/diff:/var/lib/docker/overlay2/1c79f0978277afcdf1c118571885a43fe9be6d979eeaafd5b27196e757628da0/diff:/var/lib/docker/overlay2/0109eef00cd0de560fdbdf6c680e1cf89511736fb8b24180c9ef889e37611e59/diff",
                "MergedDir": "/var/lib/docker/overlay2/e902d5b0ebae48707cca044bdbf7b1397c560bd58b397c841e426b20de6f5333/merged",
                "UpperDir": "/var/lib/docker/overlay2/e902d5b0ebae48707cca044bdbf7b1397c560bd58b397c841e426b20de6f5333/diff",
                "WorkDir": "/var/lib/docker/overlay2/e902d5b0ebae48707cca044bdbf7b1397c560bd58b397c841e426b20de6f5333/work"
            },
            "Name": "overlay2"
        },
        "RootFS": {
            "Type": "layers",
            "Layers": [
                "sha256:43c67172d1d182ca5460fc962f8f053f33028e0a3a1d423e05d91b532429e73d",
                "sha256:21ec61b65b20ec53a1b7f069fd04df5acb0e75434bd3603c88467c8bfc80d9c6",
                "sha256:1d0dfb259f6a31f95efcba61f0a3afa318448890610c7d9a64dc4e95f9add843",
                "sha256:f55aa0bd26b801374773c103bed4479865d0e37435b848cb39d164ccb2c3ba51"
            ]
        },
        "Metadata": {
            "LastTagTime": "0001-01-01T00:00:00Z"
        }
    }
]

イメージのレイヤーの話

各イメージは、複数のレイヤーという要素で構成されています。
各イメージでレイヤーが何個で構成されているかは前述のdocker image inspectコマンドで確認することができます。

Ubuntuのイメージで確認してみると、以下のように4つのレイヤーで構成されていることが分かります(バージョンによってレイヤー数はある程度前後します)。

$ docker image inspect ubuntu:latest
...
        "RootFS": {
            "Type": "layers",
            "Layers": [
                "sha256:43c67172d1d182ca5460fc962f8f053f33028e0a3a1d423e05d91b532429e73d",
                "sha256:21ec61b65b20ec53a1b7f069fd04df5acb0e75434bd3603c88467c8bfc80d9c6",
                "sha256:1d0dfb259f6a31f95efcba61f0a3afa318448890610c7d9a64dc4e95f9add843",
                "sha256:f55aa0bd26b801374773c103bed4479865d0e37435b848cb39d164ccb2c3ba51"
            ]
        },
...

例えば自分でイメージを作る際に、Linuxのイメージに加えて、Python関係を色々入れたり・・・と、要素を追加していくとレイヤーがどんどん増えていきます。
一方で、メタデータのみの追加であれば特にレイヤーは追加されません(詳細は後述しますが、例えばDockerfileにて指定する、コンテナ起動時に実行して欲しいコマンドの指定のためのCMDの設定などは、メタデータの追加のみのためレイヤーは追加されません)。

なぜレイヤーが分かれているのか

いくつか理由がありますが、大きな点の1つとしてイメージをpullする際に、すでにローカルに該当のイメージで必要な特定のレイヤーが存在すれば、そのレイヤーのダウンロードはスキップされ、処理時間が短縮されるというメリットがあります。

たとえば、特定のイメージでレイヤーを4つ使っているとします。次のマイナーバージョンが公開され、その際に4つのレイヤーのうち1つが更新されているとします。
ユーザーがアップデートのためpullすると、新バージョンのイメージ全体のダウンロードが実行されるのではなく、更新された1レイヤーのみがダウンロードされ、該当バージョンのイメージが利用可能になります。
この複数のイメージ間でレイヤーが共有されることによって、ローカルのディスクサイズの節約やダウンロード時間の節約に繋がります。

この辺りはpullコマンドの際に確認することができます。
ダウンロードがレイヤーごとに実行されるため、すでにローカルに該当のレイヤーが存在すればAlready existsという表示になり、pullがスキップされていることが確認できます。

$ docker image pull mongo:4.2.2
5c939e3a4d10: Already exists
c63719cdbe7a: Already exists
19a861ea6baf: Already exists
651c9d2d6c4f: Already exists
85155c6d5fac: Pull complete
85fb0780fd97: Pull complete
85b3b1a901f5: Pull complete
6a882e007bb6: Pull complete
f7806503a70f: Pull complete
e23d5068c270: Pull complete
56eb708963d7: Pull complete
fc4def32f081: Pull complete
ea1dc19faea9: Pull complete
Digest: sha256:a1a71d8659ef2f944469f9eb876a44b7ae7c05153f356c18c165fd485fc60f2b
Status: Downloaded newer image for mongo:4.2.2
docker.io/library/mongo:4.2.2

コンテナの自動の再起動設定(restart policy)

コンテナが何らかの要因で終了してしまったり、Docker自体を再起動した際に自動で再起動するようにする機能があります。
なんらかの要因でプロセスが死んでしまって、手動で再起動したりせずに済むようになります。

設定はコンテナごとに行えます。docker container runコマンドに--restartオプションを指定することで設定ができます。

--restartオプションには以下のいずれかの値を指定します。

  • always: 一通りの条件でコンテナが再起動するようになる。
  • on-failure: コンテナのプロセスがエラーで終了した場合や、Docker再起動時などに正常に終了しなかった場合などに再起動するようになる。
  • unless-stopped: 明示的にstopコマンドでコンテナが停止している場合にDockerを再起動などしてもコンテナは再起動しない。それ以外はalwaysと同様大体再起動する。

※明示的に対象のコンテナをstopコマンドで手動で止めた場合にはalwaysでも再起動しません(そこで再起動してしまうと止めれなくなりますしね・・・)。ただし、停止させている状態でDocker自体を再起動した際などには該当コンテナも再起動します。

実際に試してみます。実験のため、一旦起動しているコンテナを全て停止し、lsコマンドの結果が空になるようにしておきます(起動中のものがあればdocker container stopコマンドで止めておきます)。

$ docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

まずは--restart alwaysから試していきます。
always_restart_ubuntuという名前で進めていきます。

$ docker container run -it --restart always --name always_restart_ubuntu ubuntu:latest bash

Ctrl + P, Ctrl + Qで起動したままbashを抜けます。
lsコマンドで、該当のコンテナが起動していることを確認します。

$ docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
eb83d8b715ed        ubuntu:latest       "bash"              8 seconds ago       Up 7 seconds                            always_restart_ubuntu

Dockerを再起動してみて、該当のコンテナも再起動してくれることを確認します。
Dockerの再起動は左下の常駐アプリのクジラのアイコンを右クリックした際に表示されるコンテキストメニューの「Restart...」から実行できます。

image.png

image.png

警告が出ますが特に問題ないのでそのまま進めます。

image.png

再起動中、クジラのアイコンにマウスオーバーすると、再起動中ですよ、と表示されます。少し待つとDockerの再起動が完了します。

image.png

lsコマンドで起動中のコンテナを表示してみます。

$ docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
eb83d8b715ed        ubuntu:latest       "bash"              7 minutes ago       Up About a minute                       always_restart_ubuntu

該当の--restart設定をしていたコンテナが無事起動していることが分かります。
また、CREATEDが約7分前になっている一方で、STATUSの方が約1分前の起動と表示されていることから、Docker再起動と共に停止され、Dockerに合わせてコンテナも新たに起動したことが分かります。

Dockerfileとは

Dockerfileとは、コンテナのイメージを作るときの指示書のようなものです。
自分でイメージを作るときに「こんな感じのコンテナのイメージにしてね」とDockerに伝えるために使用します。
決められたフォーマットがあるのでそちらに準じて書いていった後に、docker image buildコマンドでイメージを作ることができます。

自分でイメージを作って(ビルドして)みる

自分でDockerfileを用意してみて、buildコマンドでビルドしてみます。
最初なので、最小限の構成で、Ubuntuのみ指定する形でまずは進めてみます。

任意の空のフォルダを用意します。今回はbuild_test_1という名前で進めます。
そちらのフォルダにDockerfileという名前のファイルを追加します(拡張子は設定しません)。

build_test_1\Dockerfile
FROM ubuntu:18.04

FROMは、ベースとなるイメージの指定です。OSのイメージなど、基本的なものを指定します(今回はUbuntuを指定しています)。
Dockerfileの先頭に記述します。

また、タグはlatestだと将来のビルドタイミングで以前と異なるバージョンのUbuntuになってしまうので、固定のバージョンを指定しています。

なお、Dockerのベストプラクティスの資料では、公式のイメージのリポジトリをFROMに指定することが推奨されています。

可能であれば、自分のイメージの元として現在の公式レポジトリを使います。私たちは Debian イメージ を推奨します。これは、非常にしっかりと管理されており、ディストリビューションの中でも最小(現在は 100 MB 以下)になるよう維持されているからです。
Dockerfile のベストプラクティス

ビルド実行の前に、Windowsのコマンドプロンプトで、このDockerファイルが設置されているフォルダに移動します。

$ cd build_test_1

dirコマンド(Windowsにおけるlsコマンドに近いコマンド)でファイルを一応確認しておきます。

$ dir
2020/01/22  17:34                19 Dockerfile

ビルドはdocker image build -t <イメージに付ける名前>:<タグ名> <Dockerfileが設置されているパス>のフォーマットのコマンドで実行します。

今回は、イメージ名はubuntu_build_test_1という名前にし、タグ名は0.0.1とします。
Dockerfileが設置されているパスに関してはDockerfile設置ディレクトリにまで移動済みなので、直下のパスを指定します。

$ docker image build -t ubuntu_build_test_1:1.0.0 ./

エラー無く終了しました。

Sending build context to Docker daemon  2.048kB
Step 1/1 : FROM ubuntu:18.04
18.04: Pulling from library/ubuntu
Digest: sha256:8d31dad0c58f552e890d68bbfb735588b6b820a46e459672d96e585871acc110
Status: Downloaded newer image for ubuntu:18.04
 ---> ccc6e87d482b
Successfully built ccc6e87d482b
Successfully tagged ubuntu_build_test_1:1.0.0

ただ、最後に警告が出ています。

SECURITY WARNING: You are building a Docker image from Windows against a non-Windows Docker host. All files and directories added to build context will have '-rwxr-xr-x' permissions. It is recommended to double check and reset permissions for sensitive files and directories.

ふむ・・・WindowsでLinuxをビルドすると、すべてのファイルのパーミッションが固定の値になるようで。センシティブなファイルやディレクトリはちゃんとパーミッション変えておいてね、といった具合のようです。
実務で使う際には要チェックですね。
今回はお試しなので一旦そのまま進めます。

lsコマンドを実行してみると、自分でビルドしたイメージがちゃんと追加になっていることが分かります。

$ docker image ls
REPOSITORY            TAG                 IMAGE ID            CREATED             SIZE
...
ubuntu_build_test_1   1.0.0               ccc6e87d482b        6 days ago          64.2MB

もちろん、今まで試してきた各dockerのコマンドも使えます。
以下のように、起動することができます。

$ docker container run -it ubuntu_build_test_1:1.0.0 bash

DockerfileにPythonのインストール関係を追記する

前節でビルドしたUbuntuのイメージでPythonコマンドを実行してみると、Pythonが入っていないことが分かります。

root@762ce425ca58:/# python -V
bash: python: command not found

これは、恐らく「各コンテナは無駄をそぎ落として小さくしておくべし」的なベストプラクティスに準じているのかな、という印象です。その分、公式のUbuntuなどのイメージは大分小さくなっています。

Dockerfile で定義されたイメージを使って作成されるコンテナは、可能ならばエフェメラル(短命;ephemeral)にすべきです。私たちの「エフェメラル」とは、停止・破棄可能であり、明らかに最小のセットアップで構築して使えることを意味します。
...
複雑さ、依存関係、ファイルサイズ、構築階数をそれぞれ減らすために、余分ないし不必要な「入れた方ほうが良いだろう」というパッケージは、インストールを避けるべきです。
Dockerfile のベストプラクティス

そこで、Python関係のインストールをUbuntuのイメージに追加してみたいと思います。実務の場合にはpipenvやらpoetryとか色々絡んでくると思いますが、Dockerの記事なので今回はその辺りは割愛します。

後でDockerfileに記載しますが、以下のようなコマンドが最低限必要になります。Dockerfileに記載する前に、直接1つずつ実行してみて、コマンドが通ることを確認してみます。

[1]. Ubuntuでインストール可能なパッケージの一覧を更新する

apt-get installコマンドなどでPythonをインストールする際に、事前に1回実行が必要になります。

root@762ce425ca58:/# apt-get update
Get:1 http://security.ubuntu.com/ubuntu bionic-security InRelease [88.7 kB]
Get:2 http://archive.ubuntu.com/ubuntu bionic InRelease [242 kB]
Get:3 http://security.ubuntu.com/ubuntu bionic-security/restricted amd64 Packages [23.7 kB]
...
Get:17 http://archive.ubuntu.com/ubuntu bionic-backports/main amd64 Packages [2496 B]
Get:18 http://archive.ubuntu.com/ubuntu bionic-backports/universe amd64 Packages [4241 B]
Fetched 17.5 MB in 58s (303 kB/s)
Reading package lists... Done

[2]. Pythonインストール用のコマンドを実行する

-yの指定が無いとインストールする?しない?と聞かれてしまうので、Dockerfileに移すときのことを考えて-yのオプションを追加しておきます。結構時間がかかるので実行後少し放置します。

root@762ce425ca58:/# apt-get install -y python3.7 python3.7-dev
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following additional packages will be installed:
  file libc-dev-bin libc6-dev libexpat1 libexpat1-dev libmagic-mgc libmagic1 libmpdec2 libpython3.7 libpython3.7-dev libpython3.7-minimal
  libpython3.7-stdlib libreadline7 libsqlite3-0 libssl1.1 linux-libc-dev manpages manpages-dev mime-support python3.7-minimal readline-common xz-utils
  zlib1g-dev
...
Setting up python3.7 (3.7.5-2~18.04) ...
Setting up libpython3.7:amd64 (3.7.5-2~18.04) ...
Setting up libpython3.7-dev:amd64 (3.7.5-2~18.04) ...
Setting up python3.7-dev (3.7.5-2~18.04) ...
Processing triggers for libc-bin (2.27-3ubuntu1) ...

特にエラーなく通ったようです。
正常に動いていることを確認するため、バージョンを確認したり、Pythonのインタラクティブシェルでprintだけ動かしてみます。

root@762ce425ca58:/# python3.7 -V
Python 3.7.5
root@762ce425ca58:/# python3.7
Python 3.7.5 (default, Nov  7 2019, 10:50:52)
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> print('Hello Python!')
Hello Python!

動いてますね。問題なさそうです。
前述までのコマンドをDockerfileに記載していきます。
Dockerfile内でコマンドを指定するには先頭にRUNと記載します。
複数のコマンドを記載するには、複数のRUNを書くか、&&で繋げられるようです。&&で繋げた場合、1行が長くなるので適宜見やすいようにバックスラッシュ(\)などで改行を加えます。

※複数のRUNを記載する方法に関して、RUNを書くたびにイメージのレイヤーが増えるのと、ベストプラクティスではレイヤー数を最小にするように、との記載があります。RUNを複数書いた方がある程度読みやすくなるケースが出そうですが、レイヤー数が多くなりすぎると好ましくない点が出てきます。

レイヤの数を最小に
Dockerfile の読みやすさと、使用するイメージレイヤーの数を最小化。両者のバランスを見つける必要があります。戦略的に注意深くレイヤー数を使います。
Dockerfile のベストプラクティス

 

RUNはなるべくまとめる
RUNでも不要なイメージのレイヤーが作成されることを防ぐため、なるべくひとつのRUN文に必要な処理を書いてしまいます。
Dockerfileを書くときに気をつけていること10選

軽く調べてみた感じ、レイヤー数が多くなると、パフォーマンスと容量を消費する、といった感じでしょうか。

レイヤ数の削減(最小化)
• Dockerの古いバージョンでは、確実に性能を出すため イメージ・レイヤ数の最小化が非常に重要だった
• 現在は、RUN COPY ADD 命令時のみレイヤを作成し、容量を消費
Dockerfile を書くためのベストプラクティス解説編 33ページより引用。

今回は&&を使う形で記載していってみます。

build_test_1\Dockerfile
FROM ubuntu:18.04

RUN apt-get update \
    && apt-get install -y \
    python3.7 \
    python3.7-dev

ビルドしています。タグのバージョン部分を1.0.0から1.0.1に切り替えています。

$ docker image build -t ubuntu_build_test_1:1.0.1 ./

なんだか若干の警告が出ましたが、今回は問題ないだろうと判断して進めます。

...
debconf: delaying package configuration, since apt-utils is not installed
...
update-alternatives: warning: skip creation of /usr/share/man/man1/lzma.1.gz because associated file /usr/share/man/man1/xz.1.gz (of link group lzma) doesn't exist
...
Successfully built e82c7cfa3b67
Successfully tagged ubuntu_build_test_1:1.0.1

起動してPythonが使えるか確認してみましょう。こちらもタグの部分を1.0.1と切り替える必要があるので注意してください。

$ docker container run -it ubuntu_build_test_1:1.0.1 bash
root@2902e7eb95df:/# python3.7 -V
Python 3.7.5

ちゃんと入っています。簡単で素晴らしいですね・・・。

pythonのエイリアスをDockerfileに設定する

このままだとPythonを動かしたりする際にpythonではなくpython3.7と入力する必要があります。
入力が増えたり、バージョンを変えたときにアプリのコマンドも変更しないとなので、pythonと入力したらインストールされているPythonが起動するようにしておきます。Linuxのエイリアスを使います。

まずは今まで同様に、Dockerfileに追記する前に、bash上で動作を確認していきます。
単純にaliasのコマンドを実行しただけではコンテナをスタートさせた際に有効になってくれないので、bashrcに追記する形を考えます。
且つ、一度停止して再度スタートさせてみたりしても問題ないことを確認するため、一度ubuntu_alias_testという名前をコンテナに付けて起動しておきます。

$ docker container run -it --name ubuntu_alias_test ubuntu_build_test_1:1.0.1 bash
root@c2f056bf6daa:/# echo 'alias python="/usr/bin/python3.7"' >> /root/.bashrc
root@c2f056bf6daa:/# source /root/.bashrc

python3.7ではなくpythonでコマンドが実行できることを確認します。

root@c2f056bf6daa:/# python -V
Python 3.7.5

大丈夫そうです。
一度、containerを停止させてから、再度起動してみてもエイリアスが効いていることを確認します。
exitコマンドでbashを抜けると共に、containerを停止させます。

root@c2f056bf6daa:/# exit

再度起動してbashに切り替えます。

$ docker start ubuntu_alias_test
$ docker container exec -it ubuntu_alias_test bash

エイリアスが効いているか確認します。

root@c2f056bf6daa:/# python -V
Python 3.7.5

大丈夫そうなので、Dockerfileに前述のコマンドを追記していきます。

build_test_1\Dockerfile
FROM ubuntu:18.04

RUN apt-get update \
    && apt-get install -y \
    python3.7 \
    python3.7-dev \
    && echo 'alias python="/usr/bin/python3.7"' >> /root/.bashrc

タグのバージョンを上げてビルドしてみます。

$ docker image build -t ubuntu_build_test_1:1.0.2 ./

ビルドしたイメージで起動します。

$ docker container run -it ubuntu_build_test_1:1.0.2 bash

aliasが効いているか確認します。

root@50c41d6289ca:/# python -V
Python 3.7.5

大丈夫そうです。

タグについてもう少し詳しく

タグは、一つのイメージに対して複数設定することができます。
ただし、同じタグを複数のイメージに付けることはできません。例えば、latestというタグを複数のバージョンのイメージに付けたりはできません。

少し試してみます。
前述までの処理でビルドされた、ubuntu_build_test_1:1.0.2というイメージとubuntu_build_test_1:1.0.1という二つのイメージを使っていきます。

タグを追加するにはdocker tag <イメージID> <新しいイメージ名>:<追加するタグ名>というフォーマットでコマンドを実行します。イメージ名を変える必要が無い場合は<新しいイメージ名>部分はそのままの値を指定します。

lsコマンドでイメージIDを調べます。

$ docker image ls
REPOSITORY            TAG                 IMAGE ID            CREATED             SIZE
ubuntu_build_test_1   1.0.2               f2b89ec36f84        14 minutes ago      241MB
ubuntu_build_test_1   1.0.1               e82c7cfa3b67        12 hours ago        241MB

試しに、1.0.1のタグが付いている方のイメージにlatestというタグを追加してみます。

$ docker tag e82c7cfa3b67 ubuntu_build_test_1:latest

もう一度lsコマンドで内容を確認してみます。

$ docker image ls
REPOSITORY            TAG                 IMAGE ID            CREATED             SIZE
ubuntu_build_test_1   1.0.2               f2b89ec36f84        17 minutes ago      241MB
ubuntu_build_test_1   1.0.1               e82c7cfa3b67        12 hours ago        241MB
ubuntu_build_test_1   latest              e82c7cfa3b67        12 hours ago        241MB

IMAGE IDの列を見ると同じIDのイメージが2行あり、1つのイメージに複数のタグが設定できていることが分かります。

続いて、追加で1.0.2のタグが設定されているイメージの方にlatestのタグを追加するコマンドを実行するとどうなるでしょう?試してみます。

$ docker tag f2b89ec36f84 ubuntu_build_test_1:latest
$ docker image ls
ubuntu_build_test_1   1.0.2               f2b89ec36f84        13 hours ago        241MB
ubuntu_build_test_1   latest              f2b89ec36f84        13 hours ago        241MB
ubuntu_build_test_1   1.0.1               e82c7cfa3b67        24 hours ago        241MB

1.0.1の方のイメージに付けていたlatestのタグが剥がされていることが分かります。このように同一のタグは複数のイメージに設定できないため、古い方のイメージに設定されているタグは無くなります。

イメージからタグを削除する

イメージからタグを削除するにはdocker rmi <イメージ名>:<タグ名>のフォーマットのコマンドを利用します。

latestタグを消してみます。

$ docker rmi ubuntu_build_test_1:latest
$ docker image ls
REPOSITORY            TAG                 IMAGE ID            CREATED             SIZE
ubuntu_build_test_1   1.0.2               f2b89ec36f84        13 hours ago        241MB
ubuntu_build_test_1   1.0.1               e82c7cfa3b67        24 hours ago        241MB

1.0.2のイメージに付いていたlatestタグが消えました。

イメージにタグが1つも設定されていないとどうなるのか

先ほどは2つタグの付いているイメージから1つタグを取り除きましたが、1つしかタグを持っていないイメージからタグを取り除くとどうなるのでしょう?こちらも試してみます。

1.0.1のタグを消してみます。

$ docker rmi ubuntu_build_test_1:1.0.1
Untagged: ubuntu_build_test_1:1.0.1
Deleted: sha256:e82c7cfa3b67a7c1354c7640ce53e0000a1d500176a63d2e86270581c3efbe46
Deleted: sha256:cb5593f33928c36ae1b90ce2fb1c469dbeb49617b100a7ddebf7bd164f7dbacf

どうなったのか確認してみます。

$ docker image ls
REPOSITORY            TAG                 IMAGE ID            CREATED             SIZE
ubuntu_build_test_1   1.0.2               f2b89ec36f84        24 hours ago        241MB

rmiコマンドの場合、イメージごと消えたようです。

では、latestタグだけ設定されているイメージで、別のイメージにlatestを設定した場合(latestタグが剥がされるケース)ではどうなるのでしょうか。
試しに、別途イメージをビルドしてみて試してみます。
Dockerfileの内容が一緒のままだと、1.0.2と同じイメージとして扱われてしまうので、一時的に末尾に意味の無いコマンドを追加してみます。

build_test_1\Dockerfile
FROM ubuntu:18.04

RUN apt-get update \
    && apt-get install -y \
    python3.7 \
    python3.7-dev \
    && echo 'alias python="/usr/bin/python3.7"' >> /root/.bashrc \
    && echo 'temporary'

latestタグを付けてビルドします。

$ docker image build -t ubuntu_build_test_1:latest ./

1.0.2とは別のイメージになっていることを、IMAGE IDで確認します。

$ docker image ls
REPOSITORY            TAG                 IMAGE ID            CREATED             SIZE
ubuntu_build_test_1   latest              734018f9903b        44 seconds ago      241MB
ubuntu_build_test_1   1.0.2               f2b89ec36f84        24 hours ago        241MB

1.0.2側にlatestタグを設定してみます。

$ docker tag f2b89ec36f84 ubuntu_build_test_1:latest

どうなったか確認します。

$ docker image ls
REPOSITORY            TAG                 IMAGE ID            CREATED             SIZE
<none>                <none>              734018f9903b        8 minutes ago       241MB
ubuntu_build_test_1   1.0.2               f2b89ec36f84        24 hours ago        241MB
ubuntu_build_test_1   latest              f2b89ec36f84        24 hours ago        241MB

今度はタグやリポジトリの表示が<none>となりました。
他のイメージにタグが移ってタグが0件になった場合には、このように削除ではなく<none>表示となります。

pipをDockerfileに追加する

少しPythonライブラリ関係も試してみようと思いますので、pip関係を追加していこうと思います。

今まで同様、コンテナで直接コマンドを打って通るかどうか確認します。
また、インストール順などでぼちぼち躓いたので、Ubuntuのイメージから順番に進めて検証します(この辺り、古い情報だとpip側で色々更新されていて躓きますね・・・)。

※参考にさせていただきました:bow: : Python3.7 + Pipenv環境をUbuntu18.04 LTSに構築

$ docker container run -it ubuntu:18.04  bash
root@d6c0ea41779a:/# apt-get update
root@d6c0ea41779a:/# apt-get install -y python3.7-dev
root@d6c0ea41779a:/# apt-get install -y python3.7
root@d6c0ea41779a:/# apt-get install -y python3-distutils
root@d6c0ea41779a:/# apt-get install -y wget
root@d6c0ea41779a:/# echo 'alias python="/usr/bin/python3.7"' >> /root/.bashrc
root@d6c0ea41779a:/# cd tmp/
root@d6c0ea41779a:/# wget https://bootstrap.pypa.io/get-pip.py
root@d6c0ea41779a:/# python3.7 get-pip.py

変更点として、

  • get-pip.pyを経由するようにしたのでwgetなどを先にインストールした。
  • python3-distutilsが無いとpipインストールで怒られるのでPython3.7インストールの後にインストールした(先だと別のPythonバージョンのみにインストールされたりと、pipインストール時に弾かれた)。

といったところです。
ビルドしてみます。
コマンドで実行してみて問題なさそうなので、Dockerfileを編集します。

build_test_1\Dockerfile
FROM ubuntu:18.04

RUN apt-get update \
    && apt-get install -y \
    python3.7-dev \
    python3.7 \
    python3-distutils \
    wget \
    && echo 'alias python="/usr/bin/python3.7"' >> /root/.bashrc \
    && cd tmp / \
    && wget https://bootstrap.pypa.io/get-pip.py \
    && python3.7 get-pip.py

$ docker image build -t ubuntu_build_test_1:1.0.3 ./

ビルド自体は問題なく通ったようです。ただ、過程で警告が出ています。

WARNING: pip is being invoked by an old script wrapper. This will fail in a future version of pip.
Please see https://github.com/pypa/pip/issues/5599 for advice on fixing the underlying issue.
To avoid this problem you can invoke Python with '-m pip' instead of running pip directly.

該当のリンク見てみましたが、なんとも今回のケースで解決できなさそう(そもそもget-pip.py経由なのに-m pipしてくれという感じだと解決できなさそう)なのと、ネットで調べている感じほとんど情報がヒットしないので、一旦このまま進めます・・・(なんだか将来のアップデートで自動的によしなになりそうな気がする)

起動して、pipコマンドを試してみます。

$ docker container run -it ubuntu_build_test_1:1.0.3  bash
root@c1beaf56d656:/# pip -V
pip 20.0.2 from /usr/local/lib/python3.7/dist-packages/pip (python 3.7)

無事pipが入ったようです。少々、この辺りまだまだ洗練されてシンプルになるといいな・・・とは思いました(エラーやら警告やら、新旧情報が色々あって迷う点など・・・)。
(毎年のようにPython環境構築記事が出ますが、似たような感じでまだまだ改善途中という印象が・・・)

イメージのビルドのキャッシュについて考える

DockerfileのRUN部分はまとめたほうがいいと先ほどは記述しました。
ただし、分けたほうが好ましいケースがあり、そのうちの一つにビルドのキャッシュ面があります。

イメージのビルド処理では、Dockerfileの記述が上から順番に実行され、もし途中まで以前実行したビルドと同じ内容であればそこはキャッシュが参照され、ビルドの短縮化が実行されます。

一方で、RUNコマンドをまとめていると、そこに新たに記述を追加したりするとそのRUNコマンドの一通りの実行が必要になってしまいます。
そのため、よく更新されるものなどはDockerfile内で下の方に、且つRUNコマンドなどは分ける形で記述しておくと日々のビルドが短くなります。

試しに、さきほど更新したDockerfileの最後に、別のRUNコマンドでライブラリ(Django)をインストールする記述を追加します(今回はrequirement.txtなどでのライブラリ管理はスキップします)。

FROM ubuntu:18.04

RUN apt-get update \
    && apt-get install -y \
    python3.7-dev \
    python3.7 \
    python3-distutils \
    wget \
    && echo 'alias python="/usr/bin/python3.7"' >> /root/.bashrc \
    && cd tmp / \
    && wget https://bootstrap.pypa.io/get-pip.py \
    && python3.7 get-pip.py

RUN pip install \
    Django==3.0.2

ビルドしてみます。

$ docker image build -t ubuntu_build_test_1:1.0.4 ./
Sending build context to Docker daemon  2.048kB
Step 1/3 : FROM ubuntu:18.04
 ---> ccc6e87d482b
Step 2/3 : RUN apt-get update     && apt-get install -y     python3.7-dev     python3.7     python3-distutils     wget     && echo 'alias python="/usr/bin/python3.7"' >> /root/.bashrc     && cd tmp /     && wget https://bootstrap.pypa.io/get-pip.py     && python3.7 get-pip.py
 ---> Using cache
 ---> 09019f05baa6
Step 3/3 : RUN pip install     Django==3.0.2
 ---> Running in e485f302a7f7
Collecting Django==3.0.2
  Downloading Django-3.0.2-py3-none-any.whl (7.4 MB)
Collecting asgiref~=3.2
  Downloading asgiref-3.2.3-py2.py3-none-any.whl (18 kB)
Collecting pytz
  Downloading pytz-2019.3-py2.py3-none-any.whl (509 kB)
Collecting sqlparse>=0.2.2
  Downloading sqlparse-0.3.0-py2.py3-none-any.whl (39 kB)
Installing collected packages: asgiref, pytz, sqlparse, Django
Successfully installed Django-3.0.2 asgiref-3.2.3 pytz-2019.3 sqlparse-0.3.0
Removing intermediate container e485f302a7f7
 ---> 1d076e3f7bb9
Successfully built 1d076e3f7bb9
Successfully tagged ubuntu_build_test_1:1.0.4
SECURITY WARNING: You are building a Docker image from Windows against a non-Windows Docker host. All files and directories added to build context will have '-rwxr-xr-x' permissions. It is recommended to double check and reset permissions for sensitive files and directories.

最後のDjangoインストール以外同じ内容なため、ビルドが大分短時間で終わりました。
コマンド実行時の出力を少しよく見てみると、キャッシュが使われている点などが確認できます。

...
Step 2/3 : RUN apt-get update     && apt-get install -y     python3.7-dev     python3.7     python3-distutils     wget     && echo 'alias python="/usr/bin/python3.7"' >> /root/.bashrc     && cd tmp /     && wget https://bootstrap.pypa.io/get-pip.py     && python3.7 get-pip.py
 ---> Using cache
 ---> 09019f05baa6
...

このように、RUNコマンドなどは無理して少なくするというものでもなく、例えば「Pythonやpipのバージョンはたまにしか更新しない」「ライブラリはそれなりの頻度でバージョンの更新や追加などを行うので分ける」「コード部分なども分ける」といった感じに適宜調整すると、ビルドが快適になります。

参考文献・サイトまとめ

19
34
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
19
34