須通り
Sudo Masaaki official site
For the reinstatement of
population ecology.

Docker のコンテナ化は実行環境の可搬性を主眼に置くので、荷物が入ったコンテナというよりも「移動式実験室」とか「工作員がわらわら出てくる漁船」とかに近いですね。

ホーム | 統計 Top | Docker おぼえがき(01)基本操作

完全に自分のための記録として、Docker の環境構築と使用例をメモっておく。なおコンテナ技術はやたら進歩が速くて、1年くらいでベストプラクティスが変わってしまうので注意。とりあえず 2021 年 4 月時点で、Ubuntu 18.04 上での動作が確認できたコードを載せておく。

目次

基本

そもそも何のために Docker やその他のコンテナ型仮想化技術を使うのか。極めて汎用的なツールなので動機は千差万別だろうが、統計や機械学習を学んだり仕事で使ったりする場合を念頭に置くと、プログラミング環境の構築や分離が、よくある回答の一つではないか。特に深層学習ではGPUのドライバ、Python 等の開発用言語、フレームワークのそれぞれにバージョン依存性があることがしばしばで、明示的に環境分離しないと、順調に死亡することになる。

なお Python には「なんちゃら env」系の仮想化ツールもある。筆者も venv で機械学習環境を作っているが、なかなか繊細な部分がある。仮想環境外で apt upgrade したら PyTorch で CUDA を認識しなくなった、などの事故が時々生じるので、トラブルシューティングの技能は必須となる。対極に位置するアプローチが VirtualBox などによる OS 丸ごとの(カーネルを自持ちする)仮想マシンだが、こちらは文字通り、新しい PC を買った際の初期設定に匹敵する手間が掛かる。

粒度が大きすぎず小さすぎず、アプリケーションの実行環境を手軽に使い捨てられ、しかもホストの環境汚染を防げるのが、個人レベルでコンテナ型仮想化を導入する際の大きな利点である。もちろん Python 以外にも特定言語の処理系が欲しいが、すぐにアンインストールするかもという場合のお試し環境にも使える(たとえば Julia だったら、julia なり jupyter/datascience-notebook なりのイメージを引っ張るだけで使える)。別の大きな利点として、ローカルマシン上のコンテナとして構築した実行環境は、LAN 上の別のマシンやクラウドにも低コストで移植できる。つまり大規模な計算や、多数顧客へのウェブサービス提供にも発展させやすい。

なお、最近の Windows マシン(Windows 10 の 21H2 以降か Windows 11)であれば、WSL2 を利用して Linux 環境を作り、その上で CUDA を使用可能になった。Windows + Docker と比べて、パフォーマンス的には有利な点も不利な点もあるらしい。が、ホスト側のファイルシステムにアクセスしやすいとか、音声入出力が容易とか、カジュアルユースには嬉しい点が多々あるので、個人的には WSL2 推奨である(単一ディストロの同バージョンを複数インストールできない等の制約もあるが)。

最低限のコンテナ稼働に必要なコマンド

この記事ではインストール方法は述べない。ホスト OS によって事情が異なり解説にスペースが割かれてしまうし、ネット上に十分な情報がある。基本的には公式情報に従って Docker CE をインストールする。ここまで上手く行ったとして、まずは「コンテナ」に慣れるとよい。

run によるコンテナ起動

最初は run コマンドを使う。既存コンテナを実行、ないしはイメージから新しいコンテナを作成して実行する。

だがそもそも Docker のコンテナとは何だろう。大雑把な構造の理解としては、最低限のアプリケーション動作環境が詰め込まれた「イメージ」ファイルに、書き込み可能レイヤを足したものがコンテナである。この書き込み可能レイヤに、ユーザーが必要とする追加のアプリケーションをインストールしたり、業務に必要なデータを足したりして(あるいはホストシステム上のデータディレクトリをマウントして)、当座の環境を構築する。まあ物理マシンにおいても、OS やアプリケーションは HDD や SSD などのディスクにデータとして記録されており、これをメモリに呼び出して実際に動作させ、変更があった箇所をディスクに書き戻すといった処理を行っているわけだ。すでに定型化されており、ユーザーごとに書き換える必要がないデータについて、イメージとして固定した形で配布していると思ってもらえばいいだろう。


