プロキシのある環境でDockerを動かす方法

日立製作所の小出です。この記事ではDockerを使う際にプロキシを設定する方法と、それに関連するDockerの挙動について説明します。

# はじめに
会社からインターネットに接続するためにはプロキシを経由する必要がある、というのはありがちなシチュエーションだと思いますが、Dockerは他のツールに比べて設定につまづくポイントが多いように思います。実際、Qiitaで検索してみると、以下の件数の記事がヒットします (2019/12/5時点)。

- `yum proxy`: 1281件
- `apt proxy`: 1042件
- `npm proxy`: 832件
- `docker proxy`: **2055件**

という具合で、開発者の苦労が伺えます。

また、Docker本体にたびたび設定方法や細かい挙動の変更が入ることも混乱の一因かと思われます。そこで、この記事では2019年12月現在のDocker事情を反映した設定方法を紹介したいと思います。

# 動作環境
この記事は以下の環境での動作を基に執筆しました。

- Ubuntu 18.04.3 LTS (Bionic Beaver)
- Docker 19.03.5

なお、Dockerの実行や設定にはroot権限が必要です。

# TL; DR
先に結論を書いておくと、以下の設定をしておけばイメージの取得からビルド、コンテナ内のコマンド実行まで、プロキシ経由で作業できます。
`192.168.0.10:8080` でプロキシサーバが動いていると仮定した場合の例を記載するので、環境に合わせてIPとポートを書き換えてください。また、認証プロキシの場合は `http_proxy=http://USER:PASSWORD@example.com:8080` のような書式でユーザ名とパスワードが指定できます。

## 1. systemdに環境変数を設定する
`systemctl edit docker` で設定ファイルを開き、以下を記載します。

```/etc/systemd/system/docker.service.d/override.conf
[Service]
Environment = 'http_proxy=http://192.168.0.10:8080' 'https_proxy=http://192.168.0.10:8080' # 必要なら 'no_proxy=...'
```

## 2. Dockerクライアントを設定する

```~/.docker/config.json
{
  "proxies": {
    "default": {
      "httpProxy": "http://192.168.0.10:8080",
      "httpsProxy": "http://192.168.0.10:8080"
    }
  }
}
```

# Docker proxy deep dive
ここからはなぜ上記の設定で良いのかの解説です。

## dockerdとdocker
Dockerはクライアントサーバモデルのソフトウェアです。MySQLを使うとき`mysqld`に対して`mysql`コマンドで話しかけるように、Dockerは`dockerd`に対して`docker`コマンドで話しかけることができます。プロキシもこれら2つのプログラムに対して設定する必要があります。

コンテナの中で動くプログラムに対してプロキシを設定する場合は`docker`を設定し、それ以外の場合 (pullとか) は`dockerd`を設定する、という風に分かれているようです。

## dockerdの設定
dockerdにプロキシを設定する方法は、環境変数ただ1つです。
`systemctl stop docker` でサービスを止めて、dockerdをフォアグラウンドで動かしてみると検証しやすいと思います。
単に`dockerd`コマンドを実行すると、フォアグラウンドでdockerdが起動します。

```sh
$ sudo dockerd
INFO[2019-12-05T06:46:10.612966150Z] detected 127.0.0.53 nameserver, assuming systemd-resolved, so using resolv.conf: /run/systemd/resolve/resolv.conf
<いろいろ出ますが省略>
INFO[2019-12-05T06:46:10.892366184Z] Docker daemon                                 commit=633a0ea838 graphdriver(s)=overlay2 version=19.03.5
INFO[2019-12-05T06:46:10.892416069Z] Daemon has completed initialization
INFO[2019-12-05T06:46:10.909923228Z] API listen on /var/run/docker.sock
```

端末をもう一つ開いて、イメージをpullしてみます。この時点では何も設定していないのでpullに失敗します。

