把一个静态页面部署到 ECS,步骤确实不多。装好 Nginx,把 HTML 和图片传上去,配置一下访问目录,再去安全组放行端口,基本就能打开了。不过,第一次部署和后面持续更新,是两回事。第一次只需要考虑文件怎么放进去,第二次就得想一想:上传过程中有人访问怎么办?新页面有问题,旧版本还能不能找回来?
这篇主要聊后面这个问题。场景很简单,就是一个放项目介绍、截图和使用说明的静态站点,用阿里云 ECS 承载,Nginx 负责提供文件访问。页面没有登录、评论这些功能,也就没有引入数据库和后端服务的必要。对这样的小项目,我更关心的是更新过程能不能看明白,出问题以后能不能恢复。
环境以一台新建的 Ubuntu 24.04 LTS ECS 为例。实例需要有公网访问条件,测试页面用 HTTP,也就是 TCP 80 端口;SSH 使用的 22 端口则限制为自己的可信公网 IP。这里有个容易混淆的地方:ECS 安全组和系统内部防火墙是两层配置,在服务器里启动了 Nginx,并不代表外面的请求一定能进来。相关访问规则可以参考阿里云的安全组说明。
先把基础环境准备好。为了方便观察版本变化,示例页面只放一个版本标记,后面可以替换成自己的完整站点文件。
sudo apt update
sudo apt install -y nginx curl
sudo install -d -m 0755 /srv/project-site/releases/v1
printf '<!doctype html><meta charset="utf-8"><h1>site-v1</h1>\n' \
| sudo tee /srv/project-site/releases/v1/index.html >/dev/null
sudo ln -s /srv/project-site/releases/v1 /srv/project-site/current
这里没有直接把页面放进 Nginx 的默认目录。releases/v1 保存第一个版本,current 是一个指向它的软链接。后面每次更新都新建版本目录,Nginx 只认 current 这个固定入口。这样安排,主要是为了避免发布时直接修改正在提供访问的那份文件。
在这台专门用于示例的实例上,把 /etc/nginx/sites-available/default 调整为下面的配置:
server {
listen 80 default_server;
server_name _;
root /srv/project-site/current;
index index.html;
autoindex off;
access_log /var/log/nginx/project-site.access.log;
error_log /var/log/nginx/project-site.error.log;
add_header Cache-Control "no-cache" always;
location / {
try_files $uri $uri/ =404;
}
}
配置里真正和发布流程有关的是 root,它指向软链接,而不是某一个写死的版本目录。try_files 则按请求路径找文件,找不到就返回 404。这个站点没有前端路由,所以没有必要把所有请求都转回首页。图片路径写错了,就让它明确报错,排查起来反而省事。Nginx 官方文档对这两个指令的行为有详细说明。
保存后先运行 sudo nginx -t,检查通过再启动或重载服务。首次启动可以执行 sudo systemctl enable --now nginx,随后用 sudo systemctl reload nginx 加载配置。验证时先在 ECS 内部执行 curl --fail http://127.0.0.1/,确认响应里有 site-v1,再从自己的电脑访问实例公网 IP。
这两次访问最好都做。本机能访问、公网访问超时,应该继续查公网地址、安全组和系统防火墙;本机就访问失败,再去看 Nginx 的监听状态、配置和文件权限。按这个顺序排,至少能先把问题缩小到某一段,不用一会儿改端口,一会儿重装软件。
页面能打开以后,就可以考虑更新了。直接往线上目录覆盖文件虽然方便,但一次上传通常涉及很多文件,HTML、样式和图片并不会同时替换完成。如果首页先更新,引用的新样式还没传完,访问者就可能看到一个缺少样式的页面。更麻烦的是,覆盖掉旧文件以后才发现新版本有问题,此时还得重新找到旧的构建产物。
版本目录就是为了解决这个麻烦。新内容先放进一个独立目录,文件准备好以后,再切换 current。下面用复制旧版本、修改版本标记来演示,实际项目中应当把新构建出的完整文件放进 v2。
sudo cp -a /srv/project-site/releases/v1 \
/srv/project-site/releases/v2
sudo sed -i 's/site-v1/site-v2/' \
/srv/project-site/releases/v2/index.html
sudo test -s /srv/project-site/releases/v2/index.html
sudo ln -s /srv/project-site/releases/v2 \
/srv/project-site/current.next
sudo mv -Tf /srv/project-site/current.next \
/srv/project-site/current
最后两条命令值得单独说一下。先创建一个临时链接,再用它替换正式入口,可以避免“删掉旧链接、再创建新链接”中间的空档。两个链接位于同一个目录,在同一文件系统内可以利用重命名操作的原子替换行为。这里的 -T 也不能随手省掉,它让 mv 把目标当作一个路径处理,避免把指向目录的链接当成目标目录。Linux 的 rename 文档说明了这种替换的语义。
切换后再请求首页,检查正文是不是 site-v2。只看 HTTP 状态码还不够,因为旧页面一样可以返回 200。示例把版本写在正文里,真实项目也可以在构建时生成版本文件,写入提交编号或构建编号。以后有人反馈“怎么还是旧页面”,至少有个可以核对的依据。
如果要退回第一个版本,操作基本相同:
sudo ln -s /srv/project-site/releases/v1 \
/srv/project-site/current.next
sudo mv -Tf /srv/project-site/current.next \
/srv/project-site/current
这次没有修改 Nginx 配置,只是把站点入口重新指向旧目录。切换之后仍然要检查响应内容,并从外部设备重新访问。命令执行成功和页面恢复正常之间,还差一次实际验证。
当然,软链接切换也有管不到的地方。浏览器加载一个页面,会分别请求 HTML、CSS、JavaScript 和图片,整个过程可能刚好跨过一次发布。即使目录入口切换没有空档,也不能保证这些请求都来自同一个版本。实际站点最好给静态资源使用带内容哈希的文件名,同时保留一段时间的旧资源,避免旧页面里的引用突然失效。示例统一使用 no-cache,是为了方便观察版本变化;它要求缓存使用前重新验证,并不等于完全禁止存储。
还有一点,旧版本留在 ECS 上只能方便回退,不能代替备份。几个版本都在同一块磁盘里,磁盘出问题时它们会一起受影响。源代码和构建产物仍然应该在服务器之外保留副本,版本目录也需要定期清理,否则更新次数多了,磁盘占用会慢慢涨上去。
对更新频率不高的小站,这套做法已经比较容易维护。每次发布先准备新目录,确认文件完整,再切换入口;发现问题就退回旧版本,之后再排查。暂时不做自动化也没关系,先把这几个动作固定下来。等手动流程跑顺,再考虑把上传、检查和切换写进脚本,会少一些边写脚本边猜线上状态的麻烦。