# docker run
# https://docs.docker.jp/engine/reference/commandline/run.html

# 基本文法
docker run [オプション] イメージ名 [コンテナ起動直後に実行するコマンド]

# 使用例 1 
# 一番初歩的なイメージを引っ張って run する。
# 上手く行けばコンソールに "hello world" および簡単なインストラクション説明が出て、それ以上はしない。
$ sudo docker run hello-world


# 使用例 2(ダウンロード容量 3 GB)
# nvidia/cuda イメージを引っ張って run した後、NVIDIA のシステム管理インターフェイス(nvidia-smi)を実行。
$ sudo docker run --runtime=nvidia --rm nvidia/cuda:11.2.2-base-ubuntu18.04 nvidia-smi

# 上の GPU 有効化フラグは、Docker 本体と nvidia-docker2 ランタイムが分離していた時代の書き方(今でも有効)。

# Version 19.03 以降の Docker では本体に GPU 連携機能が備わったので、以下の表記になる。
$ sudo docker run --gpus all --rm nvidia/cuda:11.2.2-base-ubuntu18.04 nvidia-smi # マシン上の全ての GPU を使うとき
$ sudo docker run --gpus 1 --rm nvidia/cuda:11.2.2-base-ubuntu18.04 nvidia-smi # 1 つだけ GPU を使うとき


# 使用例 3(ダウンロード容量 3 GB)
# tensorflow/tensorflow イメージを引っ張って run した後、ターミナル上からコンテナ内のシステムを bash で操作できる状態にする。
$ sudo docker run -it --runtime=nvidia tensorflow/tensorflow:latest-gpu bash

注意:Docker のバージョンにもよるが、一般ユーザーではグループが未設定だと docker なんちゃらの各コマンドを打てない。ローカルマシンで完結するならば、一番安直&安全な方法は sudo を付けて実行。Ubuntu のバニラ状態では常に必要で、Windows 11 では基本的に不要。今後の解説ではコード例に sudo があったりなかったりするので、適宜調節してほしい。

イメージ

コンテナの起動時、コンテナの雛型というべき「イメージ」を指定する。初回起動時、イメージは Docker Hub というウェブサイト(hub.docker.com)から拾ってくる。なお、使用例 2 の nvidia/cuda と使用例 3 の tensorflow/tensorflow は割と重くて各 3 GB くらいある。次回以降は、イメージが既にローカルマシン上の、Docker が管理する領域に存在していれば、そのイメージが再利用される。このディレクトリは特殊なので、ユーザーが docker 外から通常のファイル操作でイメージを消すことは非推奨。


nvidia/cuda:11.2.2-base-ubuntu18.04 とあるのは、コンテナの雛型になるイメージの指定。
/ および : の前後はそれぞれ、
⋆ Docker Hub のリポジトリ名 / イメージ名
⋆ イメージ名 : イメージのバージョン
である。

今回の使用例 2 だと、Docker Hub で nvidia が公開している cuda というイメージの、バージョン 11.2.2-base-ubuntu18.04 を決め打ちして取得。11.2.2 とは CUDA v 11.2.2 が入っているイメージという意味で、base-ubuntu18.04 は Docker 環境で ubuntu 18.04 が動作する、最も保守的な構成(cuDNN は入っていない)。

