私的サーバー構築日誌：nginx on docker のリバースプロキシ環境で Let's Encrypt

# 能書き

[私的サーバー構築日誌：仕切り直し](https://qiita.com/katz_engineer/items/1f21de2bd9072c260ae6)からの自宅サーバー構築の続きです。

表題通りです。[前回は nginx on docker でリバースプロキシを立ててみました](https://qiita.com/katz_engineer/items/d05537ffd9a8212c59be)が、この環境下で Let's Encrypt の証明書を取得し、維持する手順を模索します。

調べてみましたが、似たような内容の記事は案外少なかったですね。特に維持の手順。私も手順を考えてみましたが、どうもDockerとは馴染みにくい方法しか思い付きませんでした。そして試行錯誤の挙句、ホストマシンのcronとの組合せになりました。

# 目標

- Dockerコンテナで立てたnginxリバースプロキシを前提とする
- リバースプロキシに証明書を持たせて、上流(upstream)にはHTTPで接続。
- 証明書を Let's Encrypt で取得（certbotを利用）
- 証明書を定期的に自動更新する。

# 概要

証明書の取得と維持の為に、リバースプロキシのDockerコンテナに```certbot```をインストールしてしまいます。後は標準的な手順通り。

本当は、褒められた方法ではない事を承知で、Dockerコンテナに```cron```を入れたかったのですが。実際にやってみると、リバースプロキシが動かなくなります。困ったものです。

そういう訳で、コマンドの定期起動は、ホストマシンの```cron```設定で実現する事にします。

こういう事をすると後で忘れるので避けたかったんですけどね。リバースプロキシだけでLXDコンテナにするのも大袈裟に過ぎる気がしますし。残念です。

# 参考文献

- [nginxによるバーチャルホスト/リバースプロキシによる Let's Encrypt 証明書の取得方法など - Qiita](https://qiita.com/murachi1208/items/c2435e6bfff13d496017)
- [SSL証明書の自動更新。 - 無料のSSL証明書Let’s Encryptを設定・更新・自動更新する方法(CentOS7, Apache2.4対応） - プログラミング入門ナビ by Proglus（プログラス）](https://blog.proglus.jp/105/#SSL-3)
- [私的サーバー構築日誌：nginx on docker でリバースプロキシ - Qiita](https://qiita.com/katz_engineer/items/d05537ffd9a8212c59be)
- [私的サーバー構築日誌：LAN内DNSサーバー Unbound - Qiita](https://qiita.com/katz_engineer/items/f2198e5cb7b222d4180e) … 我が家の家庭内DNSです。バーチャルホストを動かす為に、少し追記が必要になります。
- [【cron】改めてcronの設定方法について勉強し直してみた - とーますメモ](https://thoames.hatenadiary.jp/entry/2018/12/30/101021) … ```/etc/cron.d/```に置く設定ファイルは、拡張子を付けてはいけないようです。
- [Docker Composeでログの設定をする - Qiita](https://qiita.com/Hanamichi/items/5be05452146f0f31b3bd)

Dockerコンテナで```cron```を使う方法は、今回はやめましたが。参考文献だけ一応挙げておきます。

- [Docker コンテナで cron を使う - Docker コンテナ内でタスクを cron 起動する - Qiita](https://qiita.com/rerofumi/items/fc0126c4e985b78f769b#docker-%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A%E3%81%A7-cron-%E3%82%92%E4%BD%BF%E3%81%86)
- [Dockerコンテナ内でcronを実行し実行ログを出力する方法 - ゲンゾウ用ポストイット](https://genzouw.com/entry/2022/10/11/201931/3116/)
- [Dockerコンテナの中身のタイムゾーンを日本時刻にする方法 - My CMS](https://crypto-nft-fire.com/docker-timezone/)

# 実験環境を構築

[前回と同じ実験環境](https://qiita.com/katz_engineer/items/d05537ffd9a8212c59be)を構築します。リバースプロキシにおいて若干の手順変更がある為、改めて全手順を掲載します。

## インターネット設定

外のインターネットから、我が家のサーバーにアクセスできるように設定します。まずはDNS、それから家庭用ルーターのポートフォワーディングですね。80番ポートと443番ポートです。これらは環境によって手順が異なりますので、各々の手順に従って設定をお願いします。

そして設定の内容を変数に設定します。

ドメインは、ここでは仮に```example.com```にしていますが、お金を出して購入した自分だけのドメインを設定します。くれぐれもこのまま使用しないで下さい。

```console:ホスト側
MY_DOMAIN=example.com
```

```console:ホスト側
TEST_DOMAIN=test.$MY_DOMAIN
```

我が家のサーバーの、家庭内LANでのIPアドレスは```172.16.1.2```です。

```console:ホスト側
HOST_IPADDR=172.16.1.2
```

## 家庭内DNS

最初に、家庭内DNSを設定してしまいます。

```console:ホスト側
cd /etc/unbound/unbound.conf.d/
cat | sudo tee -a machines.list >/dev/null <<___
local-data: "$TEST_DOMAIN. 3600000 IN A $HOST_IPADDR"
___
```

```console:ホスト側
unbound-checkconf
sudo systemctl restart unbound
```

## Webサーバー

Webサーバーを立てます。基本的な設定ばかりなので、解説無しで一気に行きます。

```console:ホスト側
TEST_PORT=20080
```

```console:ホスト側
cd
mkdir docker-nginx1
cd docker-nginx1
mkdir -p config/nginx
mkdir -p data/nginx
mkdir html
```

```console:ホスト側
cat <<___ >docker-compose.yml
version: "3.9"
services:
  nginx1:
    image: nginx:latest
    container_name: nginx1
    restart: unless-stopped
    ports:
      - $TEST_PORT:80
    volumes:
      - ./config/nginx:/etc/nginx/conf.d
      - ./data/nginx:/var/log/nginx
      - ./html:/usr/share/nginx/html
___
cat <<___ >config/nginx/test.conf
server {
    listen        80;
    server_name   _;
    server_tokens off;

    location / {
        root   /usr/share/nginx/html;
        index  index.html;
    }
}
___
```

```console:ホスト側
cat <<___ >html/index.html
<!DOCTYPE html>
<html lang="ja">
 <head>
 <meta charset="UTF-8" />
 <title>nginx test</title>
</head>
<body>
 <h1>test</h1>
 <p>hello,world</p>
</body>
</html>
___
```

```console:ホスト側
docker compose up -d
```

動作確認。

```console:ホスト側
curl http://$HOST_IPADDR:$TEST_PORT/
```

## リバースプロキシ

リバースプロキシですが、概要に書いた通り、```certbot```をインストールします。

```console:ホスト側
cd
mkdir docker-proxy
cd docker-proxy
mkdir -p config/nginx
mkdir -p data/nginx
mkdir -p html/ssl-proof
mkdir letsencrypt
```

```console:ホスト側
cat <<___ >docker-compose.yml
version: "3.9"
services:
  proxy:
    build: .
    container_name: proxy
    restart: unless-stopped
    ports:
      - 80:80
      - 443:443
    volumes:
      - ./config/nginx:/etc/nginx/conf.d
      - ./data/nginx:/var/log/nginx
      - ./html:/usr/share/nginx/html
      - ./letsencrypt:/etc/letsencrypt
    logging:
      driver: json-file
      options:
        max-size: 10m
        max-file: '3'
___
```

```console:ホスト側
cat <<___ >Dockerfile
FROM nginx:latest
RUN apt update \
 && apt install -y certbot \
 && apt -y clean
___
```

```console:ホスト側
cat <<___ >config/nginx/default.conf
server {
    listen        80 default_server;
    server_name   _;
    server_tokens off;

    location / {
        root   /usr/share/nginx/html;
        index  index.html;
    }
}
___
```

```console:ホスト側
cat <<___ >config/nginx/test.conf
server {
    listen        80;
    server_name   $TEST_DOMAIN;
    server_tokens off;

    location ^~ /.well-known/acme-challenge/ {
        root   /usr/share/nginx/html/ssl-proof;
    }

    location / {
        proxy_pass http://$HOST_IPADDR:$TEST_PORT;
    }
}
___
```

```console:ホスト側
docker compose up -d
```

動作確認。

```console:ホスト側
curl http://$TEST_DOMAIN/
```

# certbotで証明書を取得

ここからが本番ですね。まずは証明書取得の手順です。```certbot```を実行します。

```console:ホスト側
docker compose exec proxy certbot certonly --register-unsafely-without-email --agree-tos --webroot -w /usr/share/nginx/html/ssl-proof -d $TEST_DOMAIN
```

証明書等は下記の場所に格納されています。但し権限が```root```で設定されています。

- 公開鍵： ```~/docker-proxy/letsencrypt/live/$TEST_DOMAIN/fullchain.pem```
- 秘密鍵： ```~/docker-proxy/letsencrypt/live/$TEST_DOMAIN/privkey.pem```

```console:ホスト側
cd
cd docker-proxy
sudo ls -l letsencrypt/live/$TEST_DOMAIN
```

こんな感じです。

```console:ホスト側
$ sudo ls -l letsencrypt/live/$TEST_DOMAIN
total 9
-rw-r--r-- 1 root root 692 Nov 28 13:50 README
lrwxrwxrwx 1 root root  42 Nov 28 13:50 cert.pem -> ../../archive/test.example.com/cert1.pem
lrwxrwxrwx 1 root root  43 Nov 28 13:50 chain.pem -> ../../archive/test.example.com/chain1.pem
lrwxrwxrwx 1 root root  47 Nov 28 13:50 fullchain.pem -> ../../archive/test.example.com/fullchain1.pem
lrwxrwxrwx 1 root root  45 Nov 28 13:50 privkey.pem -> ../../archive/test.example.com/privkey1.pem
```

証明書の中身を確認してみましょう。

```console:ホスト側
sudo openssl x509 -text -in letsencrypt/live/$TEST_DOMAIN/fullchain.pem | grep -E "Issuer:|Subject:"
```

```console:ホスト側
sudo openssl x509 -dates -noout -in letsencrypt/live/$TEST_DOMAIN/fullchain.pem
```

# リバースプロキシに証明書を設定

転送の部分に証明書を設定します。そして80番ポートは、証明書の保守の為に残しておきます。

```console:ホスト側
cat <<___ >config/nginx/test.conf
server {
    listen        80;
    server_name   $TEST_DOMAIN;
    server_tokens off;

    location ^~ /.well-known/acme-challenge/ {
        root /usr/share/nginx/html/ssl-proof;
    }

    location / {
        return 301 http://$TEST_DOMAIN;
    }
}
server {
    listen        443 ssl;
    server_name   $TEST_DOMAIN;
    server_tokens off;

    ssl_certificate     /etc/letsencrypt/live/$TEST_DOMAIN/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/$TEST_DOMAIN/privkey.pem;

    location / {
        proxy_pass http://$HOST_IPADDR:$TEST_PORT;
    }
}
___
```

nginxを再起動します。

```console:ホスト側
docker compose exec proxy nginx -s reload
```

動作確認。

```console:ホスト側
curl https://$TEST_DOMAIN/
```

# 自動更新

証明書更新の設定です。

```console:ホスト側
cd
cd docker-proxy
cat <<___ >certbot-renew.sh
#!/bin/bash
cd
cd docker-proxy
/usr/bin/date -Iseconds
/usr/bin/docker compose exec proxy certbot renew
/usr/bin/docker compose exec proxy nginx -s reload
echo "====="
___
chmod +x certbot-renew.sh
sudo ln -s $PWD/certbot-renew.sh /usr/local/bin/certbot-renew.sh
```

ログに関しては、```logger```コマンドはやめました。標準出力と標準エラー出力をローカルにリダイレクトするだけにしました。それを```logrotate```で回します。

```console:ホスト側
sudo mkdir /var/log/letsencrypt
```

```console:ホスト側
sudo chown $USER /var/log/letsencrypt
```

```console:ホスト側
cat <<___ | sudo tee /etc/logrotate.d/certbot-renew >/dev/null
/var/log/letsencrypt/certbot-renew.log {
    missingok
    rotate 3
    compress
    monthly
    minsize 1M
}
___
```

Let's Encrypt の証明書は有効期限が90日ですので、月に2回、毎月1日と15日の午前3時に更新チェックを走らせます。残り日数が30日以上かどうかを見て更新すべきかどうか自動的に判断してくれるようなので。

なお、```/etc/cron.d```に設定するファイルは、拡張子は無しにしないとダメのようです。

```console:ホスト側
cat <<___ | sudo tee /etc/cron.d/certbot-renew >/dev/null
0 3 1,15 * * $USER /usr/local/bin/certbot-renew.sh >>/var/log/letsencrypt/certbot-renew.log 2>&1
___
```

後は実際の動作を確認するだけですね。ちょっと息の長い話になります。

```console:ホスト側
cd
cd docker-proxy
sudo ls -l letsencrypt/live/$TEST_DOMAIN/
```

```console:ホスト側
cat /var/log/letsencrypt/certbot-renew.log
```

# 仕舞い

今回も手順のチェックが目的です。動作確認が終わったら、全部綺麗に消しましょう。

```console:ホスト側
sudo rm /etc/cron.d/certbot-renew
```

```console:ホスト側
sudo rm /etc/logrotate.d/certbot-renew
```

```console:ホスト側
sudo rm /usr/local/bin/certbot-renew.sh
```

```console:ホスト側
sudo rm -r /var/log/letsencrypt
```

```console:ホスト側
cd
cd docker-proxy
docker compose down --rmi all --volumes
```

```console:ホスト側
cd
cd docker-nginx1
docker compose down --rmi all --volumes
```

```console:ホスト側
cd
sudo rm -r docker-proxy docker-nginx1
```

```console:ホスト側
sudo sed -i -e"/$TEST_DOMAIN./d" /etc/unbound/unbound.conf.d/machines.list
```

```console:ホスト側
unbound-checkconf
sudo systemctl restart unbound
```

今回は Let's Encrypt で証明書を取得し、証明書を維持する、それをリバースプロキシの環境下で設定する手順を確認できました。