在管理个人服务器和各种内网服务的过程中,HTTPS 证书的自动续签一直是个绕不开的话题。很多同学(包括我自己)喜欢利用青龙面板的定时任务结合 acme.sh 来实现自动化。但在实际部署中,往往会遇到几个非常抓狂的“暗坑”:
面板假死: 脚本明明执行完了,邮件也收到了,但青龙面板的日志界面左上角一直转圈,任务永远无法结束。
莫名暴毙: 开启了 Bash 严格模式(
set -e)后,脚本在判断“证书未过期跳过”时直接异常中断。跨服部署难: 申请下来的证书无法优雅地推送到远程目标服务器并重载 Nginx。
经过反复的底层排查和逻辑重构,我终于写出了两套堪称“坚如磐石”的自动化续签脚本。一套用于本地部署,一套用于跨服务器推送。今天就把这套终极方案分享出来。

核心痛点与解决思路
在放出脚本之前,先简单聊聊为什么以前的脚本会出问题。
但如果你直接照搬网上的普通脚本扔进青龙运行,大概率会遇到每天重复申请证书、脚本无限转圈挂起、依赖安装超时报错等一系列高血压问题。
经过几天的反复测试和深度排查,我终于摸索出了一套在青龙面板下完美运行 acme.sh 并通过 SCP 自动推送到远端 Nginx 的“终极方案”。今天就把这份防坑指南完整记录下来。
1. 容器重启,数据火葬场(重复申请地狱)
网上很多教程会把 acme.sh 安装在默认的 ~/.acme.sh(也就是容器的 /root/.acme.sh)下。 致命后果:青龙容器的 /root 目录默认是不持久化的。一旦容器重启或升级,本地的证书历史记录就会灰飞烟灭。下次定时任务执行时,acme.sh 查不到本地记录,就会无视证书还有 80 多天有效期,强行向 CA 机构重新申请。长期以往,极易触发 Let's Encrypt 或 ZeroSSL 的速率限制(Rate Limit),导致账号被封禁。
2. Alpine 源的网络玄学(依赖安装超时)
很多脚本喜欢在开头写一段 apk update && apk add ... 来自动安装 curl、socat 等依赖。 致命后果:青龙底层是 Alpine Linux,国内网络连 Alpine 官方源经常抽风。一旦 apk update 卡住超时,整个续签脚本直接宣告失败。
3. 幽灵进程与无限转圈(面板卡死)
有时候网络波动导致 acme.sh 内部的 curl 请求卡死,或者跨服务器的 SSH/SCP 连接断开,如果没有做好严谨的进程管控,这个任务就会在青龙面板里无限“运行中”,也就是经典的转圈圈,甚至吃满 CPU。
准备工作
第一步:弃用脚本安装,改用青龙面板持久化依赖
把专业的事交给专业的工具。不要再让脚本每次去跑 apk add 了,直接在青龙面板里配置,一劳永逸。
进入青龙面板 -> 依赖管理 -> Linux。
点击右上角“添加依赖”。
批量填入以下名称并安装:
Plaintext
bash coreutils socat openssl git curl openssh-client等待它们全部显示绿色打勾状态。就算以后容器销毁重建,青龙也会在启动时自动帮你装好。
第二步:配置跨服务器免密登录 (SSH Key)
因为我的证书是申请后要推送到另一台远端服务器(比如我的 Nginx 节点),所以需要持久化 SSH 密钥。 务必将私钥存放在青龙的持久化目录 /ql/data/ 下,例如 /ql/data/id_rsa_acme,并确保目标服务器已经添加了对应的公钥。
为了防止每次重新安装依赖,因此,需要将依赖托管到青龙面板

