Let's Encrypt Docker 部署与定时续期教程
使用 Docker + Certbot 部署 Let's Encrypt 证书,并通过定时脚本实现无限自动续期,彻底告别 90 天过期焦虑。 适用:单台 Linux 服务器(物理机 / VPS / 云主机),Nginx / Caddy / 其他服务均可。
目录
- 一、核心原理
- 二、环境准备
- 三、方案 A:Certbot 独立模式(推荐入门)
- 四、方案 B:Webroot 模式(与 Nginx 共存,推荐生产)
- 五、定时脚本实现无限续期
- 六、证书自动部署到应用
- 七、证书与目录结构
- 八、验证续期是否正常
- 九、常见问题排查
- 十、安全与最佳实践
- 附:完整 docker-compose.yml(方案 B 生产版)
一、核心原理
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)
没有要显示的评论
没有要显示的评论