diff --git a/Dockerfile b/Dockerfile index a320a12..718bb0b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -54,6 +54,21 @@ RUN mkdir -p /out \ && cd /src/chrome && zip -qr /out/navi-chrome.zip . \ && cd /src/firefox && zip -qr /out/navi-firefox.xpi . +# ── Stage 3b: bundled acme.sh (for cert-agent) ── +# 为什么要内置:cert-agent 若在运行时安装 acme.sh,installer 会从 +# raw.githubusercontent.com 拉源码,而该域名在境内直连超时 +# (实测直连 8.2s 失败 / 经代理 0.53s 成功),导致首次启动静默失败。 +# 内置一份让 cert-agent 彻底不依赖运行时下载。 +FROM debian:bookworm-slim AS acmebuild + +RUN apt-get -o Acquire::Retries=5 install -y --no-install-recommends \ + git ca-certificates curl openssl \ + && rm -rf /var/lib/apt/lists/* + +RUN git clone --depth 1 https://github.com/acmesh-official/acme.sh /src/acme.sh \ + && chmod +x /src/acme.sh/acme.sh \ + && rm -rf /src/acme.sh/.git + # ── Stage 4: Runtime ── FROM debian:bookworm-slim @@ -103,6 +118,9 @@ COPY --from=extpack /out/navi-firefox.xpi /app/dashy/extension/navi-firefox.xpi # 容器内自动续签(cert-agent)。脚本本身不含任何凭据,凭据只从环境变量读。 COPY deploy/cert-agent.sh /usr/local/bin/cert-agent RUN chmod +x /usr/local/bin/cert-agent +# 内置 acme.sh:cert-agent 优先用它,避免运行时从 raw.githubusercontent.com +# 下载失败(该域名境内直连超时)。 +COPY --from=acmebuild /src/acme.sh /usr/local/share/acme.sh # Inject search script into Dashy COPY web/navi.js /app/dashy/navi.js diff --git a/deploy/cert-agent.sh b/deploy/cert-agent.sh index 8fadc74..c63e4c4 100755 --- a/deploy/cert-agent.sh +++ b/deploy/cert-agent.sh @@ -45,19 +45,45 @@ fi [ -d "$HOME_DIR" ] || mkdir -p "$HOME_DIR" [ -d "$CERT_DIR" ] || mkdir -p "$CERT_DIR" -# ── 1. 首次运行装 acme.sh(数据卷里已存在就跳过)── +# ── 1. 定位 acme.sh:镜像内置优先,运行时安装是兜底 ───────────────── +# 为什么不运行时安装:installer 内部从 raw.githubusercontent.com 取 acme.sh, +# 而该域名在境内直连会超时(实测直连 8.2s 失败、经 .60:7890 代理 0.53s 成功), +# 于是「首次启动装 acme.sh」这一步会静默失败,之后永远起不来。 +# 因此镜像里内置一份(Dockerfile 的 acmebuild stage),运行时只剩兜底路径。 +BUNDLED=/usr/local/share/acme.sh ACME="$HOME_DIR/acme.sh" + +if [ -x "$BUNDLED/acme.sh" ]; then + # 拷贝到数据卷:acme.sh 会在自己所在目录写 ca/ dnsapi/ 与证书状态, + # 放在只读的镜像层里会写失败。 + mkdir -p "$HOME_DIR" + cp -a "$BUNDLED/." "$HOME_DIR/" 2>/dev/null || true + [ -x "$ACME" ] || say "内置 acme.sh 拷贝失败,落到运行时安装路径" +fi + if [ ! -x "$ACME" ]; then - say "首次运行:安装 acme.sh 到 $HOME_DIR" - curl -fsSL https://get.acme.sh -o "$HOME_DIR/acme.sh.install" 2>>"$LOG" || { - say "下载 acme.sh 失败,跳过本次" + say "镜像内置 acme.sh 不可用,尝试运行时安装(需要能访问 raw.githubusercontent.com)" + # 兜底:需要代理时设 CERT_ACME_PROXY(容器内直连不通的环境)。 + PROXY_OPT="" + [ -n "${CERT_ACME_PROXY:-}" ] && PROXY_OPT="-x $CERT_ACME_PROXY" + INSTALLER="$HOME_DIR/acme.sh.install" + OK=0 + for i in 1 2 3; do + # shellcheck disable=SC2086 # PROXY_OPT 需按空格拆开 + if curl -fsSL -m 120 $PROXY_OPT https://get.acme.sh -o "$INSTALLER" 2>>"$LOG"; then + OK=1; break + fi + say "下载 get.acme.sh 第 $i 次失败,重试" + sleep $((i * 5)) + done + if [ "$OK" = "1" ]; then + sh "$INSTALLER" --home "$HOME_DIR" --nocron >>"$LOG" 2>&1 || say "安装 acme.sh 失败" + rm -f "$INSTALLER" + else + say "下载失败,跳过本次(下次循环重试)" exit 0 - } - sh "$HOME_DIR/acme.sh.install" --home "$HOME_DIR" --nocron >>"$LOG" 2>&1 || { - say "安装 acme.sh 失败,跳过本次" - exit 0 - } - rm -f "$HOME_DIR/acme.sh.install" + fi + [ -x "$ACME" ] || { say "安装后仍找不到 $ACME,跳过本次"; exit 0; } fi # ── 2. 从 conf/gateway.yml 读出要签的域名 ──────────────────────────── @@ -99,6 +125,24 @@ esac CA="letsencrypt" [ -n "${CERT_ACME_CA:-}" ] && CA="$CERT_ACME_CA" +# ★ 必须把 CERT_DNS_CRED 里的键值**导出成真实环境变量名**,acme.sh 的 +# dns_ali.sh 读的是 $Ali_Key / $Ali_Secret(不是我们的 CERT_DNS_CRED)。 +# 只挑出插件名而不导出凭据的表现是:DNS 步骤报 +# "Error adding TXT record to domain",而 ACME 其它步骤都正常 —— +# 很容易误判成"凭据无效/网络问题"。 +# 用 set -a? 不行(要过滤)。这里显式 eval 展开赋值。 +save_ifs="$IFS" +IFS=',' +for kv in $CERT_DNS_CRED; do + k="${kv%%=*}" + v="${kv#*=}" + case "$k" in + ''|*[!A-Za-z0-9_]*) ;; # 跳过空项与非法变量名 + *) export "$k=$v" ;; + esac +done +IFS="$save_ifs" + run_acme() { # 拼出 -d domain 列表。刻意不用 `set -- $DOMAINS`:那会改掉函数的位置参数。 DFLAGS=""