跳转到主要内容

Let's Encrypt Docker 部署与定时续期教程

使用 Docker + Certbot 部署 Let's Encrypt 证书,并通过定时脚本实现无限自动续期,彻底告别 90 天过期焦虑。 适用:单台 Linux 服务器(物理机 / VPS / 云主机),Nginx / Caddy / 其他服务均可。


目录


一、核心原理

1.1 为什么需要自动续期

Let's Encrypt 证书有效期只有 90 天。虽然可手动续,但遗忘 = 网站 HTTPS 瘫痪。官方强烈建议自动化续期,这也是 Certbot 默认行为。

1.2 整体架构

┌────────────────────────────────────────────────────────┐
│  宿主机(Linux)                                        │
│                                                         │
│  ┌──────────────┐    共享卷    ┌──────────────────┐    │
│  │ Certbot 容器  │ ◄────────── │  Nginx / 其他服务  │    │
│  │ (acme 客户端) │  /etc/lets │  (读取证书)        │    │
│  └──────┬───────┘  encrypt    └────────┬─────────┘    │
│         │                              │              │
│         │ ACME 挑战 (HTTP-01)          │              │
│         ▼                              ▼              │
│  :80 (/.well-known/acme-challenge)   :443 (HTTPS)     │
└─────────┼──────────────────────────────┼──────────────┘
          │                              │
          ▼                              ▼
      Let's Encrypt             客户端浏览器
      (acme-v02.api.letsencrypt.org)

1.3 两种验证方式对比

方式 原理 优点 缺点 适用
HTTP-01(本教程) 在 http://域名/.well-known/acme-challenge/ 放验证文件 简单通用 需 80 端口可达 推荐
DNS-01 在 DNS 添加 TXT 记录 可签通配符 *.example.com、无需开放端口 需 DNS API 权限 通配符 / 内网服务

本教程以最通用的 HTTP-01 为主,DNS-01 仅在第 9 节提及思路。


二、环境准备

2.1 前置条件

  • Linux 服务器(Ubuntu 22.04 / Debian 12 / CentOS 7+ 均可)
  • 已安装 Docker 20+ 与 Docker Compose v2
  • 一个已解析到服务器公网 IP 的域名(A 记录)
  • 服务器 80 端口对外可达(Let's Encrypt 会访问验证)
  • 防火墙放行 80(HTTP)、443(HTTPS)

2.2 确认环境

docker --version
docker compose version

# 确认域名解析正确
dig +short wiki.example.com
# 或
nslookup wiki.example.com

2.3 创建专用目录

sudo mkdir -p /opt/letsencrypt
cd /opt/letsencrypt

# 证书与续期日志目录
sudo mkdir -p ./certbot/www ./certbot/logs

三、方案 A:Certbot 独立模式(推荐入门)

适用:服务器没有其他服务占用 80 端口,或愿意临时停掉占用 80 的服务。

3.1 单条命令申请证书

docker run --rm \
  -p 80:80 \
  -v /opt/letsencrypt/certbot/etc:/etc/letsencrypt \
  -v /opt/letsencrypt/certbot/www:/var/www/html \
  --name certbot \
  certbot/certbot:latest \
  certonly --standalone \
    --email your@email.com \
    --agree-tos \
    --no-eff-email \
    -d wiki.example.com \
    -d www.example.com

参数说明:

参数 说明
-p 80:80 映射 80 端口供 ACME 验证
-v ...:/etc/letsencrypt 持久化证书、账户、续期配置
-v ...:/var/www/html standalone 模式验证文件目录
certonly 只获取证书,不安装(自己配置 Nginx)
--standalone 独立模式(Certbot 自带临时 web 服务)
--email 注册邮箱(重要通知、过期提醒)
--agree-tos 同意 Let's Encrypt 服务条款
--no-eff-email 不订阅 EFF 邮件
-d 域名(可多个,多 -d 即 SAN 多域名证书)

3.2 申请成功标志

看到以下输出即成功:

Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/wiki.example.com/fullchain.pem
   /etc/letsencrypt/live/wiki.example.com/privkey.pem

3.3 一键脚本(方案 A)

cat > /opt/letsencrypt/issue-standalone.sh << 'EOF'
#!/bin/bash
set -e

DOMAINS="-d wiki.example.com -d www.example.com"
EMAIL="your@email.com"

docker run --rm \
  -p 80:80 \
  -v /opt/letsencrypt/certbot/etc:/etc/letsencrypt \
  -v /opt/letsencrypt/certbot/www:/var/www/html \
  --name certbot \
  certbot/certbot:latest \
  certonly --standalone \
    --email ${EMAIL} \
    --agree-tos \
    --no-eff-email \
    ${DOMAINS}
EOF

chmod +x /opt/letsencrypt/issue-standalone.sh

四、方案 B:Webroot 模式(与 Nginx 共存,推荐生产)

适用:80 端口已被 Nginx 占用,不想停服务。Certbot 把验证文件放到 Nginx 的 webroot,Let's Encrypt 通过正常 HTTP 访问验证。

4.1 思路

Nginx 配置一个通用 location,把 /.well-known/acme-challenge/ 指向 Certbot 可写的目录,Certbot 用 --webroot 模式把验证文件放进去,全程无需停 Nginx。

4.2 Nginx 配置(关键片段)

server {
    listen 80;
    server_name wiki.example.com www.example.com;

    # Let's Encrypt 验证路径
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;   # 与 Certbot 容器挂载的 www 目录对应
    }

    # 其他路径重定向到 HTTPS
    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    server_name wiki.example.com;

    ssl_certificate     /etc/letsencrypt/live/wiki.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/wiki.example.com/privkey.pem;

    # ... 其他配置
}

