💈

Docker のプロキシ設定は割と地獄

2024/05/27に公開

髪の手入れ?地獄だよ。考えたくもない。
          —— 『葬送のフリーレン』 ——

Docker のプロキシ設定は割と地獄

企業のプロキシ下にあるサーバーなどで Docker を使う場合は正しくプロキシ設定をしないと外部へのアクセスが通らないため、イメージを pull できなかったり build 中にコケたりする。

この設定というのが『Docker behind proxy - setup nightmare』という issue が立てられ「現在の公式ドキュメントは客観的に見て間違っています」と書き込まれるほどの魔境である(2024/05現在)。

ChatGPT-4o とやらがどれだけ優秀か知らないが、公式ドキュメントが不十分である以上、いくら質問しても解決することはないというタイプの沼であり、私も既に 100 時間は溶かしているだろう。

https://github.com/docker/cli/issues/4501

ほとんどの企業では既に社内に誰かが 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]なので、curlwget などのコマンドを用いて疎通確認する。これらのツールは環境変数によるプロキシ設定から影響を受ける。

curlwget は 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種類の方法がある。

  1. .bashrc に環境変数を書き込んでシェルの環境変数として保持する
  2. 実行時に環境変数を定義して与える
  3. シェルスクリプトや Makefile に書いておく

個人的には 3 の方法がよいと思っている。なぜならば全体の環境変数を汚さないし、仮に全体の環境変数が既に汚れていても上書きできるし、上書きされていることに簡単に気付けるからである。

1. .bashrc に環境変数を書き込んでシェルの環境変数として保持する

以下のような内容を .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 ...

3. シェルスクリプトや Makefile

シェルスクリプトや 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 がそのままであってほしい』で書いた方法でできるので、必要であれば管理者に依頼して設定してもらうこと。

https://zenn.dev/wsuzume/articles/71f00c9e81225c

ただし、この変更は当然他のユーザーが sudo コマンドを実行したときにも影響を及ぼすので、既にプロキシ設定がズブズブになっているサーバーでは docker コマンドを実行するユーザーを docker グループに追加してもらい、sudo なしでも docker コマンドを実行できるようにしたほうがよい。

# ユーザーを docker グループに追加する
sudo usermod -aG docker [username]

その上に Dockerfile に書かれた内容をビルドする

ベースイメージが pull し終わったあと、Dockerfile の内容がビルドされるときはビルドコンテキストの設定にしたがう。この設定をしておかないと、ビルド時に apt-getcurlpip などのツールが環境変数のプロキシ設定を参照することができずビルドに失敗する。

ビルドコンテキストに環境変数を設定するには 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 などのコマンドで設定されていること自体は確認できるとは思うが、どこで設定されたのかネットで検索してあれこれ探さないと見つからないという意味で凶悪な設定である。

https://docs.docker.com/config/daemon/systemd/#httphttps-proxy

他にも /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]。この記事を書くことを決意した瞬間である。

https://docs.docker.com/network/proxy/#configure-the-docker-client

Dockerfile 内の ENV コマンドでイメージに焼き込む

Dockerfile 内に ENV コマンドで環境変数を定義することで、ビルドコンテキストに環境変数を定義し、イメージに焼き込むことができる。手前2つに比べれば、Dockerfile を読めば普通に把握できるという点でまったく凶悪さはなく、やりたければやればいい程度のものである。

Dockerfile
ENV HTTP_PROXY=http://...
ENV HTTPS_PROXY=http://...

Dockerfile にこの記述を行っておくことで、ビルド時にプロキシ設定が有効となる。また、ENV でイメージに焼き込まれた環境変数は、そのイメージをもとにコンテナを起動したときにデフォルトでコンテナ内にもセットされているので、この方法でビルド時とコンテナ起動時の両方をカバーできるというメリットがある。

しかし逆に言えば

  • プロキシ設定という組織固有の情報がイメージに焼き込まれてしまう
  • この環境変数がいつどこで設定されたものなのか分かりづらい

