Docker サーバー管理

Dockerのnginxコンテナが80番を使っているサーバーで、ホスト側のcertbotが standalone 方式で登録されていると、更新のたびに Could not bind TCP port 80 で失敗します。初回取得時にnginxを止めて取った証明書がそのまま残っている構成です。直し方は、nginxに /.well-known/acme-challenge/ を配信させて certbot certonly --webroot を1回流すことで、これだけで renewal 設定も webroot に書き換わります。

失敗しているかは journal と renewal 設定で確認する

journalctl -u certbot --since '7 days ago' | grep -i 'failed to renew'
grep authenticator /etc/letsencrypt/renewal/example.com.conf

失敗している場合、1つ目にこのメッセージが出ます。

Failed to renew certificate example.com with error: Could not bind TCP port 80 because it is already in use by another process on this system (such as a web server). Please stop the program in question and then try again.

2つ目が authenticator = standalone なら原因は確定です。certbot.timer は動いているので、期限の30日前から毎日更新を試みて、そのたびにこのエラーで失敗します。期限は openssl x509 -enddate -noout -in /etc/letsencrypt/live/example.com/cert.pem で見ておきます。

nginxにACMEチャレンジ用のパスを配信させる

certbot が書き込むディレクトリをホストに用意し、nginxコンテナにマウントして、80番の server ブロックでそのパスだけ配信します。

# docker-compose.yml(nginx サービス)
volumes:
  - /etc/letsencrypt:/etc/letsencrypt:ro
  - ./certbot/www:/var/www/certbot
# nginx の 80 番 server ブロック
location /.well-known/acme-challenge/ {
    root /var/www/certbot;
}
location / {
    return 301 https://$host$request_uri;
}

HTTPSへのリダイレクトより前にこの location を置くのが要点です。設定を反映したら、certbot を動かす前にホストから疎通を確かめます。

mkdir -p ./certbot/www/.well-known/acme-challenge
echo ok > ./certbot/www/.well-known/acme-challenge/probe
curl -s -w ' %{http_code}\n' http://example.com/.well-known/acme-challenge/probe
rm ./certbot/www/.well-known/acme-challenge/probe

ok 200 と返れば配信できています。301 が返るならリダイレクトの location が先に当たっています。

dry-runを成功させてから本番更新する

certbot certonly --webroot -w /path/to/certbot/www -d example.com --dry-run --non-interactive
certbot certonly --webroot -w /path/to/certbot/www -d example.com --force-renewal --non-interactive
grep -E 'authenticator|webroot' /etc/letsencrypt/renewal/example.com.conf

The dry run was successful. が出たら本番を流します。--force-renewal は期限に関係なく今すぐ更新するためで、同時に renewal 設定が書き換わります。3つ目の出力が次のようになっていれば、以後の自動更新は webroot 方式で実行されます。

authenticator = webroot
webroot_path = /path/to/certbot/www,
[[webroot_map]]
example.com = /path/to/certbot/www

証明書ファイルはシンボリックリンクの差し替えで更新されるので、nginxには読み直しを指示します。

docker exec nginx-container nginx -s reload
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -enddate
certbot renew --dry-run

最後の renew --dry-run が成功すれば、certbot.timer による次回以降の自動更新も webroot 方式で成功します。期限12日前に切り替えた実測では、期限が90日先まで延びました。reload を自動化したい場合は /etc/letsencrypt/renewal-hooks/deploy/docker exec ... nginx -s reload を書いたスクリプトを置きます。