blog-demo · 运维与踩坑

Astro 静态站 ECS 运维手册

面向独立 Edge 后方静态站的日常检查、手工发布、回滚、故障排查与备份边界。

状态
维护中
最后核对
源码修订
e0d3baa2

Astro 静态站 ECS 运维手册

本手册用于维护已经切换到 ECS 的静态生产站,以及仅供发布验收的回环预览。当前唯一公开站点是 https://www.doyouhang.live;GitHub Pages 已退出维护和回退策略。

v1 的生产结构由两个仓库共同组成:私有 ai-infra 维护独立 doyouhang-edge,唯一发布 80/443、管理 TLS 和域名路由;本仓库的 blog-demo-production-web-1 只在外部 Docker 网络 doyouhang-edge 上提供 blog-web:80。远程 Studio 通过同一网络内的 studio-auth 认证入口访问,但与博客 Web 独立运行。博客发版不能重建 Edge、AI、Studio 或 oauth2-proxy。

连接和路径速查

项目 当前约定
SSH ssh -i SSH_KEY DEPLOY_USER@ECS_HOST
发布根目录 /opt/blog-demo/releases/
当前版本指针 /opt/blog-demo/current
生产环境配置 /etc/blog-demo/production.env
预发布环境配置 /etc/blog-demo/staging.env
Compose 基础文件 /opt/blog-demo/current/infra/compose.yaml
Compose 生产覆盖 /opt/blog-demo/current/infra/compose.production.yaml
生产容器 blog-demo-production-web-1
公网 Edge 容器 doyouhang-edge,由私有 ai-infra 维护
外部 Docker 网络 doyouhang-edge;博客别名 blog-web
生产入口 https://www.doyouhang.live/
预发布容器 infra-web-1
预发布入口 http://127.0.0.1:8080/,仅 ECS 本机或 SSH Tunnel
证书与 Certbot 属于独立 Edge;博客容器不挂载
Studio 配置 /etc/blog-studio/studio.env
Studio 私密状态 /var/lib/blog-studio/runtime
Studio 工作副本 /var/lib/blog-studio/worktree
受限部署器配置 /etc/blog-studio/deployer.env
受限部署器 Socket /run/blog-studio-deployer/deployer.sock,root:blog-studio-deploy 0660
权威线上状态 /var/lib/blog-studio-deployer/state/deployed.json,Studio 只读

私钥只留在可信电脑。不要把私钥、完整环境文件、访问令牌或带凭据的命令输出粘贴进 Issue、聊天记录和仓库。

安装远程 Studio 前的输入

以下值必须在生产变更窗口现场填写,不能使用示例占位符:唯一 GitHub 用户名、GitHub 已验证白名单邮箱、OAuth App client ID/secret、ACR registry/namespace/账号凭据、Studio GitHub 细粒度 token、部署器只读 GitHub token,以及首次通行密钥。OAuth callback 固定为 https://studio.doyouhang.live/oauth2/callback。

把 infra/studio.env.example 和 infra/deployer.env.example 复制到 /etc/blog-studio/ 后逐项填写。创建独立 blog-studio 用户、blog-studio-deploy 组,并让 Compose 中的 UID/GID 与宿主机一致。Studio 只加入 Socket 组,不加入 docker 组。

STUDIO_ALPINE_MIRROR 和 STUDIO_NPM_REGISTRY 默认留空,分别使用 Alpine 与 npm 官方源。中国大陆 ECS 若持续超时,可显式设为 https://mirrors.aliyun.com/alpine 和 https://registry.npmmirror.com;两者只影响 Studio 镜像构建,不进入运行时环境,也不能替代 lockfile。

秘密文件至少按三类隔离:oauth2-proxy 只读邮箱、OAuth secret 和 Cookie secret;Studio 只读恢复令牌、用于 Actions dispatch 的 GitHub token 和独立 GitHub SSH deploy key;部署器以 root 读取独立 GitHub 只读 token 和 ACR 凭据。每个文件禁止 other 权限,目录使用 0700;不要让 oauth2-proxy 或 Studio 能读取部署器凭据。部署前逐个执行 stat 核对 owner、group 和 mode,不在命令行打印内容。