なお nvidia/cuda 謹製のイメージからコンテナを起動して GPU を使うには、ホストシステムに nvidia-docker2 をインストールしておく必要がある。入れ方はネット上に腐るほど情報があるので割愛。ホストに CUDA と cuDNN を入れる必要はない。これらはコンテナ側が持つものであり、適切なイメージさえ拾えば CUDA Toolkit と cuDNN は好きなバージョンを組み合わせられるのが、Docker の利点である(おかげで、特定のバージョンでしか動かない深層学習フレームワークの動作環境を作り分けられる)。ただし CUDA のバージョンが 10 以上であるコンテナを動かすには、ホスト側に(初代 nvidia-docker ではなく)nvidia-docker2 および Docker 19.03 以上が必要。


引数の -gpus all、-gpus 1 もしくは --runtime=nvidia は GPU を使いたい場合の nvidia-docker2 を指す。コンテナで使うランタイムを nvidia 製のものに指定する。未指定なら普通の(CPU しか使わない)Docker が起動。

-it は、疑似 TTY(pseudo-TTY)を、コンテナの標準入力に接続するよう Docker に対して命令する。要するにユーザーがターミナル上で打ち込んだコマンドにより、コンテナをコマンド操作できる。

-rm は、コンテナを stop コマンドで停止すると、コンテナが自動的に削除される。

pull によるイメージの取得

上記の run コマンドは、インターネット上からイメージを取得してきて直ちにコンテナを起動する。だが、先にイメージだけダウンロードしておきたいこともある。この場合は docker run を docker pull イメージ名 に置き換えると、起動なしでイメージを取得できる。

なお pull と聞いて git みたいだなぁとの印象を受ける方もいるだろう。実は docker には commit や tag といった基本コマンドもあるし、将来的に慣れてきたら自ら push で、Docker Hub にイメージを投稿もできるので、まあ思った以上に git っぽさはある。

stop によるコンテナの停止

コンテナを起動したら、本格的にいじってみる前に停止方法を覚えよう。基本的には docker の stop というコマンドを使うのだけど、起動時に -it したか否かで、すこし対応が変わってくる。


上で起動したコンテナの止め方

# 使用例 1 
$ sudo docker run hello-world # 起動
→ 何も打たなくても、メッセージ出力後に自動で停止する。

# 使用例 2
# -it 付けずにコンテナを立ち上げ
$ sudo docker run --runtime=nvidia --rm nvidia/cuda:11.2.2-base-ubuntu18.04 nvidia-smi 
→ このコンテナの場合は、GPU の情報表示後に自動で停止する。

# 使用例 3
# -it 付けてコンテナを立ち上げ、bash 起動
$ sudo docker run -it --runtime=nvidia tensorflow/tensorflow:latest-gpu bash

疑似 TTY(pseudo-TTY)がコンテナの標準入力に接続されたので、プロンプトが $ じゃなくて # に変わっているはず。
root@9304ca82970d:/notebooks# 

この状態で 
exit 
と打ち込むと、疑似 TTY が切断され、ホストシステムのコマンド入力画面に戻っているはずだ。
そして(下の docker container ls -a でコンテナを一覧すると)停止していることが分かる。

なお
Ctrl + D キー
を押すショートカットも、exit と同じくコンテナを止める。

ちょっと込み入った話をすると、上の使用例 3 は exit するとコンテナが停止する。また使用例 2 に至っては、そもそも exit 等の操作をしていないにもかかわらず、nvidia-smi を打った直後にコンテナは停止する。ただし他のイメージからコンテナを起動した場合、exit しても依然動き続ける事例も存在する。一見、上で述べた原則論が通用しないようにも思える。この挙動を理解するには「PID = 1(コンテナのルートプロセス)」という概念を説明する必要があるため、次のセクションの下の方に情報をまとめておく。

container ls(えるえす)でコンテナの状態を一覧する


# 以下のコマンドで現在起動中のコンテナを一覧できる(古い文献には docker ps とある)
$ docker container ls 

# また以下のコマンドで、停止中も含めシステム上に存在するコンテナを一覧できる。
$ docker container ls -a