```sh
$ sudo docker pull alpine
Using default tag: latest
Error response from daemon: Get https://registry-1.docker.io/v2/: dial tcp: lookup registry-1.docker.io: No address associated with hostname
```

dockerdの標準出力にも、名前解決ができずエラーになったという旨のメッセージが表示されます。

```
WARN[2019-12-05T07:34:29.915501297Z] Error getting v2 registry: Get https://registry-1.docker.io/v2/: dial tcp: lookup registry-1.docker.io: No address associated with hostname
INFO[2019-12-05T07:34:29.915555514Z] Attempting next endpoint for pull after error: Get https://registry-1.docker.io/v2/: dial tcp: lookup registry-1.docker.io: No address associated with hostname
ERRO[2019-12-05T07:34:29.915607313Z] Handler for POST /v1.40/images/create returned error: Get https://registry-1.docker.io/v2/: dial tcp: lookup registry-1.docker.io: No address associated with hostname
```

dockerdに`http_proxy`, `https_proxy`を渡せば、pullできるようになります。

```sh
$ export http_proxy=http://192.168.0.10:8080
$ export https_proxy=http://192.168.0.10:8080
$ sudo -E dockerd
```

```sh
$ sudo docker pull alpine
Using default tag: latest
latest: Pulling from library/alpine
Digest: sha256:c19173c5ada610a5989151111163d28a67368362762534d8a8121ce95cf2bd5a
Status: Downloaded newer image for alpine:latest
docker.io/library/alpine:latest
```

また、`docker info`でdockerdが認識しているプロキシを確認することもできます。

```sh
$ sudo docker info
Client:
 Debug Mode: false

Server:
 Containers: 0
  Running: 0
<略>
 Debug Mode: false
 HTTP Proxy: http://192.168.0.10:8080
 HTTPS Proxy: http://192.168.0.10:8080
 Registry: https://index.docker.io/v1/
<略>
```

この時点で`docker pull`がうまく動かない場合は、そもそもネットワーク設定が正しくできていない可能性が高いので、OSの流儀に従ってネットワーク設定を見直してください。

### systemdの設定
上記と同じ設定をdockerサービスに反映するために、systemdのserviceファイルを編集します。

systemdの設定ファイルは `/etc/systemd/system` ディレクトリにあります (他にもたくさんありますが、詳細は `man systemd.unit` を参照してください)。このディレクトリに `docker.service` というファイルを作って設定を記述すれば良いのですが、プロキシ設定のような「既存の設定にプラスアルファで付け加えたい」という類のものに適したドロップインファイルという機能があるので、それを使うのがおすすめです。

以下のコマンドを実行すると、ドロップインファイルの編集画面が開きます。

```sh
$ sudo systemctl edit docker
```

このファイル (`/etc/systemd/system/docker.service.d/override.conf`) に対して、環境変数の設定を記載すれば、dockerサービスがプロキシ経由で動くようになります。

```/etc/systemd/system/docker.service.d/override.conf
[Service]
Environment = "http_proxy=http://192.168.0.10:8080" "https_proxy=http://192.168.0.10:8080"
```

あとはサービスを起動すれば、dockerdの設定は完了です。

```sh
$ sudo systemctl start docker
$ sudo docker pull alpine
Using default tag: latest
latest: Pulling from library/alpine
Digest: sha256:c19173c5ada610a5989151111163d28a67368362762534d8a8121ce95cf2bd5a
Status: Downloaded newer image for alpine:latest
docker.io/library/alpine:latest
```

かつて `/etc/sysconfig/docker` というファイルに環境変数を設定する、という方法もありましたが、今はdeprecatedになったようです。

## dockerクライアントの設定
冒頭に書いた通り、 `~/.docker/config.json` に設定を書けば良いのですが、この設定方法はDocker 17.07以降で有効な設定方法で、昔のバージョンでは使えませんでした (CentOS 7のextrasリポジトリで配布されているdockerは1.13.1のようなので、この方法は使えません)。

この設定ファイルができる前は、以下のように `docker run` の引数で環境変数を指定したり、