当前仓库 remote 使用 SSH。把 infra/git-ssh-config.example 复制为 /etc/blog-studio/git-ssh/config,私钥保存为同目录 id_ed25519,并从 GitHub 官方来源预置 known_hosts;禁止 StrictHostKeyChecking=no。deploy key 只给这一仓库写权限,不复用个人 SSH 私钥。Actions API token 与 Git push 密钥保持分离。

allowed-emails 每行只有一个完整邮箱。不要在 oauth2-proxy 配置中同时加入 email_domains=["*"]:7.15.1 对域名与邮箱文件采用并集,星号会绕过单邮箱限制。OAuth client secret 文件不能带末尾换行;Cookie secret 必须是原始 16、24 或 32 字节,可用仓库脚本生成且不会把 secret 打印到终端:

sudo node /opt/blog-demo/current/scripts/studio-cookie-secret.mjs \
  /etc/blog-studio/secrets/oauth-cookie-secret

三个 oauth2-proxy 文件由容器 UID 65532 只读,建议 owner 设为 65532:65532、mode 0400。Studio 的恢复令牌、GitHub token、SSH 私钥和工作副本由 STUDIO_UID:STUDIO_GID 持有,秘密文件 0400、SSH 目录 0700、工作目录 0700。部署器 secret 保持 root:root 0600。实际 UID/GID 与模板不一致时先改环境文件,不能为了省事放宽到 0644。

固定工作流哈希必须来自已审阅的目标提交:

git show APPROVED_COMMIT:.github/workflows/content-release.yml | sha256sum

把结果写入 DEPLOYER_WORKFLOW_SHA256。部署器还会确认线上 SHA 到目标 SHA 的完整差异只有内容 Markdown、Studio 重编码的 JPEG、src/data/editorial.json 和 src/data/now.json;AVIF、HEIF、SVG、工作流或其他代码有变化时,快速发布必然拒绝。

安装 systemd 模板并检查,不要在尚未填写配置时启动:

sudo install -m 0644 infra/systemd/blog-studio-deployer.service /etc/systemd/system/
sudo install -m 0644 infra/systemd/blog-studio-deployer.socket /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemd-analyze verify /etc/systemd/system/blog-studio-deployer.service /etc/systemd/system/blog-studio-deployer.socket
sudo systemctl enable --now blog-studio-deployer.socket

先用四层 Compose 渲染结果,确认 Web、Studio 和 oauth2-proxy 都没有 ports、Docker Socket 或特权模式,并且 Web 与 oauth2-proxy 正确加入外部 Edge 网络:

sudo docker compose -p blog-demo-production \
  --env-file /etc/blog-studio/studio.env \
  -f infra/compose.yaml \
  -f infra/compose.production.yaml \
  -f infra/compose.studio.yaml \
  -f infra/compose.content-release.yaml \
  config

首次启用 compose.content-release.yaml 前,BLOG_IMAGE 必须填写为当前已验收生产镜像的不可变引用,不能用 latest。先创建 /var/lib/blog-studio-deployer/state,设置为 root:blog-studio-deploy 0750;初始化 deployed.json 时写入当前公网 /version.json 的 40 位提交和对应镜像摘要,设置为 root:blog-studio-deploy 0640。Studio 依靠补充组只读挂载,不能写。两者不一致就停止,不猜线上版本。

登录、通行密钥与 SSH 恢复

阿里云 DNS 增加 studio.doyouhang.live 指向现有 ECS A 记录即可,不需要 Cloudflare。先把 Studio 域名加入 Edge 证书,再让 oauth2-proxy 以 studio-auth 接入外部网络。验收至少包含:未知 Host 被拒绝、另一个真实 GitHub 账号无法进入、唯一账号可登录、退出后 Cookie 清除、Studio 容器无主机端口,以及上传上限和安全响应头。

