← 返回博客

Astro 静态站 ECS 运维手册

面向静态生产站的日常检查、手工发布、回滚、证书续期、故障排查与备份边界。

2026-09-12 约 6 分钟读完

补记于 2026-09-12

本文目录 跳到想读的章节
  1. 连接和路径速查
  2. 每次登录先看什么
  3. 从自己的电脑检查预发布
  4. 查看生产状态和日志
  5. 手工发布新版本
  6. 回滚到上一版本
  7. 检查证书续期
  8. 常见故障
  9. 备份和恢复范围
  10. 排障时如何留证据

本手册用于维护已经切换到 ECS 的静态生产站,以及短期保留的私有预发布环境。当前生产入口是 https://www.doyouhang.live,GitHub Pages 在观察期作为独立静态回退。若服务器结构与搭建与切流记录不一致,先确认实际状态,不要直接复制执行更新或删除命令。

连接和路径速查

项目当前约定
SSHssh -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
生产入口https://www.doyouhang.live/
预发布容器infra-web-1
预发布入口http://127.0.0.1:8080/,仅 ECS 本机或 SSH Tunnel
证书副本/etc/blog-demo/tls,只读挂载到容器
Certbot webroot/var/lib/letsencrypt

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

每次登录先看什么

readlink /opt/blog-demo/current
sudo docker ps --filter name=blog-demo-production-web-1
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 指向一个明确的发布版本目录。
  • 容器为 runninghealthy
  • 生产镜像标签中的版本与 /etc/blog-demo/production.envRELEASE_ID 一致。
  • 博客生产入口只发布 IPv4 的 80 与 443;8080 只出现在 127.0.0.1:8080
  • 原有 18080、7000 等服务仍保持原状态。
  • Certbot timer 为 active,证书未接近到期;证书副本权限没有放宽。
  • 磁盘和内存没有接近耗尽。

从自己的电脑检查预发布

保持下面的 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 静态站正常空闲时日志很少。

手工发布新版本

自动发布工作流完成后,本节只作为应急流程。所有命令都应从干净、已经验证的提交执行。

1. 本地确定版本和构建参数

git status --short
git rev-parse --short HEAD

记下短提交号作为 RELEASE_ID,记下实际规范域名作为 SITE_URL。先完成仓库验证:

SITE_URL=https://YOUR_DOMAIN SITE_BASE=/ npm run check
SITE_URL=https://YOUR_DOMAIN SITE_BASE=/ npm run build
SITE_URL=https://YOUR_DOMAIN SITE_BASE=/ npm run test

然后构建运行镜像:

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

必须替换 YOUR_DOMAINRELEASE_ID,不能把占位文本原样用于正式发布。

2. 导出源码和镜像

git archive \
  --format=tar \
  --output=/tmp/blog-demo-RELEASE_ID.tar \
  RELEASE_ID

docker save \
  --output=/tmp/blog-demo-web-RELEASE_ID.tar \
  blog-demo-web:RELEASE_ID

sha256sum /tmp/blog-demo-RELEASE_ID.tar
sha256sum /tmp/blog-demo-web-RELEASE_ID.tar

上传两个文件:

scp -i SSH_KEY \
  /tmp/blog-demo-RELEASE_ID.tar \
  /tmp/blog-demo-web-RELEASE_ID.tar \
  DEPLOY_USER@ECS_HOST:/tmp/

3. 服务器核验并安装

登录服务器后再次计算两个 SHA-256,必须与本地逐字一致:

sha256sum /tmp/blog-demo-RELEASE_ID.tar
sha256sum /tmp/blog-demo-web-RELEASE_ID.tar

创建新目录并导入镜像:

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

tar -xf /tmp/blog-demo-RELEASE_ID.tar \
  -C /opt/blog-demo/releases/RELEASE_ID

sudo docker load --input /tmp/blog-demo-web-RELEASE_ID.tar
sudo docker image inspect blog-demo-web:RELEASE_ID

先备份 /etc/blog-demo/production.env,再用 sudoedit 更新 SITE_URLSITE_BASE=/RELEASE_ID。这里的值必须与刚才构建镜像时一致;证书与 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

确认镜像标签、80/443 端口和安全选项后启动:

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-build 很关键:镜像已经导入,服务器又可能无法访问 Docker Hub,不能在切换过程中临时重新构建。

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 不可访问。检查容器健康、重启次数、5xx、内存和磁盘;全部通过后执行:

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

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

回滚到上一版本

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

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.envRELEASE_ID 改成 PREVIOUS_ID,并确认 SITE_URLSITE_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

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

a75bbc7 是第一份通过生产健康检查的版本,a83488ec35f381 不能作为生产回滚目标。首次生产版本故障时,优先保留现场并使用已验证的 GitHub Pages 默认地址提供静态回退;若需要释放 80,可停止 blog-demo-production,恢复 /etc/nginx/sites-enabled/blog-demo-acme 指向已保留的站点配置,再执行 nginx -t 和 reload。不要停止 18080 的原有站点。

检查证书续期

Certbot 使用生产容器挂载的 webroot 完成 HTTP-01。生产容器必须继续提供 /.well-known/acme-challenge/,安全组 80 不能只因全站使用 HTTPS 就关闭。

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

续期成功后,/etc/letsencrypt/renewal-hooks/deploy/blog-demo.sh 更新容器证书副本,并向 blog-demo-production-web-1 发送 HUP。随后检查证书有效期、容器健康和公网 HTTPS。deploy hook 不运行时,先检查 RENEWED_LINEAGE、目标目录权限、容器名和 Docker 权限,不要手工放宽私钥权限。

常见故障

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
  • 生产环境检查 80 和 443,预发布环境检查 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/htmlinfra/nginx/production.conf
  • 若只有一篇文章 404,先确认对应内容是否进入当前提交和构建产物。
  • 检查 URL 中是否错误带有 GitHub Pages 的 /blog-demo/ 前缀;ECS 构建使用 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/etc/blog-demo/staging.env 的安全备份。
  • Certbot 配置、账户资料、续期配置和 /etc/blog-demo/tls 证书副本;私钥备份必须加密并限制访问。
  • 当前和上一个可用发布目录。
  • 当前和上一个可用镜像,或能从可信仓库按摘要重新拉取它们的方法。
  • ECS 安全组、DNS 和未来证书配置的变更记录。

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

排障时如何留证据

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

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

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

读到这里,谢谢你的时间。

如果还想继续读下去,
新的文章和闪念会送进 RSS。

订阅这本档案 ↗