# スドウくんの環境で docker container ls -a した結果
CONTAINER ID   IMAGE                                  COMMAND    CREATED         STATUS                     PORTS     NAMES
450907d1cff7   hello-world                            "/hello"   4 minutes ago   Exited (0) 4 minutes ago             fervent_cohen
1cec33b9a727   tensorflow/tensorflow:latest-gpu-py3   "bash"     19 months ago   Exited (0) 19 months ago             odtb
715e6e3caf91   tensorflow/tensorflow:1.12.0-gpu-py3   "bash"     20 months ago   Exited (0) 19 months ago             odtest
sdmk@Bekoneko:~$ 

(発展)コンテナのルートプロセスについて

コンテナにはルートプロセスという、起動時に必ず実行されるアプリケーションが存在する。これはイメージの設計者が Dockerfile を書き下す際に、ENTRYPOINT(もしくは CMD)という命令文で定めるもので、コンテナを起動すると栄光のプロセス ID 1 番(PID = 1)として実行される。特に centos とか ubuntu とか、フルスペックの Linux ディストロをコンテナで再現する系のイメージでは、ホストのターミナルからコンテナを操作するために、シェルを起動することが多いようだ。

実際の物理マシンや仮想マシン上で動く Linux では、/sbin/init(実際は systemd へのシンボリックリンクになっていることが多い)というプロセスが PID 1 に位置付けられ、こいつがシステム内の他のサービスを順次起動する(pstree というコマンドを打つと確認できる)。一方 Docker ではある種の特権を与えない限り、systemd を起点に複数サービスを起動といった使い方はできない。というのも Docker の設計哲学が「1 つのコンテナは 1 つのアプリケーションを動かす目的で使い捨てる」であり、基本的には上記のルートプロセスだけを過不足なく動かせる、ミニマムな環境として作るからである。

これに根ざした Docker の基本挙動として、PID 1 のプロセスを kill すると、コンテナそのものが停止する。"docker run -it 適当なイメージ bash" などとして起動した後、たとえ別のプロセスを立ち上げてジョブを投入したとしても、ルートプロセスである bash を Ctrl + D や exit で落とすと、そのコンテナ自体を docker stop したのと同じことになる。なお -rm を付けていれば、停止したコンテナは自動削除されるので、この場合はコンテナに再接続して計算中のデータをレスキューすることは(ホスト側のフォルダを共有して中間生成物を置かない限り)絶望的である。

コンテナからの出入りを制御するコマンド

というわけで計算中に一時的にホストのコンソール環境に戻りたいときは、stop してはいけない。代わりに detach を使う。

detach でコンテナをデタッチする


使用例 3 のコンテナを「止めずに抜ける」

# -it 付けてコンテナを立ち上げ、bash 起動
$ sudo docker run -it --runtime=nvidia tensorflow/tensorflow:latest-gpu bash

root@9304ca82970d:/notebooks# 

この状態で Ctrl キーを押しながら(p key → q key)の順に打ち込むと、疑似 TTY が切断され、ホストシステムのコマンド入力画面に戻るが、水面下ではまだコンテナは起動している。

sdmk@Bekoneko:~$ docker container ls -a
CONTAINER ID   IMAGE                                  COMMAND    CREATED          STATUS                      PORTS                NAMES
9304ca82970d   tensorflow/tensorflow:latest-gpu-py3   "bash"     20 minutes ago   Up 47 seconds               6006/tcp, 8888/tcp   adoring_swanson
450907d1cff7   hello-world                            "/hello"   34 minutes ago   Exited (0) 33 minutes ago                        fervent_cohen
1cec33b9a727   tensorflow/tensorflow:latest-gpu-py3   "bash"     19 months ago    Exited (0) 19 months ago                         odtb
715e6e3caf91   tensorflow/tensorflow:1.12.0-gpu-py3   "bash"     20 months ago    Exited (0) 19 months ago                         odtest
sdmk@Bekoneko:~$ 