生成一次性通行密钥登记令牌:

STUDIO_UID=$(sudo sed -n 's/^STUDIO_UID=//p' /etc/blog-studio/studio.env)
STUDIO_GID=$(sudo sed -n 's/^STUDIO_GID=//p' /etc/blog-studio/studio.env)
sudo node /opt/blog-demo/current/scripts/studio-recovery-token.mjs \
  /etc/blog-studio/secrets/studio-recovery-token "$STUDIO_UID" "$STUDIO_GID"

命令只显示一次令牌,并把文件保持为 Studio 运行 UID/GID 所有、0400;缺少 UID 或 GID 时脚本会拒绝执行,不能退回 root:root 0600。重启 Studio 容器让新文件生效,然后在 https://studio.doyouhang.live 的“安全设置”中立即登记;成功后服务器只保存已消费令牌摘要,同一令牌在进程重启后也不能复用。WebAuthn 绑定正式 HTTPS 域名,不能在 localhost 为生产域名登记。

Studio 工作副本首次创建或重新克隆后,还要设置仓库级提交身份;不要使用服务器全局 Git 配置,也不要把真实邮箱写进仓库:

sudo -u blog-studio git -C /var/lib/blog-studio/worktree config --local user.name '站点作者的 Git 提交名'
sudo -u blog-studio git -C /var/lib/blog-studio/worktree config --local user.email '站点作者的 Git 提交邮箱'

构建 Studio 镜像时,Compose 会把 STUDIO_UID 和 STUDIO_GID 同时作为构建参数写入容器账号数据库。部署前必须用目标 UID/GID 完成真实镜像构建,并在容器内执行 getent passwd "$STUDIO_UID";只有 Compose 的数字 user 配置而镜像中没有对应账号时,OpenSSH 会以 No user exists for uid 拒绝推送。

OAuth 故障时从 SSH 检查 Edge、oauth2-proxy 日志和 secret 文件权限,恢复或轮换 OAuth 配置后再回到正式域名:

  • callback 返回 500 时,先区分 GitHub token/API 请求的 timeout、context canceled 或 unexpected EOF,不要直接改白名单或应用代码。
  • callback 返回 403 时,核对 GitHub 当前实际验证邮箱与唯一 allowed-emails 条目;文件保持容器 UID 65532 只读和 0400,不在日志或工单中回显邮箱。
  • OAuth code 被首次 callback 兑换后不能再次使用。首次请求若在后续 GitHub API 阶段失败,刷新旧 callback 会得到 bad_verification_code;必须从 Studio 首页重新发起全新登录。
  • 短时网络故障应通过全新登录重试并保留错误证据;不要接入第三方 OAuth 代理,也不要长期硬编码 GitHub IP。

要让全部 180 天会话立即失效,用上面的脚本原子替换 Cookie secret 并重启 oauth2-proxy。Cookie 会话模式不能逐台远程撤销;这是当前单作者部署的已知边界。

内容快速发布和自动回滚

作者在 Studio 中保存、提交并推送后,仍需单独点击“发布到线上”并使用通行密钥。Actions 构建和 ACR push 成功后,受限部署器才会处理请求。它不信任 Studio 的布尔状态,而是重新核对 Git、GitHub run、固定 workflow、artifact、ACR 摘要和候选页面;公网 /version.json 不匹配时自动恢复上一镜像,Studio 只显示失败。

排查时查看:

sudo systemctl status blog-studio-deployer.socket blog-studio-deployer.service
sudo journalctl -u blog-studio-deployer.service --since today
sudo docker compose -p blog-demo-production --env-file /etc/blog-studio/studio.env \
  -f infra/compose.yaml -f infra/compose.production.yaml \
  -f infra/compose.studio.yaml -f infra/compose.content-release.yaml ps