方案一:单机部署版 (适用于青龙面板与 Nginx 在同一台机器)
这个脚本适用于大多数一体化服务器环境(例如我用来管理 yanchang.cc 的主机)。它会在申请证书后,直接在宿主机进行配置重载。
环境准备
需要在青龙面板映射的 /ql/data/ 目录下准备好用于免密登录宿主机的 SSH 私钥 (id_rsa_acme)。
核心脚本
Bash
#!/bin/bash
##!name: 泛域名证书自动续签
##!desc: 自动续签 yanchang.cc (终极修复:彻底解决挂起与转圈问题及持久化丢失问题)
##!cron: 30 3 * * *
# ================= 核心优化:终极进程管控与断流 =================
set -euo pipefail
# 核弹级清理函数:杀光子孙进程,强制拔掉日志输出网线
cleanup() {
# 1. 清理临时文件
rm -f "/tmp/mail_$$.txt"
# 2. 终极绝杀:强行关闭标准输出(1)和标准错误(2)
# 这一步极其关键,立刻告诉青龙面板:“我彻底闭嘴了,立刻给我打勾!”
exec 1>&-
exec 2>&-
# 3. 忽略 TERM 信号,防止我们发信号时连自己一起杀掉导致脚本中断
trap '' TERM
# 4. 株连九族:向当前进程组(-$$)发送终止信号,清理所有儿子和孙子幽灵进程
kill -TERM -$$ 2>/dev/null || true
sleep 1 # 给进程 1 秒钟优雅退出的时间
kill -9 -$$ 2>/dev/null || true
}
# 绑定 EXIT 信号到 cleanup 函数
trap cleanup EXIT
# ================= 配置区域 =================
DOMAIN="yanchang.cc"
SSH_KEY_PATH="/ql/data/id_rsa_acme"
HOST_USER="yanchang"
HOST_IP="192.168.0.250"
RECEIVER_EMAIL="yanchang@yanchang.cc"
SSL_DIR="/data/ssl_output"
# 临时文件加上 PID 防止并发冲突
MAIL_TMP="/tmp/mail_$$.txt"
# ===========================================
# 定义发送邮件函数
send_email() {
local status="$1"
local content=$(echo -e "$2")
local subject="【青龙】yanchang的证书状态通知 - ${status}"
echo "📧 正在发送邮件: $subject"
if [ -z "${SMTP_SERVER:-}" ] || [ -z "${SMTP_EMAIL:-}" ] || [ -z "${SMTP_PASSWORD:-}" ]; then
echo "❌ 邮件配置缺失:SMTP_SERVER/SMTP_EMAIL/SMTP_PASSWORD 未定义"
return 1
fi
cat > "$MAIL_TMP" <<EOF
From: "yanchang.cc的证书监控" <$SMTP_EMAIL>
To: <$RECEIVER_EMAIL>
Subject: $subject
Content-Type: text/plain; charset=UTF-8
任务名称:泛域名证书自动续签
目标域名:$DOMAIN
执行时间:$(date "+%Y-%m-%d %H:%M:%S")
当前状态:$status
$content
-------------------------
此邮件由青龙面板自动发送
EOF
# 优化:增加 -k 5s,如果 30 秒还没结束,再等 5 秒直接发 SIGKILL 强杀
if ! timeout -k 5s 30s curl --silent --show-error --ssl-reqd \
--url "smtps://$SMTP_SERVER:465" \
--user "$SMTP_EMAIL:$SMTP_PASSWORD" \
--mail-from "$SMTP_EMAIL" \
--mail-rcpt "$RECEIVER_EMAIL" \
--upload-file "$MAIL_TMP"; then
echo "❌ 邮件发送失败"
return 1
fi
return 0
}
# 1. 检查核心环境变量
if [ -z "${Ali_Key:-}" ] || [ -z "${Ali_Secret:-}" ]; then
send_email "配置错误" "❌ 未检测到 Ali_Key 或 Ali_Secret 环境变量。" || true
exit 1
fi
# 2. 检查 SSH 密钥
if [ ! -f "$SSH_KEY_PATH" ]; then
msg="❌ 未找到持久化 SSH 密钥:$SSH_KEY_PATH\n请按照教程在容器内生成密钥并配置到宿主机。"
echo -e "$msg"
send_email "环境错误" "$msg" || true
exit 1
fi
# 3. 检查核心依赖 (已交由青龙面板持久化管理)
echo "🛠️ 正在检查系统依赖..."
MISSING_DEPS=""
# 检查几个最核心的命令是否存在
for dep in bash socat curl git openssl ssh; do
if ! command -v $dep >/dev/null 2>&1; then
MISSING_DEPS="$MISSING_DEPS $dep"
fi
done
if [ -n "$MISSING_DEPS" ]; then
msg="❌ 缺少必要的系统依赖:$MISSING_DEPS\n请前往青龙面板网页端 ->【依赖管理】->【Linux】,添加以下依赖:\nbash coreutils socat openssl git curl openssh-client"
echo -e "$msg"
send_email "依赖缺失" "$msg" || true
exit 1
else
echo "✅ 系统核心依赖检查通过"
fi
# 4. 预创建证书部署目录
if [ ! -d "$SSL_DIR" ]; then
echo "📁 创建证书目录:$SSL_DIR"
mkdir -p "$SSL_DIR" || { send_email "目录创建失败" "❌ 无法创建证书目录 $SSL_DIR"; exit 1; }
fi
# 5. 准备 acme.sh (修复:更改为青龙持久化目录)
export ACME_DIR="/ql/data/acme"
if [ ! -f "$ACME_DIR/acme.sh" ]; then
echo "⬇️ acme.sh 未找到,正在安装到持久化目录..."
rm -rf /tmp/acme.sh
timeout -k 10s 120s git clone https://gitee.com/neilpang/acme.sh.git /tmp/acme.sh >/dev/null 2>&1 || { send_email "安装失败" "git clone acme.sh 失败"; exit 1; }
cd /tmp/acme.sh || exit 1
# 强制指定安装目录为持久化目录
./acme.sh --install --home "$ACME_DIR" --nocron -m "${SMTP_EMAIL:-default@example.com}" >/dev/null 2>&1
cd ~ || exit 1
else
echo "✅ acme.sh 已安装"
fi
export PATH="$ACME_DIR:$PATH"
# 6. 注册账户 (容错处理) (修复:指定持久化配置目录)
timeout -k 5s 30s acme.sh --register-account --home "$ACME_DIR" -m "${SMTP_EMAIL:-default@example.com}" --server zerossl >/dev/null 2>&1 || true
# 7. === 核心:申请证书 ===
echo "🚀 开始检查证书状态..."
ISSUE_EXIT_CODE=0
# 增加 -k 参数防止死锁,并增加 --home 确保读取持久化历史记录
ISSUE_LOG=$(timeout -k 10s 180s bash "$ACME_DIR/acme.sh" --issue --home "$ACME_DIR" --dns dns_ali -d "$DOMAIN" -d "*.$DOMAIN" 2>&1) || ISSUE_EXIT_CODE=$?
CLEAN_LOG=$(echo "$ISSUE_LOG" | grep -v "integer expected" | grep -v "out of range" || true)
# 8. === 逻辑判断 ===
if [[ "$ISSUE_LOG" == *"Skipping"* ]] || [[ "$ISSUE_LOG" == *"Domains not changed"* ]]; then
echo "✅ 证书无需更新 (Skipped)"
NEXT_TIME=$(echo "$ISSUE_LOG" | grep -oE "Next renewal time is:.*" | head -n 1 || true)
if [ -z "$NEXT_TIME" ]; then
NEXT_TIME=$(echo "$CLEAN_LOG" | tail -n 3)
fi
send_email "未更新(有效期内)" "✅ 证书依然有效,跳过更新。\n\n[ $NEXT_TIME ]" || true
exit 0
fi
if [ $ISSUE_EXIT_CODE -ne 0 ]; then
echo "❌ acme.sh 执行出错"
echo "$CLEAN_LOG"
send_email "执行失败" "❌ 证书申请错误,日志:\n\n$CLEAN_LOG" || true
exit 1
fi
echo "✅ 证书已重新申请,开始部署..."
RELOAD_CMD="ssh -i $SSH_KEY_PATH -o StrictHostKeyChecking=no -o ConnectTimeout=10 $HOST_USER@$HOST_IP 'sudo systemctl reload nginx'"
DEPLOY_EXIT_CODE=0
# 修复:部署时带上 --home,防止 acme.sh 找不到刚刚申请的证书
INSTALL_LOG=$(timeout -k 10s 120s acme.sh --install-cert --home "$ACME_DIR" -d "$DOMAIN" \
--key-file "$SSL_DIR/www.yanchang.key" \
--fullchain-file "$SSL_DIR/www.yanchang.pem" \
--reloadcmd "$RELOAD_CMD" 2>&1) || DEPLOY_EXIT_CODE=$?
if [ $DEPLOY_EXIT_CODE -eq 0 ]; then
echo "🎉 部署完成"
send_email "已更新(Success)" "🎉 泛域名证书已成功续签并重载 Nginx!\n\n[部署日志]:\n$INSTALL_LOG" || true
else
echo "❌ 部署失败"
echo "$INSTALL_LOG"
send_email "更新失败(Deploy Error)" "⚠️ 证书申请成功,但部署或 Nginx 重载失败。\n\n[错误日志]:\n$INSTALL_LOG" || true
exit 1
fi方案二:跨服务器 SCP 推送版 (适用于多节点集群管理)
随着设备的增多,我的目标是将青龙面板打造成一个中心化的“证书分发大脑”。比如给远程的 yanchang.pw 服务器续签时,我们就需要利用 scp 跨服推送文件,然后再触发远程 Nginx 的重启。
关键差异
引入了本地临时目录
/tmp/ssl_output_$DOMAIN。通过
scp将证书安全地推送到目标主机的/etc/nginx/ssl。防冗余机制: 如果证书还在有效期(触发
Skipping),不仅不会申请,也绝对不会发起无意义的 SCP 传输和 Nginx 重载。
核心脚本
Bash
#!/bin/bash
##!name: yanchang.pw 泛域名证书续签
##!desc: 自动续签 *.yanchang.pw (修复跨服务器SCP推送问题及持久化依赖问题)
##!cron: 30 3 * * *
# ================= 核心优化:终极进程管控与断流 =================
set -euo pipefail
cleanup() {
rm -f "/tmp/mail_$$.txt"
rm -rf "/tmp/ssl_output_$DOMAIN" # 清理本地临时证书
exec 1>&-
exec 2>&-
trap '' TERM
kill -TERM -$$ 2>/dev/null || true
sleep 1
kill -9 -$$ 2>/dev/null || true
}
trap cleanup EXIT
# ================= 配置区域 =================
DOMAIN="yanchang.pw"
SSH_KEY_PATH="/ql/data/id_rsa_acme"
HOST_USER="yanchang"
HOST_IP="8.137.122.123"
RECEIVER_EMAIL="yanchang@yanchang.cc"
SSL_DIR="/etc/nginx/ssl"
MAIL_TMP="/tmp/mail_$$.txt"
# ===========================================
# 定义发送邮件函数
send_email() {
local status="$1"
local content=$(echo -e "$2")
local subject="【青龙】yanchang证书状态通知 - ${status}"
echo "📧 正在发送邮件: $subject"
if [ -z "${SMTP_SERVER:-}" ] || [ -z "${SMTP_EMAIL:-}" ] || [ -z "${SMTP_PASSWORD:-}" ]; then
echo "❌ 邮件配置缺失:SMTP_SERVER/SMTP_EMAIL/SMTP_PASSWORD 未定义"
return 1
fi
cat > "$MAIL_TMP" <<EOF
From: "yanchang.pw的证书监控" <$SMTP_EMAIL>
To: <$RECEIVER_EMAIL>
Subject: $subject
Content-Type: text/plain; charset=UTF-8
任务名称:yanchang.pw 泛域名证书续签
目标域名:$DOMAIN
目标主机:$HOST_IP
执行时间:$(date "+%Y-%m-%d %H:%M:%S")
当前状态:$status
$content
-------------------------
此邮件由青龙面板自动发送
EOF
if ! timeout -k 5s 30s curl --silent --show-error --ssl-reqd \
--url "smtps://$SMTP_SERVER:465" \
--user "$SMTP_EMAIL:$SMTP_PASSWORD" \
--mail-from "$SMTP_EMAIL" \
--mail-rcpt "$RECEIVER_EMAIL" \
--upload-file "$MAIL_TMP"; then
echo "❌ 邮件发送失败"
return 1
fi
return 0
}
# 1. 检查核心环境变量
if [ -z "${Ali_Key:-}" ] || [ -z "${Ali_Secret:-}" ]; then
send_email "配置错误" "❌ 未检测到 Ali_Key 或 Ali_Secret 环境变量。" || true
exit 1
fi
# 2. 检查 SSH 密钥
if [ ! -f "$SSH_KEY_PATH" ]; then
msg="❌ 未找到持久化 SSH 密钥:$SSH_KEY_PATH"
echo -e "$msg"
send_email "环境错误" "$msg" || true
exit 1
fi
# 3. 检查核心依赖 (已交由青龙面板持久化管理)
echo "🛠️ 正在检查系统依赖..."
MISSING_DEPS=""
# 检查这几个最核心的命令是否存在(包含了推送需要的 scp 和 ssh)
for dep in bash socat curl git openssl scp ssh; do
if ! command -v $dep >/dev/null 2>&1; then
MISSING_DEPS="$MISSING_DEPS $dep"
fi
done
if [ -n "$MISSING_DEPS" ]; then
msg="❌ 缺少必要的系统依赖:$MISSING_DEPS\n请前往青龙面板网页端 ->【依赖管理】->【Linux】,添加以下依赖:\nbash coreutils socat openssl git curl openssh-client"
echo -e "$msg"
send_email "依赖缺失" "$msg" || true
exit 1
else
echo "✅ 系统核心依赖检查通过"
fi
# 4. 准备 acme.sh (修复:更改为青龙持久化目录)
export ACME_DIR="/ql/data/acme"
if [ ! -f "$ACME_DIR/acme.sh" ]; then
echo "⬇️ acme.sh 未找到,正在安装到持久化目录..."
rm -rf /tmp/acme.sh
timeout -k 10s 120s git clone https://gitee.com/neilpang/acme.sh.git /tmp/acme.sh >/dev/null 2>&1 || exit 1
cd /tmp/acme.sh || exit 1
# 强制指定安装目录为持久化目录
./acme.sh --install --home "$ACME_DIR" --nocron -m "${SMTP_EMAIL:-default@example.com}" >/dev/null 2>&1
cd ~ || exit 1
else
echo "✅ acme.sh 已安装"
fi
export PATH="$ACME_DIR:$PATH"
# 注册账号时也强制指定工作目录
timeout -k 5s 30s acme.sh --register-account --home "$ACME_DIR" -m "${SMTP_EMAIL:-default@example.com}" --server zerossl >/dev/null 2>&1 || true
# 5. === 核心:申请证书 ===
echo "🚀 开始检查证书状态..."
ISSUE_EXIT_CODE=0
# 修复:加上 --home 参数确保读取持久化目录的历史记录
ISSUE_LOG=$(timeout -k 10s 180s bash "$ACME_DIR/acme.sh" --issue --home "$ACME_DIR" --dns dns_ali -d "$DOMAIN" -d "*.$DOMAIN" 2>&1) || ISSUE_EXIT_CODE=$?
CLEAN_LOG=$(echo "$ISSUE_LOG" | grep -v "integer expected" | grep -v "out of range" || true)
# 6. === 逻辑判断 ===
if [[ "$ISSUE_LOG" == *"Skipping"* ]] || [[ "$ISSUE_LOG" == *"Domains not changed"* ]]; then
echo "✅ 证书无需更新 (Skipped)"
NEXT_TIME=$(echo "$ISSUE_LOG" | grep -oE "Next renewal time is:.*" | head -n 1 || true)
[ -z "$NEXT_TIME" ] && NEXT_TIME=$(echo "$CLEAN_LOG" | tail -n 3)
send_email "未更新(有效期内)" "✅ 证书依然有效,跳过更新,未触发服务重载。\n\n[ $NEXT_TIME ]" || true
exit 0
fi
if [ $ISSUE_EXIT_CODE -ne 0 ]; then
echo "❌ acme.sh 执行出错"
send_email "执行失败" "❌ 证书申请错误,日志:\n\n$CLEAN_LOG" || true
exit 1
fi
# 7. === 关键修复:跨服务器推送证书 ===
echo "✅ 证书已重新申请,开始通过 SCP 推送至远端服务器..."
# 7.1 将证书提取到青龙容器的临时目录
LOCAL_SSL_DIR="/tmp/ssl_output_$DOMAIN"
mkdir -p "$LOCAL_SSL_DIR"
# 修复:加上 --home 参数确保能找到刚刚生成的证书
acme.sh --install-cert --home "$ACME_DIR" -d "$DOMAIN" \
--key-file "$LOCAL_SSL_DIR/$DOMAIN.key" \
--fullchain-file "$LOCAL_SSL_DIR/$DOMAIN.pem" >/dev/null 2>&1
DEPLOY_EXIT_CODE=0
INSTALL_LOG=""
# 7.2 通过 SCP 将临时目录的证书传送到目标主机的 /etc/nginx/ssl
echo "📤 正在上传 .key 和 .pem 文件..."
scp -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=no -o ConnectTimeout=10 "$LOCAL_SSL_DIR/$DOMAIN.key" "$HOST_USER@$HOST_IP:$SSL_DIR/$DOMAIN.key" 2>&1 || DEPLOY_EXIT_CODE=$?
scp -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=no -o ConnectTimeout=10 "$LOCAL_SSL_DIR/$DOMAIN.pem" "$HOST_USER@$HOST_IP:$SSL_DIR/$DOMAIN.pem" 2>&1 || DEPLOY_EXIT_CODE=$?
# 7.3 推送成功后,重载远端 Nginx
if [ $DEPLOY_EXIT_CODE -eq 0 ]; then
echo "🔄 文件上传成功,准备重载远端 Nginx..."
RELOAD_CMD="sudo systemctl reload nginx"
INSTALL_LOG=$(ssh -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=no -o ConnectTimeout=10 "$HOST_USER@$HOST_IP" "$RELOAD_CMD" 2>&1) || DEPLOY_EXIT_CODE=$?
else
INSTALL_LOG="SCP 上传文件失败。请检查远端目录 $SSL_DIR 是否已赋予 $HOST_USER 写入权限 (sudo chown yanchang:yanchang $SSL_DIR)。"
fi
if [ $DEPLOY_EXIT_CODE -eq 0 ]; then
echo "🎉 部署完成"
send_email "已更新(Success)" "🎉 泛域名证书已成功续签,推送到目标服务器并成功重载 Nginx!\n\n[部署日志]:\n推送与重载均执行成功。" || true
else
echo "❌ 部署失败"
send_email "更新失败(Deploy Error)" "⚠️ 证书申请成功,但推送或重载失败。\n\n[错误日志]:\n$INSTALL_LOG" || true
exit 1
fi给新手的特别提醒
如果你使用的是非 Root 普通用户进行跨服部署,请务必在目标服务器上做好两件事:
赋予该用户操作
/etc/nginx/ssl目录的写入权限 (sudo chown -R 用户名:用户名 /etc/nginx/ssl)。使用
visudo为该用户配置 Nginx 的免密重启权限,否则脚本会在远端卡死在等待输入密码的环节。
结语
通过这两套脚本,我们彻底告别了青龙面板假死和意外中断的玄学问题。如果你有几十个分散在不同云厂商的域名,这套“中枢分发”架构能帮你省下极大的运维精力。折腾 HomeLab 的乐趣,不正是看着机器有条不紊地替我们干活嘛!