第2の人生の構築ログ

自分の好きなことをやりつつ、インカムもしっかりと。FIRA60 (Financial Independence, Retire Around 60) の実現を目指します。SE を生業としていますが、自分でプログラミングしながら自分が欲しいと思うアプリケーションを作ることが楽しみです。旅行と温泉、音楽と読書は欠かすことができません。

Dockerコンテナ起動するだけでHTTPS対応、証明書管理の自動化、nginx-proxy + Let's Encryptの便利構成

AWS や GCP を使った環境も便利ですが、サーバだけ借りて自前でサーバ環境を立てることもまだよくやります。 それをやる一番の理由はコストと自由度、手軽さです。

ちょっとした Web アプリやテスト用の Web アプリなどを動作させたい場合、 1つのサーバに Docker を使って Web アプリを複数稼働させる場合など、 レンタルサーバ環境の方が、稼働時間やトラフィックなどの従量課金を気にせずにバンバン使えるため、 圧倒的にコスト安で、精神的な安心感も得られます。

ただ、自前のサーバ環境となると、各種設定や日々のメンテナンス、HTTPS 環境の場合にはサーバ証明書の管理まで必要となって、 メンドクサイ...という気持ちにもなります。

ですが、

に出会ってからは、一度 nginx-proxy + letsencrypt-nginx-proxy-companion 環境を用意すれば、

  • nginx-proxy がリバースプロキシとして機能し、
  • letsencrypt-nginx-proxy-companion が SSL/TLS 証明書 (Let's Encrypt) を自動発行・更新し、
  • 新たに Docker コンテナを起動するだけで HTTPS 対応のプロキシが自動で構成される

ことになります。とても楽です。

とても便利ですので、nginx-proxy + docker-letsencrypt-nginx-proxy-companion の紹介をこの記事でしようと思います。

概要

この環境の大枠のイメージですが、 名前(ホスト名)ベースで背後にいるコンテナにリクエストを振り分ける nginx のリバースプロキシ(nginx-proxy)と 各ホストのサーバ証明書を管理(サーバ証明書の取得/更新)する docker-letsencrypt-nginx-proxy-companion の2つのコンテナがベースとなります。

公開する Web アプリのコンテナの Docker ファイルで必要な環境情報をセットしておいて、同じ Docker ネットワークで起動してあげると、 nginx-proxy がそれを見つけて勝手にリバースプロキシの設定を作成して、プロキシしてくれるようになります。

更に、初回起動時に letsencrypt-nginx-proxy-companion が letsencrypt のサーバ証明書を勝手に取得してくれて、 追加された Web アプリのコンテナが HTTPS 通信できるようになります。

ホントに便利です。

仕組み

もう少し詳しく仕組みの部分を記載します。 改めまして、この環境は以下の2つのコンテナイメージで構成されます。

このコンテナを利用し、docker-compose.yml を以下のように記載します。

各コンテナの簡単な説明は以下の通りです。

  • jwilder/nginx-proxy

    • 機能: 起動しているコンテナ(Web アプリなど)を自動的に検出して、nginx のリバースプロキシ設定を作成します。
    • 仕組み:
      • Docker ソケット (/var/run/docker.sock) を監視し、
      • 各コンテナの VIRTUAL_HOST などの環境変数を読み取って、
      • Nginx の設定ファイルを自動生成・更新します。
  • jrcs/letsencrypt-nginx-proxy-companion

    • 機能: nginx-proxy が検出したホストに対して、Let's Encrypt の証明書を取得・更新します。
    • 仕組み:
      • nginx-proxy の設定や対象ホスト情報を取得 (NGINX_PROXY_CONTAINER で指定)
      • 各コンテナの LETSENCRYPT_HOST / LETSENCRYPT_EMAIL 環境変数を読み取って対象ホストを判断
      • certbot を内部的に使って証明書を取得し、./docker/encrypt/certs に保存
      • nginx-proxy に証明書を適用するよう設定を反映

この構成による起動フローの概要は以下の通りです。

  1. nginx-proxy が起動し、他のコンテナを監視。
  2. letsencrypt-nginx が起動し、対象ホスト名を持つコンテナを監視。
  3. 新しいアプリコンテナが VIRTUAL_HOSTLETSENCRYPT_HOST を指定して起動。
    • 「各アプリ側のコンテナに必要な環境変数」を参照。
  4. nginx-proxy が設定を生成。
  5. letsencrypt-nginx が証明書を取得し、証明書ファイルと nginx 設定を更新。
  6. nginx が自動でリロードされ、HTTPS 対応のプロキシが完成。

構成に関する補足

  1. Docker ソケット共有 (/var/run/docker.sock)
    • これにより両コンテナは「現在起動している他のコンテナの情報」を読み取ることができます。
    • nginx-proxy は「どのホスト名がどのコンテナに向くべきか」を自動判別。
    • letsencrypt-nginx-proxy-companion は「どのホストに SSL を適用すべきか」を自動判別。
  2. 共有ボリューム proxy
    • Nginx の設定と証明書ファイルを共有します。
    • これにより、letsencrypt が取得した証明書が nginx-proxy に即時反映されます。
  3. 自動設定のトリガーは「環境変数」
    • 各アプリ側のコンテナに、環境変数を指定することで、nginx-proxy が自動的に設定を更新し、encrypt が証明書を取得します。

各アプリ側のコンテナに必要な環境変数

「構成に関する補足」の「3. 自動設定のトリガーは「環境変数」」で述べた、自動設定のトリガーとなる環境変数は以下の通りです。 各アプリ側のコンテナに以下の環境変数を設定すれば、nginx-proxy と letsencrypt-nginx-proxy-companion が自動的に設定を更新します。

    environment:
      - VIRTUAL_HOST=a.example.com # nginx-proxyのホスト設定 ※example.comを書き換えてください
      - LETSENCRYPT_HOST=a.example.com # Let's Encryptのホスト設定 ※example.comを書き換えてください
      - LETSENCRYPT_EMAIL=taro@example.com # Let's Encryptのメール設定
      - LETSENCRYPT_TEST=false # Let's Encryptのテストかどうかのフラグ 本番ではfalseにする (指定しないとテスト扱いになる)
      # - CERT_NAME=default # ローカルで認証局を立てるときに使う ※本番ではLet's Encryptから直接取得するのでコメントアウトする
      - HTTPS_METHOD=noredirect # リダイレクトを無効にする # 必要に応じて有効化
      - NETWORK_ACCESS=internal # 内部ネットワークアクセスを有効にする(IPアドレス制御をかける場合は必要)

コンテナの docker-compose.yml の設定例は以下のような感じです。

IP アクセス制御の考え方

テスト環境などですと、フル公開というのは避けたいです。IP アドレスによるアクセス制御がかけられると嬉しいものです。

この構成においては 2 つのレイヤで IP 制限をかけられます。

  1. プロキシ全体で共通ルールを適用する方法
    docker-compose.yml では ./network_internal.conf:/etc/nginx/network_internal.conf をマウントし、 NETWORK_ACCESS=internal を指定したコンテナに対して nginx-proxy が include /etc/nginx/network_internal.conf; を差し込みます。 nginx-proxy/network_internal.confallow/deny を記述すると、該当フラグを持つすべてのホストが同じリストで保護されます。 社内 CIDR のみ許可したい場合など、共通ルールを一箇所で管理できます。

  2. ホスト(振り分け先コンテナ)単位で個別ルールを適用する方法
    jwilder/nginx-proxy のテンプレートは /etc/nginx/vhost.d/<VIRTUAL_HOST> を各 server ブロック末尾で読み込みます(公式ドキュメント 参照)。 docker-compose.ymlnginx-proxyletsencrypt-nginx の両方で ./vhost.d:/etc/nginx/vhost.d をマウントしておけば、 ホスト側に ./vhost.d/testing1.example.com のようなファイルを置くだけで、そのホスト限定の allow/deny を差し込めます。 このファイルは testing1.example.com へのリクエストだけに適用され、他ホストには影響しません。 ファイルの中身は以下のような感じです。

# ./vhost.d/testing1.example.com
allow 202.1.111.0/24;
deny all;

共通ルールで大枠を閉じた上で、特定ホストだけ追加許可する場合などは、上記 2 つを組み合わせて使用します。

使い方

Docker ネットワークの作成

使用する Docker ネットワーク (shared) がない場合には、先に作成しておきます。

taro@v111-1111-111-111:/opt$ sudo docker network ls
Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg.
NETWORK ID    NAME        DRIVER
2f259bab93aa  podman      bridge
taro@v111-1111-111-111:/opt$ sudo docker network create shared
Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg.
shared
taro@v111-1111-111-111:/opt$ sudo docker network ls
Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg.
NETWORK ID    NAME        DRIVER
2f259bab93aa  podman      bridge
3266f375f203  shared      bridge

適切に記載された docker-compose.yml を使って起動します。

シツコイですが、便利ですので、一度試してみてください。