Docker muti-stage builds で中間イメージにタグ付けする

## モチベーション
Docker image の容量を少しでも小さくするために，ビルド用イメージとメインのイメージを分けることがあります．特に，Docker 17.05以降では，[multi-stage builds](https://docs.docker.com/engine/userguide/eng-image/multistage-build/)がサポートされ取り回しがよくなりました．

```:Dockerfile
## Build pytorch. ##
FROM python:3.6.3 AS build-env
# Install dependencies.
RUN pip install numpy pyyaml mkl setuptools cmake cffi
# Disable GPU.
ENV NO_CUDA 1
# Download and build.
RUN wget https://github.com/pytorch/pytorch/archive/v0.2.0.tar.gz && \
    tar -zxvf v0.2.0.tar.gz && \
    cd pytorch-0.2.0 && \
    python setup.py install

## Build main image. ##
FROM python:3.6.3
ENV LD_LIBRARY_PATH /usr/local/lib
# Install dependencies.
RUN pip --no-cache-dir install numpy mkl
# Install pytorch.
RUN mkdir /usr/local/lib/python3.6/site-packages/torch \
          /usr/local/lib/python3.6/site-packages/torch-0.2.0-py3.6.egg-info
COPY --from=build-env /usr/local/lib/python3.6/site-packages/torch /usr/local/lib/python3.6/site-packages/torch
COPY --from=build-env /usr/local/lib/python3.6/site-packages/torch-0.2.0-py3.6.egg-info /usr/local/lib/python3.6/site-packages/torch-0.2.0-py3.6.egg-info
```
```bash
$ docker build -t pytorch .
$ docker images
REPOSITORY             TAG                 IMAGE ID            CREATED             SIZE
pytorch                latest              a47d831b6b81        7 seconds ago        1.52GB
<none>                 <none>              775d7f035616        About a minute ago   2.14GB
```

一方で， multi-stage builds で生成された中間イメージは `docker image prune` で削除されてしまったり， CI環境でのキャッシュの取り回しが若干面倒であったりします．そこで， multi-stage builds の中間イメージにタグ付けしたいというのがモチベーションです．


## どうやるの？
`COPY --from=<image> <src> <dest>` の構文を使うことで，ビルド用イメージとメインのイメージをつなぐようにします．
まず，ビルド用イメージの `Dockerfile` を用意します．

```:Dockerfile.build
FROM python:3.6.3

# Install dependencies.
RUN pip install numpy pyyaml mkl setuptools cmake cffi
# Disable GPU.
ENV NO_CUDA 1
# Download and build.
RUN wget https://github.com/pytorch/pytorch/archive/v0.2.0.tar.gz && \
    tar -zxvf v0.2.0.tar.gz && \
    cd pytorch-0.2.0 && \
    python setup.py install
```

続いて，メインイメージの `Dockerfile` を用意します．

```:Dockerfile
FROM python:3.6.3

ENV LD_LIBRARY_PATH /usr/local/lib
# Install dependencies.
RUN pip --no-cache-dir install numpy mkl
# Install pytorch.
RUN mkdir /usr/local/lib/python3.6/site-packages/torch \
          /usr/local/lib/python3.6/site-packages/torch-0.2.0-py3.6.egg-info
COPY --from=pytorch_build:local /usr/local/lib/python3.6/site-packages/torch /usr/local/lib/python3.6/site-packages/torch
COPY --from=pytorch_build:local /usr/local/lib/python3.6/site-packages/torch-0.2.0-py3.6.egg-info /usr/local/lib/python3.6/site-packages/torch-0.2.0-py3.6.egg-info
```

この方法だと，2つの `Dockerfile` に分かれてしまうため，最後にビルド手順を記述する `Makefile` を用意します．

```:Makefile
all: main

pytorch_build: Dockerfile.build
	docker build -f Dockerfile.build -t pytorch_build:local --cache-from=pytorch_build:local .

main: Dockerfile pytorch_build
	docker build -f Dockerfile -t pytorch --cache-from=pytorch .
```

ビルドすると，

```bash
$ make
$ docker images
REPOSITORY             TAG                 IMAGE ID            CREATED             SIZE
pytorch                latest              da56568f200a        20 seconds ago      1.52GB
pytorch_build          local               587212a81a2f        22 seconds ago      2.14GB
```

中間イメージにもちゃんとタグが付いていることが分かると思います．また，mnist のサンプルもちゃんと動きます．

```bash
$ docker run -it --rm pytorch bash
root@ba9f46714e02:/# git clone https://github.com/pytorch/examples pytorch-examples
Cloning into 'pytorch-examples'...
remote: Counting objects: 1484, done.
remote: Compressing objects: 100% (6/6), done.
remote: Total 1484 (delta 1), reused 4 (delta 0), pack-reused 1478
Receiving objects: 100% (1484/1484), 29.96 MiB | 249.00 KiB/s, done.
Resolving deltas: 100% (783/783), done.
Checking connectivity... done.
root@ba9f46714e02:/# cd pytorch-examples/mnist
root@ba9f46714e02:/pytorch-examples/mnist# pip install -r requirements.txt 
Requirement already satisfied: torch in /usr/local/lib/python3.6/site-packages (from -r requirements.txt (line 1))
Collecting torchvision (from -r requirements.txt (line 2))
  Downloading torchvision-0.1.9-py2.py3-none-any.whl (43kB)
    100% |████████████████████████████████| 51kB 797kB/s 
Collecting pyyaml (from torch->-r requirements.txt (line 1))
  Downloading PyYAML-3.12.tar.gz (253kB)
    100% |████████████████████████████████| 256kB 2.0MB/s 
Requirement already satisfied: numpy in /usr/local/lib/python3.6/site-packages (from torch->-r requirements.txt (line 1))
Collecting six (from torchvision->-r requirements.txt (line 2))
  Downloading six-1.11.0-py2.py3-none-any.whl
Collecting pillow (from torchvision->-r requirements.txt (line 2))
  Downloading Pillow-4.3.0-cp36-cp36m-manylinux1_x86_64.whl (5.8MB)
    100% |████████████████████████████████| 5.8MB 253kB/s 
Collecting olefile (from pillow->torchvision->-r requirements.txt (line 2))
  Downloading olefile-0.44.zip (74kB)
    100% |████████████████████████████████| 81kB 7.0MB/s 
Building wheels for collected packages: pyyaml, olefile
  Running setup.py bdist_wheel for pyyaml ... done
  Stored in directory: /root/.cache/pip/wheels/2c/f7/79/13f3a12cd723892437c0cfbde1230ab4d82947ff7b3839a4fc
  Running setup.py bdist_wheel for olefile ... done
  Stored in directory: /root/.cache/pip/wheels/20/58/49/cc7bd00345397059149a10b0259ef38b867935ea2ecff99a9b
Successfully built pyyaml olefile
Installing collected packages: six, olefile, pillow, torchvision, pyyaml
Successfully installed olefile-0.44 pillow-4.3.0 pyyaml-3.12 six-1.11.0 torchvision-0.1.9
root@ba9f46714e02:/pytorch-examples/mnist# python main.py 
Downloading http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz
Downloading http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz
Downloading http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz
Downloading http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz
Processing...
Done!
Train Epoch: 1 [0/60000 (0%)]	Loss: 2.316824
Train Epoch: 1 [640/60000 (1%)]	Loss: 2.311471
Train Epoch: 1 [1280/60000 (2%)]	Loss: 2.298839
Train Epoch: 1 [1920/60000 (3%)]	Loss: 2.317593
Train Epoch: 1 [2560/60000 (4%)]	Loss: 2.282021
Train Epoch: 1 [3200/60000 (5%)]	Loss: 2.272163
・・・
Train Epoch: 10 [56960/60000 (95%)]	Loss: 0.142048
Train Epoch: 10 [57600/60000 (96%)]	Loss: 0.199800
Train Epoch: 10 [58240/60000 (97%)]	Loss: 0.102195
Train Epoch: 10 [58880/60000 (98%)]	Loss: 0.305267
Train Epoch: 10 [59520/60000 (99%)]	Loss: 0.162137

Test set: Average loss: 0.0543, Accuracy: 9822/10000 (98%)

```


## まとめ
`COPY --from=<image> <src> <dest>` の構文を使うことで中間イメージにタグ付けすることが出来ました．しかし，1つの `Dockerfile` では完結できないあたり，コレジャナイ感もあります．メリットとしては，ローカルを介することがないため， Docker に閉じた世界でビルドが完結しローカル環境でもCI環境でも同様に扱うことが出来る点が挙げられるかと思います．
`COPY --from=<image> <src> <dest>` の構文自体あまり知られていないようにも思いますので，もしかしたら，既存のイメージから必要なパッケージのみを取り出すなどの用途に利用できるかも分かりません．
なお，本家では，[`LABEL` を使う方法](https://forums.docker.com/t/tag-intermediate-build-stages-multi-stage-build/34795)が提案されていたりします．しかし，この方法だとCI環境でのラベルのライフサイクルの管理がやや面倒です． multi-stage builds で適切にキャッシュを扱えるような機能がリリースされることを待っています．