纯公网 IP 申请 HTTPS 证书指南 (Docker + Lego 自动续期)
最近折腾一些内部服务,不想绑定域名,直接用 IP 访问,但浏览器那个“不安全”的红色警告看着实在难受。于是研究了一下怎么给公网 IP 申请免费的 SSL 证书。
这里必须要提一个重磅消息:Let’s Encrypt 官方已于本月(2025年12月19日)正式支持公网 IP 证书签发。这意味着我们不再需要去凑合使用 HiCA 或 ZeroSSL,直接用官方接口即可,稳得一匹。
本教程使用 Docker + Lego 工具来实现。这套方案最大的优点是兼容机器上现有的 Nginx/Caddy 等服务——只有在真正需要续期的那几秒钟才会短暂占用 80 端口,平时互不干扰。
1. 搞定环境:Docker
有 Docker 的跳过这一步。没装的跑一下命令:
非大陆服务器 (Github 直连):
Bash
curl -fsSL https://get.docker.com | sh
国内服务器 (加速镜像):
Bash
bash <(curl -sSL https://linuxmirrors.cn/docker.sh)
2. 首次手动申请
为了不污染宿主机环境,我们直接拉个 Lego 容器来跑。因为申请证书需要验证 80 端口,所以如果你的 80 端口被占用了(比如跑着 Nginx),得先停一下。
# 1. 暂停现有的 Web 服务 (根据你的实际情况改,没有就跳过)
# systemctl stop nginx
# 2. 定义一下变量,方便后面复用
DATA_DIR="$HOME/lego"
IP=$(curl -s -4 ip.sb) # 自动获取公网IP
# 3. 启动 Lego 容器申请证书
# 这里的 --email 记得改成你自己的,虽然乱填也能过,但最好填真实的
docker run --rm -it \
-v "$DATA_DIR":/.lego \
-p 80:8888 \
goacme/lego \
--email="example@gmail.com" \
--accept-tos \
--server="https://acme-v02.api.letsencrypt.org/directory" \
--http \
--http.port=":8888" \
--key-type="ec384" \
--domains="$IP" \
--disable-cn \
run --profile "shortlived" \
&& echo -e "\n✅ 申请成功! 证书位置如下:" \
&& echo "证书 (Public): $DATA_DIR/certificates/$IP.crt" \
&& echo "私钥 (Private): $DATA_DIR/certificates/$IP.key"
# 4. 恢复 Web 服务
# systemctl restart nginx
小贴士:拿到证书后,赶紧配置到你的 Nginx 或面板里去,测试一下 HTTPS 访问是否正常。
3. 编写智能续期脚本 (核心步骤)
Lego 虽然自带 renew 命令,但不管三七二十一直接停 Web 服务有点太暴力了。
所以在论坛找了个脚本:先检查证书有效期,只有剩余时间不足 3 天时,才停止 Nginx 进行续期。最大程度保证服务在线率。
创建脚本:nano $HOME/auto_ssl.sh
#!/bin/bash
# --- 核心配置区 ---
# 证书存储目录 (务必使用绝对路径)
DATA_DIR="$HOME/lego"
EMAIL="example@gmail.com"
# 触发续期的剩余天数 (建议设为 3-7 天)
RENEW_DAYS=3
# 自动获取本机 IP
IP=$(curl -s -4 ip.sb)
CERT_PATH="$DATA_DIR/certificates/$IP.crt"
# --- 核心逻辑 ---
check_validity() {
# 如果证书都没了,那必须续期
if [ ! -f "$CERT_PATH" ]; then
echo "❌ 未找到证书文件: $CERT_PATH"
return 1
fi
# 计算剩余天数
EXP_DATE=$(openssl x509 -enddate -noout -in "$CERT_PATH" | cut -d= -f2)
EXP_EPOCH=$(date -d "$EXP_DATE" +%s)
NOW_EPOCH=$(date +%s)
LEFT_SECONDS=$((EXP_EPOCH - NOW_EPOCH))
LEFT_DAYS=$((LEFT_SECONDS / 86400))
echo "📅 证书剩余有效期: $LEFT_DAYS 天 (续期阈值: $RENEW_DAYS 天)"
if [ "$LEFT_DAYS" -le "$RENEW_DAYS" ]; then
return 1 # 时间不够了,要续期
else
return 0 # 时间还早,继续睡
fi
}
# 1. 检查阶段
if check_validity; then
echo "✅ 证书坚挺中,无需操作,Web 服务保持运行。"
exit 0
fi
# 2. 续期阶段 (只有返回 1 才会走到这里)
echo "⚠️ 证书即将过期,开始执行续期流程..."
echo "🛑 正在暂停 Web 服务以释放 80 端口..."
# --- 在这里修改你的停止命令 ---
systemctl stop nginx
# ---------------------------
echo "🚀 启动 Docker 容器进行续期..."
docker run --rm \
-v "$DATA_DIR":/.lego \
-p 80:8888 \
goacme/lego \
--email="$EMAIL" \
--path="/.lego" \
--server="https://acme-v02.api.letsencrypt.org/directory" \
--http --http.port=":8888" \
--domains="$IP" \
--disable-cn \
renew --profile "shortlived" --days "$RENEW_DAYS" --reuse-key --no-random-sleep
LEGO_STATUS=$?
echo "▶️ 正在恢复 Web 服务..."
# --- 在这里修改你的启动命令 ---
systemctl start nginx
# ---------------------------
# 3. 报告结果
if [ $LEGO_STATUS -eq 0 ]; then
echo "🎉 续期成功,服务已恢复。"
else
echo "❌ Lego 续期失败,请检查日志!Web 服务已尝试重启。"
fi
记得给脚本加执行权限:
chmod +x $HOME/auto_ssl.sh
4. 设个闹钟 (Crontab)
让服务器每天早上 5 点自己检查一遍。
输入 crontab -e,添加一行:
代码段
# 每天凌晨 5:00 执行 IP 证书续期检查
0 5 * * * /bin/bash /root/auto_ssl.sh >> /root/lego_renew.log 2>&1
(注意:请将 /root/auto_ssl.sh 替换为你脚本的实际绝对路径)
💡 避坑指南
- 路径问题:脚本里所有的路径(
DATA_DIR等)一定要用绝对路径,Crontab 执行时的环境和我不一样。 - Web 服务冲突:脚本里的
systemctl stop nginx是核心,如果你用的不是 Nginx(比如 Caddy、Apache 或者 Docker 跑的 Nginx),记得把那两行命令改掉。 - 生效验证:续期成功后,虽然证书文件更新了,但你的 Web 服务器可能加载的还是内存里的旧证书。务必确保脚本里的重启/重载命令是有效的。
搞定收工!以后这个 IP 的 HTTPS 就不用操心了。