Docker のプロキシ設定は割と地獄
髪の手入れ?地獄だよ。考えたくもない。
—— 『葬送のフリーレン』 ——
Docker のプロキシ設定は割と地獄
企業のプロキシ下にあるサーバーなどで Docker を使う場合は正しくプロキシ設定をしないと外部へのアクセスが通らないため、イメージを pull できなかったり build 中にコケたりする。
この設定というのが『Docker behind proxy - setup nightmare』という issue が立てられ「現在の公式ドキュメントは客観的に見て間違っています」と書き込まれるほどの魔境である(2024/05現在)。
ChatGPT-4o とやらがどれだけ優秀か知らないが、公式ドキュメントが不十分である以上、いくら質問しても解決することはないというタイプの沼であり、私も既に 100 時間は溶かしているだろう。
ほとんどの企業では既に社内に誰かが Docker 入りのサーバーを構築していてノウハウを共有してくれるだろうからハマってもインフラ担当が何とかしてくれると思うが、自分がインフラ担当になったケースはその限りではない。
いちいち誰かに説明するのは面倒なので記事で共有しておこうと思う[1]。
Docker にプロキシ設定できる場所は複数あり、それぞれ役割が違う
Docker の仕組みを分かっていれば、たとえばビルド時とコンテナ内では別々にプロキシを設定する必要があることは想像がつくだろう。複数の設定が必要であること自体は構わないのだが、Docker においてはその設定方法がことごとく直感に反する。
意味が重複する設定を複数ヶ所に書くことが可能であり、重複させた場合にどれが優先されるかなど考えたくもない。まして他人がセットアップしたサーバーなどどこに何が書いてあるのか分かったものではなく、設定を消したはずがどこかに書き込まれていてなおゾンビのごとく生き残り続けているなんてこともある。あった。悲しかった。
したがって必要十分な設定を重複しないように書くのが基本となる。他人がセットアップしたサーバーの場合は仕方がないので頑張ってこれから書く項目をすべてチェックしよう。
予備知識:プロキシ設定の書き方
プロキシ設定は大抵、以下のような環境変数を設定することで行われる。
# 以下、[] 内は省略可能。
HTTP_PROXY=http[s]://[username:password@]proxy_host[:port]
HTTPS_PROXY=http[s]://[username:password@]proxy_host[:port]
NO_PROXY=127.0.0.1,localhost
http_proxy=http[s]://[username:password@]proxy_host[:port]
https_proxy=http[s]://[username:password@]proxy_host[:port]
no_proxy=127.0.0.1,localhost
# たとえば以下のようになる。
HTTP_PROXY=http://josh:123456Seven@my_proxy:3128
HTTPS_PROXY=http://my_proxy:3128
# プロキシサーバー自体が HTTPS でのアクセスを受け付ける場合は
# 以下のように書ける場合もある
HTTPS_PROXY=https://my_proxy:3128
# NO_PROXY は以下のようにネットワークの範囲指定もできる。
NO_PROXY=127.0.0.1,localhost,192.168.1.0/8
その名の通り、HTTP_PROXY
は HTTP 通信に対して適用されるプロキシ、HTTPS_PROXY
は HTTPS 通信に対して適用されるプロキシ、NO_PROXY
はプロキシ通信を行わない通信である。NO_PROXY
には典型的にはサーバーを立てるなどしてループバック通信がある場合はループバックアドレスを指定したり、社内ネットワークにある無制限でアクセス可能なサーバー(むしろプロキシを通すとアクセスできないサーバー)にアクセスを通したいときに指定する。
様々なツールはこの環境変数を読み取ってよしなに通信を行ってくれる。逆に言えば、環境変数に指定すると様々なツールが影響を受けてしまうことを意味するので、面倒ではあるが .bashrc
のようなスコープの広い場所に書くのではなく、可能であればシェルスクリプトや Makefile
の中に分離したほうが賢い[2]。
大文字の HTTP_PROXY
, HTTPS_PROXY
, NO_PROXY
と小文字の http_proxy
, https_proxy
, no_proxy
のどちらを利用するか、また両方指定されている場合にどちらを優先するかなどは明確な規定がなくツールによって異なるため、両方とも同じものを指定しておくのが定石となる。
疎通確認
プロキシが中継するのは基本的に HTTP 通信(および HTTPS 通信)のみ[3]なので、curl
や wget
などのコマンドを用いて疎通確認する。これらのツールは環境変数によるプロキシ設定から影響を受ける。
curl
や wget
は Docker やその周辺ライブラリのインストールなどにもよく使われるコマンドなので、プロキシ関係のオプションを書いておく。
curl
# 対象のサーバーへのアクセス
curl 172.18.0.3
curl google.com
# ポート指定
curl 172.18.0.3:8000
# プロキシ指定
curl -x http://my_proxy:3128 172.18.0.3
# no_proxy の指定
curl --noproxy 127.0.0.1,localhost,172.0.0.0/24 172.18.0.3
# 詳細表示によるデバッグ
curl -v 172.18.0.3
wget
# 対象のサーバーからページをダウンロード
# index.html が出力される
wget 172.18.0.3
wget google.com
# ダウンロードしたページを標準出力に出力する
wget -O - google.com
# プロキシの存在による SSL エラーを無視する
wget --no-check-certificate google.com
# プロキシの環境変数を指定する
wget -e HTTP_PROXY=http://... -e HTTPS_PROXY=http://... [多いので省略] google.com
docker image pull に必要な環境変数
docker image pull
で外部からイメージを pull してくるには docker デーモンに対して環境変数を設定する必要がある。いまのところ一番管理しやすいと思っているのは systemctl
コマンドで設定を追記する方法である。
以下の設定を行うと、docker image pull
がプロキシを経由するようになり、Docker Hub などのアクセス先がプロキシから許可されていれば pull が可能になる。
設定を揃えておくという意味では NO_PROXY
を書いてもよいが、docker image pull
のときしか使われないので、NO_PROXY
を追加する必要はない。
[Service]
Environment="HTTP_PROXY=http://proxy.example.com:80"
Environment="HTTPS_PROXY=https://proxy.example.com:443"
# Docker デーモンの設定を確認
sudo systemctl cat docker
# Docker デーモンの設定を編集
sudo systemctl edit docker
# Docker デーモンを再起動
sudo systemctl restart docker
または公式の設定手順に従い、/etc/systemd/system/docker.service.d/http-proxy.conf
を作成して直接編集してもよい。
# Docker デーモンの設定を確認
sudo systemctl cat docker
# Docker デーモンの設定を編集
sudo vim /etc/systemd/system/docker.service.d/http-proxy.conf
# デーモンの設定を更新
sudo systemctl daemon-reload
# Docker デーモンを再起動
sudo systemctl restart docker
docker image build に必要な環境変数
Docker イメージのビルドは
- 最初にベースイメージを pull する
- その上に Dockerfile に書かれた内容をビルドする
の2段階で行われる。
最初にベースイメージを pull する
ここが Docker のキショいポイントだが、docker image build
コマンドがベースイメージを pull するときはシェルの環境変数を参照する。
つまり docker image pull
のための設定が正しくできていても、docker image build
は最初のベースイメージの pull でコケるのである。これは docker image pull
でベースイメージをダウンロードしたあとに docker image build
を実行すると最初のベースイメージの pull は(既にローカルにイメージがあるので)通過できるという非常にキモい現象となって現れる。
シェルの環境変数設定が必要となる関係上、docker image build
で最初のベースイメージの pull がコケないようにするためにはおおよそ以下の3種類の方法がある。
-
.bashrc
に環境変数を書き込んでシェルの環境変数として保持する - 実行時に環境変数を定義して与える
- シェルスクリプトや
Makefile
に書いておく
個人的には 3 の方法がよいと思っている。なぜならば全体の環境変数を汚さないし、仮に全体の環境変数が既に汚れていても上書きできるし、上書きされていることに簡単に気付けるからである。
.bashrc
に環境変数を書き込んでシェルの環境変数として保持する
1. 以下のような内容を .bashrc
に追記する。やはり NO_PROXY
を設定する必要はないが、設定を揃えておきたい場合は書いておいてもよい。
export HTTP_PROXY=http://proxy.example.com:3128
export HTTPS_PROXY=http://proxy.example.com:3128
export http_proxy=http://proxy.example.com:3128
export https_proxy=http://proxy.example.com:3128
2. 実行時に環境変数を定義して与える
直前に export
コマンドを実行しておく。
# 以下を順次実行しておく
export HTTP_PROXY=http://proxy.example.com:3128
export HTTPS_PROXY=http://proxy.example.com:3128
export http_proxy=http://proxy.example.com:3128
export https_proxy=http://proxy.example.com:3128
または、実行時にコマンドの手前に書くことで、そのコマンドにのみ環境変数を与えることができる。
HTTP_PROXY=... HTTPS_PROXY=... http_proxy=... https_proxy=... docker image build ...
Makefile
3. シェルスクリプトや シェルスクリプトや Makefile
の冒頭に以下のようにプロキシ設定を書いておく。
HTTP_PROXY=http://proxy.example.com:3128
HTTPS_PROXY=http://proxy.example.com:3128
http_proxy=http://proxy.example.com:3128
https_proxy=http://proxy.example.com:3128
予備知識:sudo を使うときの注意
.bashrc
などに書き込まれ、現在のシェルに設定されている環境変数は sudo
時には基本的に引き継がれない。したがって docker image build
がうまくいっても、sudo docker image build
がうまくいかないという現象が起こる場合、それは sudo
時の環境変数の引継ぎがうまく行っていない。
sudo
時も設定が引き継がれてほしい場合は HTTP_PROXY
, HTTPS_PROXY
, NO_PROXY
, http_proxy
, https_proxy
, no_proxy
などのうち、引き継がれてほしいものを env_keep
の対象に指定する必要がある。
以前、『sudo のときに $PWD がそのままであってほしい』で書いた方法でできるので、必要であれば管理者に依頼して設定してもらうこと。
ただし、この変更は当然他のユーザーが sudo
コマンドを実行したときにも影響を及ぼすので、既にプロキシ設定がズブズブになっているサーバーでは docker
コマンドを実行するユーザーを docker
グループに追加してもらい、sudo
なしでも docker
コマンドを実行できるようにしたほうがよい。
# ユーザーを docker グループに追加する
sudo usermod -aG docker [username]
その上に Dockerfile に書かれた内容をビルドする
ベースイメージが pull し終わったあと、Dockerfile
の内容がビルドされるときはビルドコンテキストの設定にしたがう。この設定をしておかないと、ビルド時に apt-get
、curl
、pip
などのツールが環境変数のプロキシ設定を参照することができずビルドに失敗する。
ビルドコンテキストに環境変数を設定するには docker image build
で --build-arg
オプションを使用する。--build-arg
で指定された変数はビルド中にのみ有効であり、イメージに焼き込まれることはなく、その後に起動するコンテナにも引き継がれることはない。
# もはや1行で書くのがキツいしタイプするのもツラい
docker image build \
--build-arg HTTP_PROXY=http://... \
--build-arg HTTPS_PROXY=http://... \
...
docker container run に必要な環境変数
ここまでの説明でイメージのビルドまではできるはずなので、残るはそのイメージを用いて起動したコンテナに行うプロキシ設定である。コンテナにプロキシ設定をするにはコンテナ内の環境変数をいじればよいので、ここまでの手順よりもドキュメントは豊富でやり方もよく知られている。
プロキシ設定の場合、おすすめするのはコンテナの起動時に -e
オプションで環境変数を渡す方法である。
docker container run
コマンドなどでコンテナを起動するときは -e
オプションを付け加えることで、そのコンテナに環境変数を渡すことができる。あとからどんなコンテナにでも設定できる上に、イメージに焼き込まれないので基本的にはこの方法を使うのがよいと思われる。
docker container run -e HTTP_PROXY=http://... HTTPS_PROXY=http://... [以下略]
アンチパターン
以上の設定を行えば、Docker のプロキシ設定に関しては必要十分であると言える。いちいちコマンド実行時に指定せねばならないという面倒臭さはあるが、それはスクリプトを書けば解消できる範囲であって、環境を汚さないというメリットのほうが大きいと個人的には思う。
公式ドキュメントやネット上の記事を見ると他にも様々な設定方法が載っているのだが、それらに関しては複数の段階に影響を与えるか、または設定の影響の範囲が非常にわかりづらく、ここまでで説明した方法に比べるとアンチパターンと呼んで差し支えないと思う。
以下が知っている範囲のアンチパターン集である。
/etc/docker/daemon.json
などに記述する
これはおそらく docker image pull
のときに必要な systemctl
での設定を代替することができる。
/etc/docker/daemon.json
にプロキシ設定を書き込む方法は、一応公式ドキュメントに紹介されている方法ではあるのだが、仮に設定されていたとしてもその設定ファイルの存在に気付くことができないという点が致命的である。
なにかおかしいことがあったとき systemctl
の設定はかなり早い段階で見に行くと思われるし、確認も systemctl cat docker
と統一されたインターフェースでアクセスできて、どこのファイルで行われた設定なのかも調査できるが、いきなり「/etc/docker/daemon.json
を見に行こうかな」と特定できるやつはまずいない。そんな場所に設定が書き込まれているなんて誰も思わない。
おそらく docker info
などのコマンドで設定されていること自体は確認できるとは思うが、どこで設定されたのかネットで検索してあれこれ探さないと見つからないという意味で凶悪な設定である。
他にも /etc/default/docker
や /etc/sysconfig/docker
などに記述することもできるようだが、ぜひともやめてほしいものである。
~/.docker/config.json
これも公式ドキュメントに書かれている方法のひとつだが、Docker デーモンではなく Docker CLI 側の設定であることに注意が必要である。
~/.docker/config.json
に書かれた設定はビルド時およびすべての起動したコンテナに反映されるという凶悪極まりない影響力を持つ。また、sudo
で docker コマンドを使っている場合、この所在は /root/.docker/config.json
のものが有効となるため、気付くことが非常に難しい。
/root/.docker/config.json
にプロキシ設定が書かれているとエラー対応が以下のようになる。
- ライブラリのバージョン上げたらあっちのマシンではうまく行ったのにこっちのマシンではなぜかエラーになる!
- うーん、どうやらプロキシ設定が原因であることは分かった。
- でも何でだろう?どこかプロキシ設定間違っただろうか?
- プロキシを経由しちゃいけない通信がプロキシを経由しようとしてない?
- え、
HTTP_PROXY
とか環境変数が勝手に設定されとるやんけ!- イメージには焼いてない、起動時に指定してない、シェルにも設定してないけど。
- デーモンの設定も一通り見たけど、コンテナに渡される機序が分からん。
- (この件に関しては ChatGPT も答えられない)
- (無限のネット検索と試行錯誤)
/root/.docker/config.json
!! おまえか!!
まぁ地獄だった[4]。この記事を書くことを決意した瞬間である。
ENV
コマンドでイメージに焼き込む
Dockerfile 内の Dockerfile
内に ENV
コマンドで環境変数を定義することで、ビルドコンテキストに環境変数を定義し、イメージに焼き込むことができる。手前2つに比べれば、Dockerfile
を読めば普通に把握できるという点でまったく凶悪さはなく、やりたければやればいい程度のものである。
ENV HTTP_PROXY=http://...
ENV HTTPS_PROXY=http://...
Dockerfile
にこの記述を行っておくことで、ビルド時にプロキシ設定が有効となる。また、ENV
でイメージに焼き込まれた環境変数は、そのイメージをもとにコンテナを起動したときにデフォルトでコンテナ内にもセットされているので、この方法でビルド時とコンテナ起動時の両方をカバーできるというメリットがある。
しかし逆に言えば
- プロキシ設定という組織固有の情報がイメージに焼き込まれてしまう
- この環境変数がいつどこで設定されたものなのか分かりづらい
ことになり、長期的にはこのデメリットが無視できないものになる。
もう少し詳しく説明しておくと、イメージに焼き込まれた環境変数はあとからでも取り出せるため、あとで配布したくなったときに配布が難しくなる。たとえばプロキシの URL にユーザー名とパスワードを書く方式の場合は特にこのやり方は適さない。したがってプロキシ設定のような情報はイメージに書き込むべきではないので --build-arg
を使うやり方のほうが安全である。
また、起動時に環境変数が指定されている場合、それ以前に環境変数がセットされていないかどうかを確かめるには、単に起動時の環境変数指定を除いてみればよい。また、起動時の環境変数指定は優先順位がもっとも高いため、どこで設定されたのか検証しやすい。
具体的には、コンテナ起動時に -e
オプションで指定された環境変数が上書きされうるタイミングは、後述の Dockerfile
で指定された ENTRYPOINT
または CMD
に指定されたスクリプトと、実際にコンテナの起動時に実行されたコマンド(docker container run コマンドの引数になっているコマンド)の計3ヶ所しかない。したがって起動時に -e
オプションで環境変数を指定することと、起動時に実行するコマンドを変えてみることの2つを試すことで、実際にコンテナ内に設定されている環境変数が設定されたタイミングがほぼ確定できる。
Dockerfile
の ENTRYPOINT
や CMD
内のスクリプトで指定する
Docker コンテナは起動時に ENTRYPOINT
を経由してから CMD
が実行される。CMD
に関しては、docker container run
コマンドなどの引数で上書きが可能である。
したがって docker container run -e
オプションで指定された環境変数も、ENTRYPOINT
や CMD
中で export
コマンドなどを用いて後から書き換えることが可能である。
これらの場所にプロキシ設定を書き込む理由はない(素直に ENV
か -e
で渡せばよいだけである)ので、なぜそこでプロキシ設定をしているのかという意味不明さはあるが、前述の方法で二手詰めで書かれている場所を特定できるため、やりたければやればよいという程度であろう。
おしまい
まとめるとこう。自分で素直に設定するときは以下。
-
docker image pull
-
sudo systemctl cat docker
で分かる場所に書く(docker デーモンが参照するため)
-
-
docker image build
- シェルの環境変数を確認する(ベースイメージの pull のため)
-
docker image build --build-args HTTP_PROXY=...
にも渡す(ビルドコンテキストで参照するため)
-
docker container run
-
docker container run -e HTTP_PROXY=...
で渡す(コンテナが参照するため)
-
他人が管理しているサーバーで問題が起こっている場合は以下の順で疑う。
-
docker image pull
の挙動がおかしいsudo systemctl cat docker
/etc/systemd/system/docker.service.d/http-proxy.conf
/etc/docker/daemon.json
/etc/default/docker
/etc/sysconfig/docker
-
docker image build
の挙動がおかしい- ベースイメージの pull がおかしい
-
sudo
なしなら~/.bashrc
など -
sudo
ありなら/root/.bashrc
など -
sudo
ありなら/etc/sudoers
や/etc/sudoers.d
のenv_keep
-
- そのあとのビルドがおかしい
-
Dockerfile
のARG
やENV
を確認する -
--build-arg
で明示的に指定すれば上書き可能
-
- ベースイメージの pull がおかしい
-
docker container run
の挙動がおかしい- まずはコンテナ内で
printenv
するのが確実 - プロキシ設定がおかしければ
-e
オプションで上書きする - 原因を調べるなら
Dockerfile
のENV
、ENTRYPOINT
、CMD
の順 - 指定してないのにコンテナ内に環境変数がいる場合は
~/.docker/config.json
のせい -
sudo
ありなら/root/.docker/config.json
のせい
- まずはコンテナ内で
『葬送のフリーレン』6巻のユーベルちゃんくらい天才か倫理観ガバめなら苦労はしない
-
もちろん社内ドキュメントは別途書いたがが、それらはその検索性の悪さから伝わるべき者に伝わらない運命にあるので、パブリックな情報のみで業務時間外に本記事を構成し、ここに供養する。 ↩︎
-
ひとつのサーバーで複数のサービスを起動する場合などは、
.bashrc
をなるべくプレーンな状態にしておいたほうが後々問題が起こったときに対処が楽である。 ↩︎ -
HTTP 通信はプロキシによって中継する方法がプロトコルレベルで定められている。他の通信プロトコルに関しては保証されていないので、たとえばデータベースへのアクセスなどはプロキシを通過できないことが多い。例外的に SSH 接続は SSH over HTTPS という仕組みでプロキシを突破できることが多いが、会社のファイアウォールに穴を開けることを意味し、勝手にやると最悪懲戒を食らうので注意する。もっと言えば piping server とか SoftEther VPN を利用することでボコスカ穴は開けられるものなのだが、こういうのは大抵やった瞬間に IT 管理者が飛んできて本当にマジでバチクソ怒られてクビになるからやっちゃダメ。 ↩︎
-
大学院で経験した数理最適化の実装とそのデバッグに比べればまったく大したことはない。数理最適化は理論的な導出の部分から実装まで自分でやると、仮に実装が合っていても収束しない可能性があり、収束しているように見えてもそれが正しい挙動であることを証明する手段がないという不安と焦燥に苛まれ続け逃れることができないまさに地獄である。プロキシは通ればそれで終わりなので。 ↩︎
Discussion