ことになり、長期的にはこのデメリットが無視できないものになる。

もう少し詳しく説明しておくと、イメージに焼き込まれた環境変数はあとからでも取り出せるため、あとで配布したくなったときに配布が難しくなる。たとえばプロキシの URL にユーザー名とパスワードを書く方式の場合は特にこのやり方は適さない。したがってプロキシ設定のような情報はイメージに書き込むべきではないので --build-arg を使うやり方のほうが安全である

また、起動時に環境変数が指定されている場合、それ以前に環境変数がセットされていないかどうかを確かめるには、単に起動時の環境変数指定を除いてみればよい。また、起動時の環境変数指定は優先順位がもっとも高いため、どこで設定されたのか検証しやすい。

具体的には、コンテナ起動時に -e オプションで指定された環境変数が上書きされうるタイミングは、後述の Dockerfile で指定された ENTRYPOINT または CMD に指定されたスクリプトと、実際にコンテナの起動時に実行されたコマンド(docker container run コマンドの引数になっているコマンド)の計3ヶ所しかない。したがって起動時に -e オプションで環境変数を指定することと、起動時に実行するコマンドを変えてみることの2つを試すことで、実際にコンテナ内に設定されている環境変数が設定されたタイミングがほぼ確定できる。

DockerfileENTRYPOINTCMD 内のスクリプトで指定する

Docker コンテナは起動時に ENTRYPOINT を経由してから CMD が実行される。CMD に関しては、docker container run コマンドなどの引数で上書きが可能である。

したがって docker container run -e オプションで指定された環境変数も、ENTRYPOINTCMD 中で 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.denv_keep
    • そのあとのビルドがおかしい
      • DockerfileARGENV を確認する
      • --build-arg で明示的に指定すれば上書き可能
  • docker container run の挙動がおかしい
    • まずはコンテナ内で printenv するのが確実
    • プロキシ設定がおかしければ -e オプションで上書きする
    • 原因を調べるなら DockerfileENVENTRYPOINTCMDの順
    • 指定してないのにコンテナ内に環境変数がいる場合は ~/.docker/config.json のせい
    • sudo ありなら /root/.docker/config.json のせい


『葬送のフリーレン』6巻のユーベルちゃんくらい天才か倫理観ガバめなら苦労はしない

脚注
  1. もちろん社内ドキュメントは別途書いたがが、それらはその検索性の悪さから伝わるべき者に伝わらない運命にあるので、パブリックな情報のみで業務時間外に本記事を構成し、ここに供養する。 ↩︎

  2. ひとつのサーバーで複数のサービスを起動する場合などは、.bashrc をなるべくプレーンな状態にしておいたほうが後々問題が起こったときに対処が楽である。 ↩︎

  3. HTTP 通信はプロキシによって中継する方法がプロトコルレベルで定められている。他の通信プロトコルに関しては保証されていないので、たとえばデータベースへのアクセスなどはプロキシを通過できないことが多い。例外的に SSH 接続は SSH over HTTPS という仕組みでプロキシを突破できることが多いが、会社のファイアウォールに穴を開けることを意味し、勝手にやると最悪懲戒を食らうので注意する。もっと言えば piping server とか SoftEther VPN を利用することでボコスカ穴は開けられるものなのだが、こういうのは大抵やった瞬間に IT 管理者が飛んできて本当にマジでバチクソ怒られてクビになるからやっちゃダメ。 ↩︎

  4. 大学院で経験した数理最適化の実装とそのデバッグに比べればまったく大したことはない。数理最適化は理論的な導出の部分から実装まで自分でやると、仮に実装が合っていても収束しない可能性があり、収束しているように見えてもそれが正しい挙動であることを証明する手段がないという不安と焦燥に苛まれ続け逃れることができないまさに地獄である。プロキシは通ればそれで終わりなので。 ↩︎

Discussion