停止とは異なり、シェルを終了せずに tty を取り外すのが detach である。 bashの場合は、「エスケープ・シーケンス」こと Ctrl-p + Ctrl-q を使う。なお tty を外すと、シェルそのものは bash から抜けてホストシステムに戻るが、コンテナは動作状態のまま残る。。

attach でコンテナ標準入力に再接続する

いったんデタッチしたコンテナの標準入出力に繋ぎなおすには、


$ docker container ls

で稼働中のコンテナの IDを調べてから、


$ docker attach コンテナのID(もしくは コンテナの NAME でもいい)

とする。またイメージの設計によっては、起動したコンテナのルートプロセスが bash ではなく、ホストのコンソールから直ちには標準入出力へ繋がらないこともある。この場合は、


$ docker container exec -it コンテナのID bash

として、コンテナ内で新しく bash を起動し tty 接続する。なお、この方法で開いた bash はルートプロセスではないので、コンテナを止めずに exit で抜けられる(末席に開いた bash が退席するだけでデーモンは仕事を続ける)。コンテナ自体を止めるには、ホストマシンのコンソールから "docker stop コンテナのID" を打つ。

start によるコンテナの再起動

なお、-rm のフラグを付けずに run したコンテナは、docker stop で停止しただけでは削除されない。こいつは "docker container ls -a" で ID を調べてから、 docker start すれば再び使用することができる。コンテナ自体は rm するまでは、マシンのディスク容量を専有し続ける。

また commit で新しいイメージを作りたいときは、基本的に「停止中」に行う。プロセスが動いている途中でもデタッチすればコミットできるが、計算中のキャッシュファイルがどこで何をするか判らないため、やるべきではない。


# まず停止中のコンテナを一覧する
$ sudo docker image ls -a
CONTAINER ID        IMAGE                                  COMMAND             CREATED             STATUS                    PORTS               NAMES
f302d0e6dce4        tensorflow/tensorflow:latest-gpu-py3   "bash"              7 months ago        Exited (0) 7 months ago                       vigilant_allen
127fa4315c4d        hello-world                            "/hello"            7 months ago        Exited (0) 7 months ago                       determined_goldwasser

ここで、 vigilant_allen とか determined_goldwasser とかあるのは、
$ docker run --name 自分で指定した名前
としなかった際に docker が勝手につける名前。docker stop や docker start の引数は by ID でも by NAME でも操作できる。

# コンテナを再起動したいときは名前でもIDでもいい。
$ sudo docker start vigilant_allen
# もしくは
$ sudo docker start f302d0e6dce4

ただし注意すべきことがあり、停止済みのコンテナを start で再起動後、直ちに /bin/bash には入れない。


というのも docker start には
    -i, --interactive (コンテナの STDIN にアタッチ)はあるが
    -t, --tty (疑似ターミナル pseudo-TTY を割り当て)というオプションはない。

なので bash に入るには、 docker start でデーモンとして再起動した後、ホストのコンソールから
$ sudo docker exec -it vigilant_allen /bin/bash
と打つ必要がある。

docker exec (docker container exec)は起動中のコンテナを名前で指定して、コンテナ内部のプロセスを動かすコマンド。上記の場合、 -it でホストとコンテナの tty を繋げつつ、コンテナ内の /bin/bash を起動する。

これでホストのコンソールからコマンド操作が可能となる。プロンプトが # に変わっているのが分かる。

コンテナ・イメージを管理するコマンド

docker rm による不要なコンテナの削除

さて、-rm を付けずに run してから stop したコンテナの残骸は、docker rm というコマンドで削除できる。


$ docker container ls -a
# 調べた ID を基にコンテナを削除
$ docker rm 127fa4315c4d

image ls でイメージを一覧する

コンテナの一覧表示コマンドとして container ls があるのと同様、Docker 管理領域に存在するイメージファイルの一覧は docker image ls で取得できる。


# ローカルに存在する image の一覧(古い文献では docker images)
$ sudo docker image ls

REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nvidia/cuda             9.0-base            34d9d9eeafd5        8 months ago        133MB
tensorflow/tensorflow   latest-gpu-py3      888a203f096f        8 months ago        3.27GB
hello-world             latest              4ab4c602aa5e        8 months ago        1.84kB

docker rmi によるイメージの削除

一方、コンテナを作成する素材となったイメージは、コンテナを削除してもハードディスク上に残り続ける。これを削除するには、docker image ls でイメージファイルの一覧を調べてから、docker rmi というコマンドを使う。


$ sudo docker image ls

REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nvidia/cuda             9.0-base            34d9d9eeafd5        8 months ago        133MB
tensorflow/tensorflow   latest-gpu-py3      888a203f096f        8 months ago        3.27GB
hello-world             latest              4ab4c602aa5e        8 months ago        1.84kB

# ID でアクセスしてイメージを削除する
$ sudo docker rmi 4ab4c602aa5e

ただし、あるイメージをベースにレイヤーを付加して作られたコンテナが存在するような場合には、rmi による削除を拒否されることがある。rmi -f として force フラグを付けると無理やり消せるが、イメージのキャッシュを伴わないコンテナが残ってしまうので、管理上の整合性としては推奨される行為ではない。

管理方針としては、自分のマシンにおいて Docker Hub からイメージをダウンロードする時間・費用と、ローカルにギガバイト単位のイメージをキャッシュしておくストレージの余裕を比べるとよい。ただしバージョンを決め打ちしたイメージ(たとえば tensorflow/tensorflow:2.4.1)と比べて、「とりあえずビール」的な気持ちで落とした最新版(tensorflow/tensorflow:latest)は、その後の本家アップデートで価値が低下するので、動作再現用に必要でない限りは消したほうがいいだろう。


使用していない docker オブジェクトの削除
https://docs.docker.jp/config/pruning.html


## コンテナで使用されておらず、タグもついていないイメージ(厳密な意味での dangling image)を全て消す
$ docker image prune

## コンテナで使用されていないイメージ全部消す
$ docker image prune -a

## 上記の image prune -a はちょっと無差別すぎる(コンテナに紐づけていないが、ローカルに持っておきたいイメージまで消されてしまう)ので、--filter を適宜使用する。
## 英語だが以下のサイトで、filter のリファレンスがみられる。現在は until と label しかないが。
https://docs.docker.com/engine/reference/commandline/image_prune/

素体となるイメージの選択

特定の用途でコンテナを作って開発を始めたい場合、DockerHub 等に公開されているイメージの中から、目的の言語やライブラリがすぐに使えて、かつ軽量なものを探して素体にするのが鉄則である。個人的に遊ぶ分には全部入りのイメージを拾ってくる方が楽なのだが、サーバー上で本番サービスを展開するとなるとコンテナを設置するストレージも無料ではないし、互換性やセキュリティ面でも不要なツールが入っていると、よろしくない。一般論として全部入りのシステムから、不要なプログラムを1つ1つ削除して最低限の動作環境を作るよりも、自分が動かしたいツールの開発元が公開した最小サイズのイメージを拾ってきて、A が足りないと叱られたら A を、B も入れろと叱られたら B を追加する方が遥かに楽であろう。

また「Docker 開発のベストプラクティス」のページに書いてあるように、最低限のランタイムが用意されているイメージを拾って、まず本番環境用のコンテナを作る。その後 Dockerfile に追記してデバッグツールのインストールをインストールし、開発用のコンテナとする。

軽量なイメージとしては次の記事 でちょっと触れる Alpine Linux や、データボリュームコンテナの素体に使われることが多い busybox 等が挙げられる。

逆にデータサイエンス系で、古典統計用の全部入りイメージが欲しければ Jupyter + Python + Julia が使える jupyter/datascience-notebook:latest とか、R の最新版がいち早くサポートされて追加のコンポーネント(rstudio, tidyverse 等)もオプションタグで選べる rocker が便利。