HTTPS 证书:申请、挂载与自动续期
2026/10/28OpsOpsnginxhttpscertbotletsencrypt大约 3 分钟约 841 字
占位符
| 写法 | 含义 |
|---|---|
【域名】 | 主站域名(与网关篇一致) |
【IP】 | 服务器公网 IP(DNS A 记录应指向这里) |
【邮箱】 | Let’s Encrypt 通知邮箱 |
路径固定:/root/deploy/nginx,用户:root。
先说场景
网关占住 80/443 后要上 HTTPS:浏览器信任的证、容器内能读到、到期自动续并 reload。
申请前检查
- DNS:
【域名】、www.【域名】A →【IP】 - 安全组 / 防火墙放行 80(及 443)
- Nginx 已配置
/.well-known/acme-challenge/→ 挂载的html - 不要另有进程抢 80
dig +short 【域名】
# 应等于 【IP】
curl -sSI http://【域名】/ | head申请方式(对照)
webroot(推荐,不停机):
certbot certonly --webroot \
-w /root/deploy/nginx/html \
-d 【域名】 -d www.【域名】 \
--email 【邮箱】 \
--agree-tos --non-interactive成功后宿主机有:
/etc/letsencrypt/live/【域名】/fullchain.pem
/etc/letsencrypt/live/【域名】/privkey.pemstandalone: 需先 docker stop nginx-gateway,Certbot 临时占 80,签完再 docker start。
通配符 *.【域名】: 必须 DNS-01,不能 HTTP-01。
挂载:禁止软链进容器
错误:ln -s /etc/letsencrypt/live/【域名】/... 到 certs/。容器只挂了 certs/,跟不到 /etc/letsencrypt,会报 BIO_new_file() ... No such file。
正确:删掉软链后 cp -L 成普通文件:
BASE=/root/deploy/nginx
DOMAIN='【域名】'
rm -f "$BASE/certs/$DOMAIN"/{fullchain,privkey}.pem
cp -L /etc/letsencrypt/live/$DOMAIN/fullchain.pem "$BASE/certs/$DOMAIN/fullchain.pem"
cp -L /etc/letsencrypt/live/$DOMAIN/privkey.pem "$BASE/certs/$DOMAIN/privkey.pem"
chmod 644 "$BASE/certs/$DOMAIN/fullchain.pem"
chmod 600 "$BASE/certs/$DOMAIN/privkey.pem"
docker exec nginx-gateway nginx -t && docker exec nginx-gateway nginx -s reloadNginx:
ssl_certificate /etc/nginx/certs/【域名】/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/【域名】/privkey.pem;校验
curl -sSI http://【域名】/ | head -15 # 301 → https
curl -sSI https://【域名】/ | head -15 # 200
echo | openssl s_client -connect 【域名】:443 -servername 【域名】 2>/dev/null \
| openssl x509 -noout -issuer -subject -dates
# issuer 含 Let's Encrypt服务端已对、浏览器仍「不安全」:多半在用 http、或以前信任过自签——无痕打开 https://【域名】 或重置站点权限。
一键完成(申请 + 拷贝 + 续期钩子)
DNS 已指向 【IP】、网关已按上一篇跑起来后,只改三行整段执行:
#!/usr/bin/env bash
set -euo pipefail
# ========== 只改这里 ==========
DOMAIN='【域名】' # 如 docs.example.com
EMAIL='【邮箱】' # 如 you@example.com
IP='【IP】' # 如 203.0.113.10;仅校验提示
# ==============================
BASE=/root/deploy/nginx
mkdir -p "$BASE/html" "$BASE/certs/$DOMAIN"
echo "检查 DNS: ${DOMAIN} 应解析到 ${IP}"
RESOLVED=$(dig +short "$DOMAIN" | tail -n1 || true)
echo "当前解析: ${RESOLVED:-<空>}"
apt-get update -qq && apt-get install -y -qq certbot >/dev/null
certbot certonly --webroot \
-w "$BASE/html" \
-d "$DOMAIN" -d "www.${DOMAIN}" \
--email "$EMAIL" \
--agree-tos --non-interactive \
--keep-until-expiring
rm -f "$BASE/certs/$DOMAIN/fullchain.pem" "$BASE/certs/$DOMAIN/privkey.pem"
cp -L "/etc/letsencrypt/live/${DOMAIN}/fullchain.pem" "$BASE/certs/$DOMAIN/fullchain.pem"
cp -L "/etc/letsencrypt/live/${DOMAIN}/privkey.pem" "$BASE/certs/$DOMAIN/privkey.pem"
chmod 644 "$BASE/certs/$DOMAIN/fullchain.pem"
chmod 600 "$BASE/certs/$DOMAIN/privkey.pem"
ls -la "$BASE/certs/$DOMAIN/"
docker exec nginx-gateway ls -la "/etc/nginx/certs/${DOMAIN}/"
docker exec nginx-gateway nginx -t
docker exec nginx-gateway nginx -s reload
# 续期后自动再拷贝 + reload
mkdir -p /etc/letsencrypt/renewal-hooks/deploy
cat > /etc/letsencrypt/renewal-hooks/deploy/copy-certs-nginx.sh <<EOF
#!/bin/sh
set -e
BASE=/root/deploy/nginx
DOMAIN=${DOMAIN}
rm -f "\$BASE/certs/\$DOMAIN/fullchain.pem" "\$BASE/certs/\$DOMAIN/privkey.pem"
cp -L "/etc/letsencrypt/live/\$DOMAIN/fullchain.pem" "\$BASE/certs/\$DOMAIN/fullchain.pem"
cp -L "/etc/letsencrypt/live/\$DOMAIN/privkey.pem" "\$BASE/certs/\$DOMAIN/privkey.pem"
chmod 644 "\$BASE/certs/\$DOMAIN/fullchain.pem"
chmod 600 "\$BASE/certs/\$DOMAIN/privkey.pem"
docker exec nginx-gateway nginx -s reload
EOF
chmod +x /etc/letsencrypt/renewal-hooks/deploy/copy-certs-nginx.sh
echo
echo "证书已挂载。自检:"
curl -sSI "https://${DOMAIN}/" | head -5
echo | openssl s_client -connect "${DOMAIN}:443" -servername "${DOMAIN}" 2>/dev/null \
| openssl x509 -noout -issuer -subject -dates
echo "干跑续期: certbot renew --dry-run"webroot 失败时,可改用 standalone 段(脚本内可手动):
docker stop nginx-gateway
certbot certonly --standalone -d "$DOMAIN" -d "www.${DOMAIN}" \
--email "$EMAIL" --agree-tos --non-interactive
# 再执行上面的 rm/cp/chmod/reload,然后
docker start nginx-gateway静态站怎么发到 html/,见下一篇。