不要手工向 Unix Socket 伪造“成功”请求,也不要给 Studio 挂 Docker Socket。若自动回滚也失败,保留日志和当前容器,不清理镜像,按本手册的人工回滚流程恢复已知版本。

每次登录先看什么

readlink /opt/blog-demo/current
sudo docker ps --filter name=blog-demo-production-web-1
sudo docker ps --filter name=doyouhang-edge
sudo docker inspect --format \
  '{{.State.Status}} {{.State.Health.Status}} {{.Config.Image}}' \
  blog-demo-production-web-1
sudo systemctl is-active certbot.timer
sudo certbot certificates
sudo ss -ltnp
df -h /
free -h

正常状态应满足:

  • current 指向一个明确的发布版本目录。
  • 容器为 running 和 healthy。
  • 生产镜像标签中的版本与 /etc/blog-demo/production.env 的 RELEASE_ID 一致。
  • 只有 doyouhang-edge 发布 IPv4 的 80 与 443;博客、Studio 和 oauth2-proxy 都没有主机端口,8080 只出现在预发布的 127.0.0.1:8080。
  • blog-demo-production-web-1 在 doyouhang-edge 网络上的别名为 blog-web。
  • 原有 18080、7000 等服务仍保持原状态。
  • Certbot timer 为 active,Edge 容器健康,证书未接近到期;证书副本权限没有放宽。
  • 磁盘和内存没有接近耗尽。

从自己的电脑检查预发布

保持下面的 SSH 会话运行:

ssh -i SSH_KEY \
  -L 8080:127.0.0.1:8080 \
  DEPLOY_USER@ECS_HOST

浏览器打开 http://127.0.0.1:8080/。如果本机 8080 已被占用,可以只改变左侧端口:

ssh -i SSH_KEY \
  -L 18081:127.0.0.1:8080 \
  DEPLOY_USER@ECS_HOST

此时打开 http://127.0.0.1:18081/。不要为了省去隧道而把 Compose 改成 0.0.0.0:8080:80。

查看生产状态和日志

sudo docker compose -p blog-demo-production \
  --env-file /etc/blog-demo/production.env \
  -f /opt/blog-demo/current/infra/compose.yaml \
  -f /opt/blog-demo/current/infra/compose.production.yaml \
  ps

sudo docker compose -p blog-demo-production \
  --env-file /etc/blog-demo/production.env \
  -f /opt/blog-demo/current/infra/compose.yaml \
  -f /opt/blog-demo/current/infra/compose.production.yaml \
  logs --tail 200 web

持续跟踪日志时使用 logs --follow web,看完按 Ctrl+C;这不会停止容器。Nginx 静态站正常空闲时日志很少。

人工验收并发布新版本

GitHub Release 完整门禁通过后,下载 ecs-release-vX.Y.Z artifact。它包含源码包、linux/amd64 镜像包、manifest.json 和 SHA256SUMS。Release 工作流不会连接 ECS;以下人工步骤是生产变更门禁。

1. 本地核验版本制品

cd DOWNLOADED_ARTIFACT_DIRECTORY
sha256sum --check SHA256SUMS
cat manifest.json

确认清单中的版本、提交、linux/amd64、https://www.doyouhang.live、根路径和文件摘要与 GitHub Release 一致。将去掉 v 的版本号记为 RELEASE_ID。

清单中的 image.id 来自 GitHub 构建机。Docker 使用不同镜像存储时,导入后的 .Id 可能改为本机 manifest 身份,不能只靠这个字符串判断制品是否一致。跨机器应核对镜像归档 SHA-256、归档 manifest.json 指向的 config digest、RootFS 层、平台和容器内 /version.json;其中任一项不符都停止部署。

若 Release 制品系统不可用,才从同一标签的干净提交执行应急本机构建:

node scripts/verify-release.mjs vX.Y.Z
SITE_URL=https://www.doyouhang.live SITE_BASE=/ npm run check
SITE_URL=https://www.doyouhang.live SITE_BASE=/ npm run build
SITE_URL=https://www.doyouhang.live SITE_BASE=/ npm run test

