ディスクを食い潰す Docker volume の落とし穴

# 概要
- `Dockerfile` の `VOLUME` は予期せずディスクを食い潰す原因になる
- MySQL など多数の公式 Docker イメージで `VOLUME` が使われているため要注意

# 遭遇した問題
ある時、CI サーバーのディスクが圧迫されており、その大部分を Docker volume が占めていることがわかりました。

```bash
$ docker system df
TYPE                TOTAL               ACTIVE              SIZE                RECLAIMABLE
Images              14                  5                   6.787GB             3.283GB (48%)
Containers          8                   4                   137.5MB             131.4MB (95%)
Local Volumes       1281                7                   224.8GB             224.3GB (99%)
Build Cache         0                   0                   0B                  0B
```

更に詳しく調べてみると、ランダムな名前でほぼ同じ中身の volume が大量に残っていて、無駄にディスク容量を食っているようでした。

```bash
$ docker system df -v
...
Local Volumes space usage:

VOLUME NAME                                                        LINKS               SIZE
68d96aaf9e8a9098c4ab38c26315f4d25b02b96a51eaf70a7037f655e0dccba4   0                   226.6MB
cdfb0a70f73d997bf5f7abee7d37adea98891d938da17bda8ae6d25ca5decc71   0                   228.6MB
f043ee71a3a318ae6479bf0205d3192666ca58b43582572b41046421f502cdef   0                   226.6MB
8d15e829f575564683006cb65340687423749fc6b1e52a5fe0e62c81ed40f2e4   0                   228.6MB
...
```

# 問題の原因
ランダムな名前の volume は **anonymous volume** と呼ばれるもので、以下のような場合に作成されます。

- `Dockerfile` に `VOLUME` 指示を書いた場合
- `docker-compose.yml` や `docker run` のオプションで ` -v /path/in/container` のように指定した場合

前述のケースでは CI のテストに Docker を使用し、終了後にコンテナを削除して後始末していたため、
起動時に毎回 anonymous volume が新しく作成され、削除されることも再利用されることもなくゴミとして溜まっていたわけです。

一度コンテナから切り離された anonymous volume の出所を特定するのは厄介ですが、
適当なコンテナに volume をアタッチするなどして中身を調べれば手がかりが得られます。

```sh
$ docker run -it --rm -v <volume の名前>:/volume busybox sh
$ ls /volume
auto.cnf	 client-key.pem  ibdata1	     private_key.pem  sys
ca-key.pem	 ib_buffer_pool  ibtmp1		     public_key.pem
ca.pem		 ib_logfile0	 mysql		     server-cert.pem
client-cert.pem  ib_logfile1	 performance_schema  server-key.pem
``` 

今回 anonymous volume を作り出していた大元の原因箇所は、ベースとして使用する [MySQL 公式の Docker イメージ](https://github.com/docker-library/mysql/blob/d72fdef749b3b6c140e4b0d6a87f12737ed3554a/8.0/Dockerfile.debian#L87)の中にありました。

```dockerfile:Dockerfile
VOLUME /var/lib/mysql
```

`Dockerfile` で一度指定された `VOLUME` は、派生イメージやコマンドラインオプションなどで解除できないため、
ベースイメージを使用する全てのユーザーが同様の問題に当たる可能性があります。
これは MySQL 以外にも多くのメジャーな公式イメージで issue として挙がっており、現在でも解決されていません。

- [Why is there volume for data in the first place? · Issue #255 · docker-library/mysql](https://github.com/docker-library/mysql/issues/255)
- [Volumes should not be defined in base images · Issue #404 · docker-library/postgres](https://github.com/docker-library/postgres/issues/404)
- [Volumes should not be defined in base images · Issue #140 · docker-library/redis](https://github.com/docker-library/redis/issues/140)
- [Should not declare VOLUME for /data/db · Issue #306 · docker-library/mongo](https://github.com/docker-library/mongo/issues/306)
- [VOLUME declaration can result in difficult to diagnose misbehavior · Issue #410 · docker-library/rabbitmq](https://github.com/docker-library/rabbitmq/issues/410)


# 対処法
## Dockerfile から VOLUME を削除
根本的に volume を作成させないための方法です。

既に述べた通り `Dockerfile` の `VOLUME` は解除できないので、イメージの使用方法の選択肢を狭めてしまいます。
volume の設定は `Dockerfile` の中ではなく、実行時に docker コマンドや `docker-compose.yml` で指定する方が良いでしょう。

問題は、 MySQL 公式のイメージなど、管理外の `Dockerfile` で `VOLUME` が指定されている場合です。
この場合は自身でレポジトリをフォークして `Dockerfile` を書き換える以外に解決策がありません。
しかしこの方法だと、管理すべきコードが増え、フォーク元の変更に追随する手間が発生するためお勧めできません。


## 定期的に volume を掃除
上記の方法が使えない場合、対症療法として volume を定期的に削除する手があります。

```bash
docker volume ls -f dangling=true --format "{{ .Name }}" | grep -E '^[a-z0-9]{64}$' | xargs --no-run-if-empty docker volume rm
```

例えばこのコマンドを実行すると、以下の流れで anonymous volume をまとめて削除してくれます。

1. 使用されていない volume のリストを取得
1. 名前が英数字 64 文字の anonymous volume だけをフィルタ
1. 該当する volume を削除

ただし、**本当に必要な volume まで削除してしまわないか事前によく確認しておきましょう**。