背景
这次要部署的是一个开源项目生成出来的静态网站。项目本身没有后端服务,最终产物就是一批 HTML、CSS、JS 和文章文件。
我以前部署静态站,脑子里默认会想到两种方式:
- 把文件直接丢到 nginx 的静态目录。
- 找个平台,比如 Vercel、GitHub Pages。
这次想换一种方式:在本地把静态站打成 Docker 镜像,传到自己的服务器,再用服务器上的 nginx 反代过去。
这样做有一个好处:服务器不需要知道项目怎么构建,也不需要安装 Python、Node 或其他依赖。它只负责加载镜像、启动容器。以后更新网站时,也是重新打一个镜像包传过去。
服务器已有环境
我的服务器上已经有一套 nginx 入口服务,所有项目都放在:
/home/deploy
nginx 配置放在:
/home/deploy/nginx/conf.d
现有结构是一个 nginx 容器负责对外暴露 80 和 443,其他项目容器接入同一个 Docker 网络,再由 nginx 通过容器名反向代理。
这篇文章里的服务器地址、用户名、域名都做了脱敏,下面统一用这些占位符:
<server_user> 服务器用户名
<server_host> 服务器 IP 或域名
<domain> 对外访问域名
<app_network> nginx 和业务容器共用的 Docker 网络
第一步:本地构建静态 nginx 镜像
静态站目录是:
site/
先进入这个目录:
cd /path/to/project/site
然后执行:
docker buildx build \
--platform linux/amd64 \
-t tech-blog-hub-static:latest \
-f - . <<'EOF'
FROM nginx:alpine
COPY . /usr/share/nginx/html/
EOF
这里最容易卡住的是:
/usr/share/nginx/html/
这个路径属于 Docker 镜像内部。构建镜像时不需要在本地电脑或服务器宿主机上手动创建它。
nginx:alpine 官方镜像默认会从容器内部的 /usr/share/nginx/html/ 读取静态文件。构建镜像时,命令里的 COPY . /usr/share/nginx/html/ 会把当前 site/ 目录下的文件复制进去。
加上 --platform linux/amd64 是因为我的本地电脑和服务器架构可能不一样。本地用 Mac 构建时,显式指定服务器常见的 Linux x86_64 架构,后面少一些奇怪的问题。
构建完成后看一下镜像:
docker images tech-blog-hub-static
第二步:本地试跑镜像
镜像构建完,先在本机跑一下,再传服务器:
docker run --rm -p 8080:80 tech-blog-hub-static:latest
浏览器打开:
http://127.0.0.1:8080
如果页面正常,说明镜像里的 nginx 能读到静态文件。
这个命令没有加 -d,所以容器会占着当前终端。测试完直接按:
Ctrl + C
因为命令里带了 --rm,容器停止后会自动删除,不需要再手动清理。
如果已经在后台跑起来了,可以用下面的方式停掉:
docker ps
docker stop <container_id>
第三步:导出镜像包
确认本地能访问后,把镜像导出成一个压缩包:
docker save tech-blog-hub-static:latest | gzip > tech-blog-hub-static-latest.tar.gz
看一下文件大小:
ls -lh tech-blog-hub-static-latest.tar.gz
这里有个小误区。我一开始也会顺口把它叫成 zip 包。这个文件是 Docker 镜像包,传到服务器后应该用 docker load 加载,普通的 unzip 处理不了它。
第四步:传到服务器
把镜像包传到服务器:
scp tech-blog-hub-static-latest.tar.gz \
<server_user>@<server_host>:/home/deploy/
登录服务器:
ssh <server_user>@<server_host>
cd /home/deploy
加载镜像:
docker load -i tech-blog-hub-static-latest.tar.gz
确认镜像已经进入服务器 Docker:
docker images tech-blog-hub-static
能看到 tech-blog-hub-static:latest 就可以继续。
第五步:用 docker compose 启动容器
在服务器上创建项目目录:
cd /home/deploy
mkdir -p tech-blog-hub
cd tech-blog-hub
创建 docker-compose.yml:
services:
static:
image: tech-blog-hub-static:latest
container_name: tech-blog-hub-static
restart: always
networks:
- app-network
networks:
app-network:
external: true
这里的 app-network 要替换成自己服务器上 nginx 容器正在使用的 Docker 网络。
可以先查一下:
docker network ls
或者看 nginx 的 compose 文件:
cat /home/deploy/nginx/docker-compose.yml
启动容器:
docker compose up -d
确认容器已经起来:
docker ps | grep tech-blog-hub-static
第六步:接入 nginx
打开 nginx 配置文件:
vim /home/deploy/nginx/conf.d/<site>.conf
在 HTTPS 的 server 块里加上:
location = /tech-blog-hub {
return 301 /tech-blog-hub/;
}
location /tech-blog-hub/ {
proxy_pass http://tech-blog-hub-static/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
前面那个精确匹配的 redirect 是为了处理少一个尾斜杠的访问:
https://<domain>/tech-blog-hub
让它跳到:
https://<domain>/tech-blog-hub/
后面的 proxy_pass http://tech-blog-hub-static/; 会把请求转给同一个 Docker 网络里的静态站容器。
改完先检查 nginx 配置:
docker exec nginx nginx -t
看到配置测试成功后,再 reload:
docker exec nginx nginx -s reload
最后访问:
https://<domain>/tech-blog-hub/
这次踩到的几个点
第一,/usr/share/nginx/html/ 是容器内部路径。构建镜像时把本地 site/ 复制进去,服务器宿主机上不需要创建这个目录。
第二,docker buildx build 之后的产物是本地 Docker 镜像。它不会出现在当前文件夹里,要用 docker images 查看。
第三,docker save 之后得到的 .tar.gz 是镜像包。传到服务器后用 docker load -i,不要按普通 zip 去解压。
第四,nginx 反代到容器名的前提是两个容器在同一个 Docker 网络里。nginx 容器和静态站容器如果不在同一个网络,http://tech-blog-hub-static/ 这个名字解析不到。
第五,先在本地用 docker run --rm -p 8080:80 测一下,能省掉很多服务器上的来回排查。
最后保留的部署流程
本地构建:
cd /path/to/project/site
docker buildx build \
--platform linux/amd64 \
-t tech-blog-hub-static:latest \
-f - . <<'EOF'
FROM nginx:alpine
COPY . /usr/share/nginx/html/
EOF
本地验证:
docker run --rm -p 8080:80 tech-blog-hub-static:latest
导出镜像:
docker save tech-blog-hub-static:latest | gzip > tech-blog-hub-static-latest.tar.gz
上传服务器:
scp tech-blog-hub-static-latest.tar.gz \
<server_user>@<server_host>:/home/deploy/
服务器加载:
cd /home/deploy
docker load -i tech-blog-hub-static-latest.tar.gz
docker images tech-blog-hub-static
服务器启动:
cd /home/deploy/tech-blog-hub
docker compose up -d
docker ps | grep tech-blog-hub-static
nginx 检查并重载:
docker exec nginx nginx -t
docker exec nginx nginx -s reload
这套流程跑通以后,我对 Docker 部署静态站的理解清楚了很多:项目文件、镜像内部文件、服务器宿主机文件,这三层要分开看。只要这三层不混,部署过程就没有想象中那么玄学。