然后构建运行镜像:

docker build \
  --platform linux/amd64 \
  --build-arg SITE_URL=https://www.doyouhang.live \
  --build-arg SITE_BASE=/ \
  -f infra/docker/web.Dockerfile \
  -t blog-demo-web:RELEASE_ID .

必须替换 vX.Y.Z 和 RELEASE_ID,不能把占位文本原样用于正式发布。

2. 上传已核验的源码和镜像

sha256sum --check SHA256SUMS

上传 artifact 中的四个文件:

scp -i SSH_KEY \
  blog-demo-vX.Y.Z-source.tar.gz \
  blog-demo-web-vX.Y.Z-linux-amd64.tar \
  manifest.json \
  SHA256SUMS \
  DEPLOY_USER@ECS_HOST:/tmp/

3. 服务器核验并安装

登录服务器后再次核验全部 SHA-256:

cd /tmp
sha256sum --check SHA256SUMS

创建新目录并导入镜像:

sudo install -d \
  -m 0755 \
  -o DEPLOY_USER \
  -g DEPLOY_USER \
  /opt/blog-demo/releases/RELEASE_ID

tar -xzf /tmp/blog-demo-vX.Y.Z-source.tar.gz \
  -C /opt/blog-demo/releases/RELEASE_ID

sudo docker load --input /tmp/blog-demo-web-vX.Y.Z-linux-amd64.tar
sudo docker image inspect blog-demo-web:RELEASE_ID

先备份 /etc/blog-demo/production.env,再用 sudoedit 更新 SITE_URL、SITE_BASE=/、RELEASE_ID 和 EDGE_NETWORK_NAME=doyouhang-edge。这里的站点值必须与构建镜像时一致;博客环境文件不再包含证书或 ACME 目录。

4. 先检查配置,再替换容器

sudo docker compose -p blog-demo-production \
  --env-file /etc/blog-demo/production.env \
  -f /opt/blog-demo/releases/RELEASE_ID/infra/compose.yaml \
  -f /opt/blog-demo/releases/RELEASE_ID/infra/compose.production.yaml \
  config

sudo docker compose -p blog-demo-production \
  --env-file /etc/blog-demo/production.env \
  -f /opt/blog-demo/releases/RELEASE_ID/infra/compose.yaml \
  -f /opt/blog-demo/releases/RELEASE_ID/infra/compose.production.yaml \
  run --rm --no-deps web nginx -t

确认镜像标签、无主机端口、外部网络和安全选项后,只替换 Web:

sudo docker compose -p blog-demo-production \
  --env-file /etc/blog-demo/production.env \
  -f /opt/blog-demo/releases/RELEASE_ID/infra/compose.yaml \
  -f /opt/blog-demo/releases/RELEASE_ID/infra/compose.production.yaml \
  up -d --no-build --no-deps web

--no-build 防止服务器在切换时临时访问镜像源;--no-deps web 保证本次只替换博客后端。执行前后记录 doyouhang-edge 的容器 ID,两次必须相同。

5. 验收后再更新 current

sudo docker compose -p blog-demo-production \
  --env-file /etc/blog-demo/production.env \
  -f /opt/blog-demo/releases/RELEASE_ID/infra/compose.yaml \
  -f /opt/blog-demo/releases/RELEASE_ID/infra/compose.production.yaml \
  ps

curl --fail --show-error --head \
  --resolve YOUR_DOMAIN:443:127.0.0.1 \
  https://YOUR_DOMAIN/
curl --fail --show-error --head \
  --resolve YOUR_DOMAIN:443:127.0.0.1 \
  https://YOUR_DOMAIN/interests/food/
curl --show-error --output /dev/null --write-out '%{http_code}\n' \
  --resolve YOUR_DOMAIN:443:127.0.0.1 \
  https://YOUR_DOMAIN/not-a-real-route/