4.3 申请证书(Webroot 模式)

docker run --rm \
  -v /opt/letsencrypt/certbot/etc:/etc/letsencrypt \
  -v /opt/letsencrypt/certbot/www:/var/www/html \
  --name certbot \
  certbot/certbot:latest \
  certonly --webroot \
    --webroot-path=/var/www/html \
    --email your@email.com \
    --agree-tos \
    --no-eff-email \
    -d wiki.example.com \
    -d www.example.com

关键:-v .../www:/var/www/html 让宿主机的 /opt/letsencrypt/certbot/www 同时被 Nginx(作 root /var/www/certbot 需调整,见下文)和 Certbot 访问。

目录对应澄清(重要):

Certbot 在容器内写 /var/www/html/.well-known/acme-challenge/xxx,映射到宿主机 /opt/letsencrypt/certbot/www/。因此 Nginx 的 root 应指向宿主机同一路径,用 bind mount 让 Nginx 容器也挂这个目录:

# 在 Nginx 服务中加
volumes:
  - /opt/letsencrypt/certbot/www:/var/www/certbot

则 Nginx 配置:

location /.well-known/acme-challenge/ {
    root /var/www/certbot;
}

4.4 完整 docker-compose.yml 见附录


五、定时脚本实现无限续期

这是本教程的核心。Certbot 内置续期逻辑(检查到期 < 30 天则自动续),我们只需要定期触发它。

5.1 续期原理

  • Let's Encrypt 证书 90 天有效
  • Certbot 只在剩余 ≤ 30 天时才真正续期,否则提示 Congratulations, no action taken —— 这是正常的
  • 因此每天跑一次 renew 是安全且推荐的做法

5.2 续期脚本

sudo tee /opt/letsencrypt/renew.sh > /dev/null << 'EOF'
#!/bin/bash
# ============================================================
# Let's Encrypt 自动续期脚本
# 配合 crontab 每日执行;仅当证书剩余 < 30 天时才真正续期
# ============================================================
set -e

CERTBOT_ETC="/opt/letsencrypt/certbot/etc"
CERTBOT_WWW="/opt/letsencrypt/certbot/www"
LOG_FILE="/opt/letsencrypt/certbot/logs/renew.log"

