SSL 憑證自動更新設定指南


問題背景

Let’s Encrypt SSL 憑證有效期為 90 天,需要定期更新。當 Nginx 運行在 Docker 容器內時,certbot 預設的 standalone 模式會因為 port 80 被佔用而無法更新憑證。

解決方案:Webroot 模式

使用 webroot 模式讓 certbot 透過檔案驗證來更新憑證,不需要停止 Nginx 服務。

運作原理

  1. Let’s Encrypt 會訪問 http://your-domain/.well-known/acme-challenge/{token}
  2. Certbot 在指定目錄建立驗證檔案
  3. 驗證成功後更新憑證
  4. Nginx reload 載入新憑證

設定步驟

1. 建立 certbot 驗證目錄

sudo mkdir -p /var/www/certbot/.well-known/acme-challenge
sudo chown -R root:root /var/www/certbot
sudo chmod -R 755 /var/www/certbot

2. Nginx 設定

確保 Nginx 設定檔包含 Let’s Encrypt 驗證路徑:

server {
    listen 80;
    server_name your-domain.com;

    # Let's Encrypt challenge
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    # Redirect all HTTP to HTTPS
    location / {
        return 301 https://$server_name$request_uri;
    }
}

3. Docker Compose 設定

在 nginx service 加入 certbot 目錄掛載:

nginx:
  volumes:
    - ./docker/nginx/production.conf:/etc/nginx/conf.d/default.conf:ro
    - /etc/letsencrypt:/etc/letsencrypt:ro
    - /var/lib/letsencrypt:/var/lib/letsencrypt:ro
    - /var/www/certbot:/var/www/certbot:ro  # 新增這行

4. Certbot renewal 設定檔

位置:/etc/letsencrypt/renewal/your-domain.conf

正確格式範例:

# renew_before_expiry = 30 days
version = 2.9.0
archive_dir = /etc/letsencrypt/archive/your-domain.com
cert = /etc/letsencrypt/live/your-domain.com/cert.pem
privkey = /etc/letsencrypt/live/your-domain.com/privkey.pem
chain = /etc/letsencrypt/live/your-domain.com/chain.pem
fullchain = /etc/letsencrypt/live/your-domain.com/fullchain.pem

# Options used in the renewal process
[renewalparams]
account = your-account-id
authenticator = webroot
webroot_path = /var/www/certbot
server = https://acme-v02.api.letsencrypt.org/directory
key_type = ecdsa

[[webroot_map]]
your-domain.com = /var/www/certbot

注意: serverkey_type 必須在 [[webroot_map]] 之前,順序很重要!

5. 設定自動更新 Cron Job

sudo crontab -e

加入:

0 3 * * * certbot renew --quiet --deploy-hook "docker exec kg_nginx nginx -s reload"

常用指令

用途指令
檢查憑證狀態sudo certbot certificates
測試更新sudo certbot renew --dry-run
手動更新sudo certbot renew
強制重新申請sudo certbot certonly --webroot -w /var/www/certbot -d your-domain.com --force-renewal
查看誰佔用 Port 80sudo lsof -i :80

疑難排解

Port 80 already in use

  • 原因: Docker 容器佔用了 port 80
  • 解決: 使用 webroot 模式,不需要停止服務

renewal config file is broken / parsefail

  • 原因: 設定檔格式錯誤
  • 檢查: authenticator 是否重複、webroot_path 後面是否多逗號、參數順序是否正確

No such file or directory

  • 原因: certbot 目錄不存在或權限不足
  • 解決: sudo mkdir -p /var/www/certbot/.well-known/acme-challenge && sudo chmod -R 755 /var/www/certbot

緊急修復(憑證已過期)

docker stop kg_nginx
sudo certbot renew
docker start kg_nginx

架構圖

                  ┌─────────────────┐
                  │  Let's Encrypt  │
                  └────────┬────────┘
                           │ GET /.well-known/acme-challenge/{token}

┌──────────────────────────────────────────────────────────┐
│                      Host Server                          │
│                                                          │
│  ┌──────────────┐         ┌──────────────────────────┐  │
│  │   Certbot    │         │   Docker (kg_nginx)      │  │
│  │              │         │   ┌──────────────────┐   │  │
│  │ writes token │         │   │      Nginx       │   │  │
│  │      │       │         │   │                  │   │  │
│  │      ▼       │  mount  │   │ serves challenge │   │  │
│  │/var/www/certbot ◄─────►│   │ from /var/www/   │   │  │
│  │              │         │   │     certbot      │   │  │
│  └──────────────┘         │   └──────────────────┘   │  │
│                           └──────────────────────────┘  │
└──────────────────────────────────────────────────────────┘

學到的重點

  1. Let’s Encrypt 憑證有效期 90 天,必須設定自動更新
  2. Standalone vs Webroot 模式: Docker 環境下應使用 webroot 模式避免 port 衝突
  3. Certbot 設定檔順序很重要: [renewalparams] 的參數必須在 [[webroot_map]] 之前
  4. Docker volume 掛載: 讓容器內的 Nginx 可以存取 host 上的驗證檔案
  5. Cron job + deploy-hook: 自動更新後自動 reload Nginx 載入新憑證