随后从 ECS 外的网络用正式域名检查首页、文章、搜索、静态资源、404、RSS、sitemap、证书链和 HTTP 跳转,并确认公网 8080、18080 不可访问。再检查 ai.doyouhang.live/healthz 为 200、未授权 API 为 401、Edge ID 未变化,以及容器健康、重启次数、5xx、内存和磁盘;全部通过后执行:

sudo ln -sfn \
  /opt/blog-demo/releases/RELEASE_ID \
  /opt/blog-demo/current

临时归档只在确认回滚所需镜像和版本目录都存在后删除。生产上线后观察 24 至 48 小时。

回滚到上一版本

先找出当前版本和可用旧版本:

readlink /opt/blog-demo/current
find /opt/blog-demo/releases -mindepth 1 -maxdepth 1 -type d -printf '%f\n'
sudo docker image ls blog-demo-web

确定 PREVIOUS_ID 后,检查旧镜像存在:

sudo docker image inspect blog-demo-web:PREVIOUS_ID

备份后使用 sudoedit /etc/blog-demo/production.env 把 RELEASE_ID 改成 PREVIOUS_ID,并确认 SITE_URL、SITE_BASE 与旧镜像的构建配置兼容。渲染配置后启动旧版本:

sudo docker compose -p blog-demo-production \
  --env-file /etc/blog-demo/production.env \
  -f /opt/blog-demo/releases/PREVIOUS_ID/infra/compose.yaml \
  -f /opt/blog-demo/releases/PREVIOUS_ID/infra/compose.production.yaml \
  config

sudo docker compose -p blog-demo-production \
  --env-file /etc/blog-demo/production.env \
  -f /opt/blog-demo/releases/PREVIOUS_ID/infra/compose.yaml \
  -f /opt/blog-demo/releases/PREVIOUS_ID/infra/compose.production.yaml \
  up -d --no-build --no-deps web

健康检查和公网验收通过后,把 current 指向旧版本。回滚失败时保留现场,不要清理镜像或发布目录。

a75bbc7 是第一份通过生产健康检查的版本,6b51911 是 v1.0.0 前的历史生产版本;a83488e 和 c35f381 不能作为生产回滚目标。回滚时只使用已验收的 ECS 镜像与发布目录。博客回滚不停止 Edge,也不停止 18080 的 AI 鉴权网关。

检查证书续期

Certbot 和 HTTP-01 webroot 由独立 Edge 管理。安全组 80 不能只因全站使用 HTTPS 就关闭。博客仓库只做外部验收,证书 hook 的安装与回滚以私有 ai-infra/deploy/public-edge/README.md 为准。

sudo systemctl status certbot.timer
sudo certbot certificates
sudo certbot renew --dry-run --no-random-sleep-on-renew
sudo stat -c '%a %U:%G %n' \
  /etc/blog-demo/tls \
  /etc/blog-demo/tls/fullchain.pem \
  /etc/blog-demo/tls/privkey.pem

续期成功后,Edge deploy hook 更新证书副本,在 doyouhang-edge 内执行 nginx -t,通过后才发送 HUP;配置检查失败则恢复上一对证书并让续期任务失败。随后检查证书有效期、Edge 健康、博客和 AI HTTPS。不要向博客 Web 容器发送证书重载信号,也不要放宽私钥权限。

常见故障

permission denied 访问 Docker socket

原因通常是直接运行了 docker ...。本环境没有把部署用户加入 docker 组,请使用:

sudo docker ps
sudo docker compose version

构建停在 registry-1.docker.io 并超时

这是已出现过的网络问题。先确认并非 Docker 服务故障:

systemctl is-active docker
curl --connect-timeout 5 --head https://registry-1.docker.io/v2/

手工应急发布使用已验收镜像的 docker save、SHA-256 校验、SCP 和 docker load 流程。常规发布应使用已验证可达的 GHCR 或阿里云镜像仓库。

容器不断重启或显示 unhealthy

sudo docker ps --filter name=blog-demo-production-web-1
sudo docker logs --tail 200 blog-demo-production-web-1
sudo docker inspect blog-demo-production-web-1

