DockerコンテナでPythonスクリプトを動かす (主要なDockerオプション添え)

先日参加した[みんなのPython勉強会#41 - connpass](https://startpython.connpass.com/event/112786/)で、Python環境として「Dockerを使うのも便利」という話が出たので、簡単なスクリプト実行とかをどうするのかについてのメモ。

[PythonのOfficialイメージはDockerHubに公開されている](https://hub.docker.com/_/python/)ので、これを使えば基本的に動く

# 単発のスクリプトを動かす

こういう1回実行して終了するような処理。

```python:sample.py 
#!/usr/bin/python2

print "hello python"
```

Descriptionの記述の"Run a single Python script"の項の通り、

> ```
>  $ docker run -it --rm --name my-running-script -v "$PWD":/usr/src/myapp -w /usr/src/myapp python:2 python your-daemon-or-script.py
> ```

を実行すれば動く。

```
zaki@epoisses:~/src/python$ sudo docker run -it --rm --name my-app -v "$PWD":/usr/src/myapp -w /usr/src/myapp python:2 python sample.py
hello python
```

この通り。

Python3で動かすなら、タグに`3`を指定すればOK

```
zaki@epoisses:~/src/python$ sudo docker run -it --rm --name my-app -v "$PWD":/usr/src/myapp -w /usr/src/myapp python:3 python sample.py
  File "sample.py", line 3
    print "hello python"
                       ^
SyntaxError: Missing parentheses in call to 'print'. Did you mean print("hello python")?
```

※ これはスクリプト側でshebangを`/usr/bin/python2`でPython2固定にしているためエラー


## オプション解説(I)

### -i (interactive)

```
  -i, --interactive                    Keep STDIN open even if not attached
```

ヘルプを見るとinteractiveと出てくる。その名の通り、これがないとstdinからの入力とかをコンテナが受け付けてくれない。

例えばこんなスクリプト

```python:input.py 
#!/usr/bin/python3

print ("input value...")
val = input()

print (val)
```

を`-i`無しで動かしてみると

```
zaki@epoisses:~/src/python$ sudo docker run -t --rm --name my-app -v "$PWD":/usr/src/myapp -w /usr/src/myapp python:3 python input.py
input value...
123




zaki@epoisses:~/src/python$ sudo docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
f60ad0b909c7        python:3            "python input.py"   9 seconds ago       Up 8 seconds                            my-app
zaki@epoisses:~/src/python$ 
```

数値を入力・enterしてもスクリプトが反応せず、Ctrl-cで抜けても「コンテナ内のスクリプトはstdinからの入力待ち」のまま起動し続けている

### -t (tty)

TTYの割り振り。
パッと見わかりづらく、なくても動くパターンは多い

```
zaki@epoisses:~/src/python$ sudo docker run -i --rm --name my-app -v "$PWD":/usr/src/myapp -w /usr/src/myapp python:3 python input.py
input value...
123
123
zaki@epoisses:~/src/python$ 
```

が、TTY制御が必要な操作は、このオプションがないと表示がおかしくなったりする
シェルの起動だと、プロンプトすら表示されない。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/256859/69b9b1aa-9386-d1b5-da4c-5694af6bb7fa.png)

多くの場合、`-i`と`-t`はセットで使うことが多く、`-it`とまとめて書ける

### --rm

コンテナの実行終了時に自動でコンテナを削除する。
オプションがない場合は削除されないが、本件のように「同じ処理を何度も動かす場合」は、実行後の残骸がどんどんたまってしまうので、残す必要のないコンテナを実行する際は付加するとよい。

### --name my-app

コンテナの名前を`my-app`に設定する。
設定がない場合はランダムな英数となる。
確認するには、`docker ps`の`NAMES`のカラムを見る。

```
zaki@epoisses:~$ sudo docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
1d0288ef8f09        python:3            "python input.py"   4 seconds ago       Up 3 seconds                            eloquent_jepsen
```

コンテナに対する操作(`stop/start`とか`rm`とか)は、このコンテナ名か、コンテナIDを引数に実行するので、何度も使うようなものは名前を付けたほうがわかりやすい。

なお、同じ名前を複数のコンテナにつけることはできない。

### -v "$PWD":/usr/src/myapp

ボリューム設定。
コンテナ内のデータは揮発性でコンテナの削除とともに消えてしまう。データを永続化させるためのもっとも簡単な方法として、ホストOSのディレクトリを参照できるようにする、という機能がある。

[Docker の Data Volume まわりを整理する - Qiita](https://qiita.com/lciel/items/e21a4ede3bac7fb3ec5a)

`-v "$PWD":/usr/src/myapp` を指定することで、「ホストOSの`$PWD`」を「コンテナ内の`/usr/src/myapp`」にマウント、という動作になる。

結果的に、カレントディレクトリにあるPythonのソースファイルが、コンテナ内からは`/usr/src/myapp/*.py`として参照できる、という構成になる。

### -w /usr/src/myapp

コンテナ起動時のカレントディレクトリを`/usr/src/myapp`に変更する。
`cd`と機能的には同じ。

### python:2 python sample.py

`python:2`は`docker run`で起動する`イメージ名:タグ名`となる。
Pythonイメージに限れば、Python3.xならタグは`3`、Python2.xなら`2`となる。
細かいサブバージョンなどの指定も必要であれば、タグ一覧から該当バージョンのものを指定すればよい。
(厳密にはDockerイメージのタグ名はただの識別用文字列であり、プロダクトのバージョンとの仕様上の繋がりはない。が、普通の(マナーの良い)イメージであれば、バージョン番号==タグ名と思って問題ない)

タグ名は省略すると`latest`となる。`latest`の実体がどのタグに紐づいているかは、イメージの仕様を確認しなければわからないため、できれば指定して実行するのが望ましい。

そのあとの`python sample.py`は、起動するpython:2イメージに対する引数となる。
全てのイメージが引数を解釈するとは限らないが、Pythonイメージはここにインタプリタ名(`python`)と実行するスクリプト名(`sample.py`)を指定することで任意のスクリプトファイルを起動できる。
この部分は、ホストOS上で`$ python sample.py`と実行するのと同等。
(ここに`bash`とか指定すると、bashシェルが起動し、シェルログインできる形になる)


# daemon動作するスクリプトを動かす

ちょうどいいスクリプトなんて手持ちにないので、Python3の`http.server`を動かしてみる。

```
zaki@epoisses:~/src/python$ sudo docker run -it --rm --name my-app -v "$PWD":/usr/src/myapp -w /usr/src/myapp python:3 python -m http.server 8080
Serving HTTP on 0.0.0.0 port 8080 (http://0.0.0.0:8080/) ...

```

起動した。
もう一つターミナルを開き、ホストOSから`docker inspect`でIPアドレスを確認

```
$ sudo docker inspect my-app | grep -i ipadd
            "SecondaryIPAddresses": null,
            "IPAddress": "172.17.0.2",
                    "IPAddress": "172.17.0.2",
```

ホストOSからcurlでアクセスしてみると

```
$ curl http://172.17.0.2:8080
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Directory listing for /</title>
</head>
<body>
<h1>Directory listing for /</h1>
<hr>
<ul>
<li><a href="input.py">input.py</a></li>
<li><a href="sample.py">sample.py</a></li>
</ul>
<hr>
</body>
</html>
```

httpdが実行されており、結果(ディレクトリ一覧)が表示される。

`docker run`でhttp.serverを実行しているターミナルは以下の通り。
(前述の`-t`オプションが付加されてない場合は、アクセスログはリアルタイムに表示されない)

```
zaki@epoisses:~/src/python$ sudo docker run -it --rm --name my-app -v "$PWD":/usr/src/myapp -w /usr/src/myapp python:3 python -m http.server 8080
Serving HTTP on 0.0.0.0 port 8080 (http://0.0.0.0:8080/) ...
172.17.0.1 - - [11/Feb/2019 15:00:29] "GET / HTTP/1.1" 200 -

```

Python2の`SimpleHTTPServer`であれば以下の通り

```
zaki@epoisses:~/src/python$ sudo docker run -it --rm --name my-app -v "$PWD":/usr/src/myapp -w /usr/src/myapp python:2 python -m SimpleHTTPServer 8080
Serving HTTP on 0.0.0.0 port 8080 ...
172.17.0.1 - - [11/Feb/2019 15:27:53] "GET / HTTP/1.1" 200 -
```

停止するにはCtrl-cか、別ターミナルで`docker stop my-app`を実行する。

# Jupyter Docker Stacks

[Pythonの話をしよう(PDF)](http://www.tsjshg.info/Tsuji_20190116.pdf)より

> データサイエンスだと、[jupyter/datascience-notebookのイメージ](https://hub.docker.com/r/jupyter/datascience-notebook/)がおすすめです


という話もあったのでついでにお試し。

DockerHubには使い方が載ってなかったけど、Descriptionからリンクされている[Jupyter Docker Stacks](https://jupyter-docker-stacks.readthedocs.io/en/latest/index.html)に起動方法が載っている

> ```
> docker run -p 8888:8888 jupyter/scipy-notebook:17aba6048f44
> ```

実行

```
zaki@epoisses:~/src/python$ sudo docker run -p 8888:8888 jupyter/scipy-notebook:17aba6048f44
Unable to find image 'jupyter/scipy-notebook:17aba6048f44' locally
17aba6048f44: Pulling from jupyter/scipy-notebook
a48c500ed24e: Downloading [====================================>              ]   22.9MB/30.96MB
1e1de00ff7e1: Download complete 
0330ca45a200: Download complete 
471db38bcfbf: Download complete 
0b4aba487617: Download complete 
1bac85b3a63e: Downloading [============================>                      ]  11.22MB/19.7MB
245be47b44f6: Download complete 
ef168d10cf08: Download complete 
9a10e240916d: Download complete 
f963f5de0a6d: Download complete 
84f4b337e3c0: Downloading [==>                                                ]  3.767MB/82.85MB
5db9a0ead114: Waiting 
a51db99fbe91: Waiting 
```

初回起動だと大量のイメージのDLが発生するので注意…

```
0a24f009722c: Pull complete 
7620f95f2bda: Pull complete 
Digest: sha256:0effde1fa6395184e2846fc7728c66a2afdf2e40618e420907015c3a4d3c8d37
Status: Downloaded newer image for jupyter/scipy-notebook:17aba6048f44
Container must be run with group "root" to update passwd file
Executing the command: jupyter notebook
[I 15:19:37.374 NotebookApp] Writing notebook server cookie secret to /home/jovyan/.local/share/jupyter/runtime/notebook_cookie_secret
[I 15:19:38.176 NotebookApp] JupyterLab extension loaded from /opt/conda/lib/python3.6/site-packages/jupyterlab
[I 15:19:38.177 NotebookApp] JupyterLab application directory is /opt/conda/share/jupyter/lab
[I 15:19:38.180 NotebookApp] Serving notebooks from local directory: /home/jovyan
[I 15:19:38.181 NotebookApp] The Jupyter Notebook is running at:
[I 15:19:38.182 NotebookApp] http://(29bcfc866bd5 or 127.0.0.1):8888/?token=e9df31886f2db43f34f9fe08a7e302e858ecfd333***
[I 15:19:38.184 NotebookApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).
[C 15:19:38.185 NotebookApp] 
    
    Copy/paste this URL into your browser when you connect for the first time,
    to login with a token:
        http://(29bcfc866bd5 or 127.0.0.1):8888/?token=e9df31886f2db43f34f9fe08a7e302e858ecfd333***
```

みたいな表示になれば、ブラウザから http://<Dockerを動かしてるホストのIPアドレス>:8888/?token=**** にアクセスすればJupyterの画面になるはず。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/256859/446243a6-030b-b269-bcef-d3c85f00ffa4.png)

イメージサイズはこの通り(2019.02.12時点)

```
zaki@epoisses:~$ sudo docker images jupyter/scipy-notebook
REPOSITORY               TAG                 IMAGE ID            CREATED             SIZE
jupyter/scipy-notebook   17aba6048f44        82f774a9c85a        6 weeks ago         4.87GB
zaki@epoisses:~$ 
```

ちなみにDocker的な[tag一覧はここ](https://hub.docker.com/r/jupyter/datascience-notebook/tags)に載っているけど、Dockerfileはどこかで見れるのかな。

## オプション解説(II)

### -p &lt;host-port&gt;:&lt;container-port&gt;

`-p 8888:8888`の部分。
[Tomcatでのwebappデプロイ](https://qiita.com/zaki-lknr/items/9ac0b519fecfbc6c1d5f#tomcat%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A%E3%81%AEpull%E3%81%A8%E8%B5%B7%E5%8B%95%E3%81%A8%E3%83%9D%E3%83%BC%E3%83%88%E3%83%95%E3%82%A9%E3%83%AF%E3%83%BC%E3%83%89%E8%A8%AD%E5%AE%9A)でも書いたが、「ホストOS上でDockerがListenするTCPアクセスを、コンテナのポートへbindする」というもの。
同じポート番号を指定すれば、「ホストOS上の8080/TCPをコンテナの8080/TCPへポートフォワーディングする」という動作になり、ホストOS上からだけでなく、ホストOSへアクセスできるリモートのPCからもコンテナにアクセスできるようになる。

`-p 8080:80`にすれば、ホストOSでは8080/TCPでlistenし、コンテナの80/TCPへポートフォワーディングする。