mkdir -p "$(dirname "${LOG_FILE}")"

echo "[$(date '+%Y-%m-%d %H:%M:%S')] 开始检查证书续期..." >> "${LOG_FILE}"

docker run --rm \
  -v "${CERTBOT_ETC}:/etc/letsencrypt" \
  -v "${CERTBOT_WWW}:/var/www/html" \
  --name certbot-renew \
  certbot/certbot:latest \
  renew \
    --webroot-path=/var/www/html \
    --quiet \
    --no-self-upgrade \
    --deploy-hook "echo DEPLOYED >> /etc/letsencrypt/deploy.log" \
    >> "${LOG_FILE}" 2>&1

echo "[$(date '+%Y-%m-%d %H:%M:%S')] 续期检查完成" >> "${LOG_FILE}"

# 若有证书被更新,则重载 Nginx 使新证书生效
if grep -q "DELAYED_RENEW" "${LOG_FILE}"; then
    echo "发现续期,重载应用..." >> "${LOG_FILE}"
    # 根据实际情况选择(二选一或都保留):
    # Docker Compose 方式:
    docker compose -f /opt/letsencrypt/docker-compose.yml exec -T nginx nginx -s reload || true
    # 直接 systemd 方式:
    # sudo systemctl reload nginx || true
fi

# 清理 30 天前的日志
find "$(dirname "${LOG_FILE}")" -name "renew*.log" -mtime +30 -delete
EOF

sudo chmod +x /opt/letsencrypt/renew.sh

📌 说明:

  • --quiet:无变化时静默,有变化时仍输出,避免邮件轰炸
  • --deploy-hook:仅在成功续期后执行,适合触发重载
  • 更稳妥的做法是把「重载」放到 --deploy-hook 里(见 5.4 优化版)

5.3 配置 crontab(每日自动执行)

# 编辑 root 的 crontab
sudo crontab -e

