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 源码。