```sh
$ sudo docker run -it -e http_proxy=http://192.168.0.10:8080 -e https_proxy=http://192.168.0.10:8080 alpine
/ # wget https://qiita.com
```

コンテナの中で環境変数を設定していました。もちろん今でもこれらの方法は有効です。

```sh
$ sudo docker run -it alpine
/ # export http_proxy=http://192.168.0.10:8080
/ # export https_proxy=http://192.168.0.10:8080
/ # wget https://qiita.com
```

`~/.docker/config.json` にプロキシ設定を書いておくと、自動的に上記の環境変数を設定してくれます (大文字の`HTTP_PROXY`, `HTTPS_PROXY`も設定されます)。

```sh
$ sudo docker run -it alpine # ~/.docker/config.json を記載した上で起動
/ # env
HTTPS_PROXY=http://192.168.0.10:8080
HOSTNAME=e60420de06d3
SHLVL=1
HOME=/root
https_proxy=http://192.168.0.10:8080
http_proxy=http://192.168.0.10:8080
TERM=xterm
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/
HTTP_PROXY=http://192.168.0.10:8080
```

ただし、環境変数が設定されるのは**コンテナの生成時**なので、起動済みのコンテナに`exec`するときは設定されません。

```sh
$ mv ~/.docker/config.json{,.bak} # ファイルをリネームして設定を無効化
$ sudo docker run -itd --name alpine alpine
$ mv ~/.docker/config.json{.bak,} # 有効化
$ sudo docker exec -it alpine ash
/ # env
<http_proxy は表示されない>
```

## `docker build`
先述した設定がしてあれば、ビルド時に特別何かする必要はありません。「あれ？ `--build-arg` でプロキシ指定するんじゃないの？」と思った方もいるかもしれません。もちろん指定しても動きますが、 `~/.docker/config.json` で指定してあれば不要です。

ちなみに、17.05より前のバージョンでは `--build-arg` に指定した内容が `docker history` に漏れなく残っていたので、認証プロキシを使っているとどうしてもイメージにユーザ名やパスワードが残ってしまう問題がありました。17.06以降のバージョンであれば、 `--build-arg` を使う方法でも、`~/.docker/config.json` に指定する方法でも、イメージに情報が残ることはないので安心です (逆に残したい場合は、DockerfileにARGで明示的に指定するとhistoryに残せます)。

# 付録

`~/.docker/config.json` に書いてある "default" の部分には、dockerdが動いているマシンが指定できます。もし複数のマシンでdockerdを動かしていて、それぞれのマシンで起動するコンテナに別々のプロキシを設定したい場合は以下のように設定できます。

```~/.docker/config.json
{
  "proxies": {
    "tcp://docker-host1:2375": {
      "httpProxy": "http://proxy1.example.com:8080",
      "httpsProxy": "http://proxy1.example.com:8080"
    },
    "tcp://docker-host2:2375": {
      "httpProxy": "http://proxy2.example.com:8080",
      "httpsProxy": "http://proxy2.example.com:8080"
    },
    "default": { // 上2つ以外のマシンに適用される
      "httpProxy": "http://proxy.example.com:8080",
      "httpsProxy": "http://proxy.example.com:8080"
    }
  }
}
```

# 参考資料
- [Control Docker with systemd](https://docs.docker.com/config/daemon/systemd/)
- [Configure Docker to use a proxy server](https://docs.docker.com/network/proxy/)
- [docker run | Set environment variables (-e, --env, --env-file)](https://docs.docker.com/engine/reference/commandline/run/#set-environment-variables--e---env---env-file)
- [docker build | Set build-time variables (--build-arg)](https://docs.docker.com/engine/reference/commandline/build/#set-build-time-variables---build-arg)
- [docker build --build-arg http_proxy=... で認証プロキシを設定すると危険だよ、という話](https://qiita.com/wadahiro/items/3b08e14e7ca2c6b31461)