添加一行(每天凌晨 3:15 执行,避开整点高峰,Let's Encrypt 推荐随机分钟):

15 3 * * * /opt/letsencrypt/renew.sh

验证 crontab:

sudo crontab -l

5.4 优化版:用 --deploy-hook 精确触发重载(推荐)

只有真正续期成功才重载 Nginx,比日志判断更可靠:

sudo tee /opt/letsencrypt/renew.sh > /dev/null << 'EOF'
#!/bin/bash
set -e

CERTBOT_ETC="/opt/letsencrypt/certbot/etc"
CERTBOT_WWW="/opt/letsencrypt/certbot/www"
LOG_FILE="/opt/letsencrypt/certbot/logs/renew.log"

mkdir -p "$(dirname "${LOG_FILE}")"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 检查续期..." >> "${LOG_FILE}"

docker run --rm \
  -v "${CERTBOT_ETC}:/etc/letsencrypt" \
  -v "${CERTBOT_WWW}:/var/www/html" \
  --name certbot-renew \
  certbot/certbot:latest \
  renew \
    --webroot-path=/var/www/html \
    --quiet \
    --no-self-upgrade \
    --deploy-hook "nginx -s reload || true; echo reloaded >> /etc/letsencrypt/deploy.log" \
    >> "${LOG_FILE}" 2>&1

echo "[$(date '+%Y-%m-%d %H:%M:%S')] 完成" >> "${LOG_FILE}"
EOF

sudo chmod +x /opt/letsencrypt/renew.sh

⚠️ --deploy-hook 在容器内需能执行 nginx -s reload。若 Nginx 在另一个容器,推荐用 --deploy-hook 写标记文件 + 宿主机脚本检测,或直接用 docker compose restart nginx(有短暂中断,可接受)。更优雅见第六章。


六、证书自动部署到应用

证书续期后,如何让不重启就无法感知新证书的服务生效?

6.1 方案 1:Nginx 自动 reload(最常用)

Nginx 对 reload 是无缝热加载,不中断连接。--deploy-hook 中 nginx -s reload 即可。

若 Nginx 在独立容器且未与 Certbot 共享 PID namespace,用:

# 在宿主机检测标记文件
sudo tee /opt/letsencrypt/watch-deploy.sh > /dev/null << 'EOF'
#!/bin/bash
# 配合 --deploy-hook 写入标记文件,宿主机轮询重载
FLAG="/opt/letsencrypt/certbot/etc/deploy.renew"
if [ -f "${FLAG}" ]; then
    docker compose -f /opt/letsencrypt/docker-compose.yml exec -T nginx nginx -s reload
    rm -f "${FLAG}"
fi
EOF
chmod +x /opt/letsencrypt/watch-deploy.sh

把 deploy-hook 改为:touch /etc/letsencrypt/deploy.renew(映射到宿主机 certbot/etc/),cron 同时跑 watch-deploy.sh。

6.2 方案 2:共享证书卷(推荐,零中断)

让应用容器直接挂载 Certbot 的 live 目录,并配置 ssl_certificate 指向该挂载点。续期后只需 reload。

# docker-compose.yml 片段
services:
  nginx:
    image: nginx:alpine
    volumes:
      - /opt/letsencrypt/certbot/etc/live:/etc/letsencrypt/live:ro
      - /opt/letsencrypt/certbot/etc/archive:/etc/letsencrypt/archive:ro
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    ports:
      - "80:80"
      - "443:443"

live/ 里是指向 archive/ 中具体版本的符号链接,所以两个目录都要挂。archive 随续期更新,live 指针随之切换,reload 即生效。

6.3 方案 3:其他服务(Node、Caddy、Traefik、Docker Swarm)

  • Caddy:内置自动 HTTPS,根本不需要 Certbot —— 但如果要用,Caddy 可监听证书目录变化自动 reload
  • Traefik:内置 Let's Encrypt,强烈推荐替代本方案,自动续期开箱即用
  • Node/Go 自研服务:挂载 live/ 目录,用 node 的 setSecureContext + 文件 watcher 热替换,或定时重启
  • Docker Swarm / K8s:用 docker secret 或 cert-manager,不在本教程范围

七、证书与目录结构

/opt/letsencrypt/
├── docker-compose.yml
├── renew.sh                    # 续期脚本
├── watch-deploy.sh             # (可选) 部署触发
├── issue-standalone.sh        # (方案A) 首次申请
└── certbot/
    ├── www/                    # ACME 验证文件 (webroot)
    ├── logs/
    │   └── renew.log
    └── etc/                    # Certbot 主目录(重要!备份此目录)
        ├── accounts/           # Let's Encrypt 账户
        ├── archive/            # 证书历史版本(真实文件)
        ├── live/               # 当前证书(符号链接,供应用使用)
        ├── renewal/            # *.conf 续期配置(决定怎么续)
        └── deploy.log

7.1 查看证书信息

# 查看证书有效期
openssl x509 -in /opt/letsencrypt/certbot/etc/live/wiki.example.com/fullchain.pem \
  -noout -dates

# 输出示例:
# notBefore=Sep  1 00:00:00 2026 GMT
# notAfter=Dec  1 00:00:00 2026 GMT

# 或用 Certbot 命令
docker run --rm -v /opt/letsencrypt/certbot/etc:/etc/letsencrypt \
  certbot/certbot:latest certificates

7.2 备份

# 备份整个 etc 目录(含账户、私钥)
sudo tar czf /root/letsencrypt-backup-$(date +%F).tar.gz \
  /opt/letsencrypt/certbot/etc

# 建议纳入定期备份(如 rsnapshot / restic)

⚠️ 私钥 privkey.pem 极其敏感,备份务必加密存储。


八、验证续期是否正常

8.1 手动模拟续期(不实际签发)

# --dry-run 使用 Let's Encrypt 测试环境,安全无限制
docker run --rm \
  -v /opt/letsencrypt/certbot/etc:/etc/letsencrypt \
  -v /opt/letsencrypt/certbot/www:/var/www/html \
  certbot/certbot:latest \
  renew --webroot-path=/var/www/html --dry-run

看到 Congratulations, no action taken 或 dry run: ... success 即配置正确。

8.2 强制立即续期(测试用,生产勿频繁)

# 缩短证书有效期以触发(仅测试)
docker run --rm \
  -v /opt/letsencrypt/certbot/etc:/etc/letsencrypt \
  certbot/certbot:latest \
  renew --force-renewal --webroot-path=/var/www/html

8.3 监控到期时间(推荐加监控)

# 一行检查是否快到期(< 14 天报警)
EXP=$(openssl x509 -in /opt/letsencrypt/certbot/etc/live/wiki.example.com/fullchain.pem \
  -noout -enddate | cut -d= -f2)
EXP_EPOCH=$(date -d "${EXP}" +%s)
NOW=$(date +%s)
DAYS=$(( (EXP_EPOCH - NOW) / 86400 ))
echo "证书剩余 ${DAYS} 天"
[ "${DAYS}" -lt 14 ] && echo "⚠️ 即将过期,请检查续期任务!"

可集成到 Uptime Kuma / Prometheus / 企业微信告警。


九、常见问题排查

问题 原因 / 解决
Connection refused / Timeout 80 端口未开放、防火墙/安全组拦截、域名未解析到本机
Invalid response from http://.../.well-known/... webroot 目录权限/挂载不对,手动访问该 URL 看是否 404
too many certificates (5) already issued for this exact set of domains 测试过度触发每周 5 张相同证书限流,等一周或用 --dry-run 测试
Certbot will only run once a day 正常限速,不要高频跑;本地测试可用 --dry-run
续期成功但站点仍是旧证书 忘记 reload Nginx,检查 --deploy-hook 是否生效
Failed to renew certificate ... (failure) 查 /opt/letsencrypt/certbot/logs/renew.log 具体错误
证书快到期但 cron 没续 cron 未执行:检查 sudo crontab -l、脚本权限、/var/log/cron
多域名/通配符 HTTP-01 不支持通配符,改用 DNS-01(需 DNS 服务商 API)
内网/无公网 IP 服务 用 DNS-01 或 --manual,或用 Tailscale + 公网代理

9.1 调试技巧

# 1. 查看详细日志
docker run --rm -it \
  -v /opt/letsencrypt/certbot/etc:/etc/letsencrypt \
  -v /opt/letsencrypt/certbot/www:/var/www/html \
  certbot/certbot:latest \
  renew --webroot-path=/var/www/html --verbose

# 2. 确认 webroot 可被外部访问
curl -v http://wiki.example.com/.well-known/acme-challenge/test
# (先手动放一个 test 文件到 www/ 目录)

# 3. 检查 cron 是否执行
grep CRON /var/log/syslog    # Debian/Ubuntu
journalctl -u cron -f        # systemd

十、安全与最佳实践

10.1 推荐做法 ✅

  • 固定镜像版本:certbot/certbot:v2.11 而非 latest,避免意外升级
  • 最小权限:Certbot 容器无需 --privileged,仅挂载必要目录
  • 邮箱真实有效:过期提醒、安全通知靠它
  • 每日定时 + 随机分钟:避开整点,如 15 3 * * *
  • 续期后必须 reload:否则证书更新不生效
  • 备份 certbot/etc/:含账户和私钥,迁移/恢复必备
  • 监控到期:< 14 天告警,双保险

10.2 避免做法 ❌

  • ❌ 90 天快到期才手动续 —— 遗忘风险高
  • ❌ --force-renewal 高频执行 —— 触发限流
  • ❌ 把 live/ 单文件复制走而不带 archive/ —— 符号链接失效
  • ❌ 私钥 privkey.pem 提交到 Git / 公开仓库
  • ❌ 用 latest 且长期不更新 —— 失去安全修复

10.3 更现代的替代方案

如果你的架构允许,以下方案内置自动 HTTPS + 续期,省心程度远超手动 Certbot:

方案 特点
Caddy 一行 reverse_proxy 自动签 Let's Encrypt、自动续,强烈推荐
Traefik Docker 原生、自动发现服务、自动证书,K8s/Swarm 首选
Nginx Proxy Manager Web UI 管理,点几下自动 HTTPS
Cloudflare Origin CA 边缘代理 + 源站证书,免服务器 80 端口

选择建议:纯 Nginx 老架构 → 本教程 Certbot;新项目 → Caddy / Traefik。


附:完整 docker-compose.yml(方案 B 生产版)

version: "3.8"

services:
  certbot:
    image: certbot/certbot:latest
    container_name: certbot
    restart: unless-stopped
    volumes:
      - ./certbot/etc:/etc/letsencrypt
      - ./certbot/www:/var/www/html
      - ./certbot/logs:/var/log/letsencrypt
    # 仅首次申请时用 standalone 需要 80 端口;webroot 模式通常不需要前台运行
    # 作为定时任务容器,可设为不常驻,由 cron 触发 docker run --rm

  nginx:
    image: nginx:alpine
    container_name: nginx
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./certbot/etc/live:/etc/letsencrypt/live:ro
      - ./certbot/etc/archive:/etc/letsencrypt/archive:ro
      - ./certbot/www:/var/www/certbot:ro
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - certbot

配套的 nginx.conf(关键部分):

events { worker_connections 1024; }

http {
    server {
        listen 80;
        server_name wiki.example.com www.example.com;

        location /.well-known/acme-challenge/ {
            root /var/www/certbot;
        }
        location / {
            return 301 https://$host$request_uri;
        }
    }

    server {
        listen 443 ssl http2;
        server_name wiki.example.com;

        ssl_certificate     /etc/letsencrypt/live/wiki.example.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/wiki.example.com/privkey.pem;

        location / {
            proxy_pass http://bookstack:80;   # 你的后端服务
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
}

快速开始(TL;DR)

# 1. 准备目录
sudo mkdir -p /opt/letsencrypt/certbot/www /opt/letsencrypt/certbot/logs
cd /opt/letsencrypt

# 2. 首次申请(webroot,需 Nginx 已配好 acme-challenge location)
docker run --rm \
  -v $(pwd)/certbot/etc:/etc/letsencrypt \
  -v $(pwd)/certbot/www:/var/www/html \
  certbot/certbot:latest certonly --webroot \
  --webroot-path=/var/www/html --email you@example.com --agree-tos \
  -d wiki.example.com

# 3. 部署续期脚本 + cron(见第五章)
sudo tee renew.sh ...(见 5.4)
sudo crontab -e  # 加: 15 3 * * * /opt/letsencrypt/renew.sh

# 4. 测试
/opt/letsencrypt/renew.sh   # 手动跑一次,应提示 no action taken(正常)
docker run --rm -v $(pwd)/certbot/etc:/etc/letsencrypt certbot/certbot:latest renew --dry-run

参考资源

  • Let's Encrypt 官方:https://letsencrypt.org/docs/
  • Certbot 官方文档:https://eff-certbot.readthedocs.io/
  • Certbot Docker 镜像:https://hub.docker.com/r/certbot/certbot
  • ACME 协议 / HTTP-01 挑战:https://letsencrypt.org/docs/challenge-types/
  • 速率限制:https://letsencrypt.org/docs/rate-limits/
  • Caddy 自动 HTTPS:https://caddyserver.com/docs/automatic-https
  • Traefik Let's Encrypt:https://doc.traefik.io/traefik/https/acme/

更新于 2026-09-11 · 适用 Certbot v2.x / Let's Encrypt 现行策略(90 天有效期、ACME v2)