重点检查:

  • /var/cache/nginx、/var/run 和 /tmp 是否仍配置为 tmpfs。
  • 只读文件系统和能力限制是否与 Compose 文件一致。
  • 镜像是否为当前 RELEASE_ID。
  • Web 生产容器不应占用主机端口;80/443 冲突属于独立 Edge,预发布环境才检查 8080。

不要进入容器手改文件;修复源码或配置后重新生成一个版本。

ECS 本机可以访问,SSH Tunnel 不通

先在 ECS 上执行:

curl --fail --head http://127.0.0.1:8080/

如果成功,检查本机隧道命令、私钥、本机端口占用和 SSH 会话是否仍在。左侧本机端口可以换成 18081,右侧仍保持 127.0.0.1:8080。

页面返回 404

  • 不存在的内容返回 404 是正确行为,Nginx 不使用 SPA fallback。
  • 若所有深层页面都 404,检查镜像中的 /usr/share/nginx/html、infra/nginx/web.conf 和 Edge 到 blog-web:80 的网络解析。
  • 若只有一篇文章 404,先确认对应内容是否进入当前提交和构建产物。
  • 检查 URL 中是否错误带有已退役的 /blog-demo/ 前缀;生产构建使用 SITE_BASE=/。

canonical、RSS 或 sitemap 还是旧域名

SITE_URL 在构建时写入静态文件。修改 /etc/blog-demo/production.env 后继续运行旧镜像不会生效。必须用新 SITE_URL 重建镜像、分配新 RELEASE_ID 并重新发布。

可以快速检查:

curl --silent https://www.doyouhang.live/
curl --silent https://www.doyouhang.live/rss.xml
curl --silent https://www.doyouhang.live/sitemap-index.xml

更新后仍看到旧内容

依次核对:

readlink /opt/blog-demo/current
sudo docker inspect --format '{{.Config.Image}} {{.Image}}' blog-demo-production-web-1
sudo docker image ls blog-demo-web

HTML 使用 no-cache,哈希资源使用长期缓存。若 HTML 已经引用新的资源文件名,浏览器缓存通常不是根因;重点检查容器镜像和发布版本是否真的切换。

磁盘空间持续减少

先看占用,不要直接执行 docker system prune -a:

df -h /
sudo docker system df
sudo docker image ls blog-demo-web
find /opt/blog-demo/releases -mindepth 1 -maxdepth 1 -type d -printf '%f\n'

至少保留当前版本和一个已演练可用的回滚版本。删除前逐项确认镜像标签、发布目录和 current 指针。

备份和恢复范围

当前阶段没有数据库,公开内容的权威副本仍是 Git 仓库。需要保留:

  • GitHub 仓库及提交历史。
  • /etc/blog-demo/production.env 的安全备份;仍使用回环验收时另备份 staging 配置。
  • 私有基础设施仓库中的 Edge 配置,以及 Certbot 账户、续期配置和证书副本;私钥备份必须加密并限制访问。
  • 当前和上一个可用发布目录。
  • 当前和上一个可用镜像,或能从可信仓库按摘要重新拉取它们的方法。
  • ECS 安全组、DNS 和未来证书配置的变更记录。

后续引入 PostgreSQL 后,必须增加独立的数据备份、异地保存和恢复演练;镜像与 Git 不能代替数据库备份。

排障时如何留证据

记录以下信息即可,不要复制整个系统或泄露环境变量:

发生时间和时区:
正在访问的入口:ECS localhost / SSH Tunnel / 正式域名
current 指向版本:
容器镜像标签和镜像 ID:
容器状态与健康状态:
相关 HTTP 状态码:
最近 200 行容器日志:
最近一次部署做了什么:
已有服务是否正常:

先区分连接失败、HTTP 错误、内容版本错误和域名生成错误,再处理对应层。这样能避免为了修一个页面问题去重启 Docker,或为了网络超时去修改 Astro 源码。