← 返回文章列表 专栏:网络与容器架构

更新于

使用 Docker Compose 与 Nginx 部署静态站点:HTTPS 卸载、缓存与安全响应头

编写生产级 compose.yaml 与 Nginx 配置文件,以只读卷挂载静态产物与 ECC 证书,配置分级缓存、真实 404 状态码与安全响应头。

Docker Compose 只读挂载 Nginx 容器与多版本软链接发布架构
Docker Compose 只读挂载 Nginx 容器与多版本软链接发布架构

准备好经过校验的静态构建产物 public/ 后,线上托管层面的目标是保持精简:不运行动态语言解释器或数据库,仅用一个只读挂载的 nginx:1.27-alpine 容器处理 HTTPS 卸载与静态文件分发。

本文整理生产环境使用的 compose.yaml 与 Nginx 站点配置,涵盖只读卷挂载、分级缓存头、真实 404 状态码配置以及安全响应头核验。

宿主机与 Nginx 容器的挂载边界

在服务器上,将与博客托管相关的文件统一放在 /srv/blog(或自定义 <站点根目录>)下,并按读写需求区分只读挂载(:ro)与读写挂载(:rw):

Docker 与 Nginx 只读容器挂载及路由缓存架构图
静态产物目录 releases/current、证书目录 certs 与配置文件 default.conf 均以 :ro 只读方式挂载进容器;仅 logs/ 目录开放写入以供宿主机轮转归档。
宿主机路径 容器内映射路径 挂载权限 用途说明
/srv/blog/releases/current /usr/share/nginx/html :ro(只读) 托管当前生效版本的静态网页,容器内 nginx worker 进程无权修改文件
/srv/blog/conf/default.conf /etc/nginx/conf.d/default.conf :ro(只读) 站点路由、压缩与安全响应头配置,防止运行时配置被改动
/srv/blog/certs /etc/nginx/certs :ro(只读) 挂载由 acme.sh 签发的 TLS 证书与私钥
/srv/blog/logs /var/log/nginx :rw(读写) 持久化访问日志与错误日志,供宿主机 logrotate 按日切分

为什么不要在 try_files 末尾直接写 /404.html

配置静态站点的 location / 时,如果把 try_files 的最后一个参数直接写成 /404.html,会导致错误的 HTTP 状态码:

  1. try_files 会按顺序检查前几个参数对应的文件或目录是否存在于 root 下,只有当前面的候选路径都不存在时,才会回退到最后一个参数;
  2. 如果最后一个参数写的是内部 URI /404.html,而站点根目录下恰好存在 404.html 文件,那么当请求一个不存在的链接(如 /posts/non-existent-slug/)时,Nginx 会执行内部重定向并返回 404.html 的页面内容;
  3. 由于内部重定向查找成功,Nginx 最终返回给客户端的 HTTP 状态码会变成 200 OK 而非 404 Not Found。

这种「页面显示 404 内容、状态码却是 200」的现象称为软 404(Soft 404),会让搜索引擎把失效链接当作正常页面收录,也会干扰监控探针的判断。

正确的写法是将「返回 404 状态码」与「指定 404 错误页面」拆成两条指令:

1
2
3
4
5
6
7
8
9
# 路径与目录均不存在时返回 HTTP 404 状态码,再交由 error_page 渲染 /404.html
location / {
try_files $uri $uri/ =404;
}

error_page 404 /404.html;
location = /404.html {
internal;
}

Nginx 站点配置(/srv/blog/conf/default.conf)

结合 Gzip 文本压缩、分级 Cache-Control 缓存策略与安全响应头,在 /srv/blog/conf/default.conf 中编写站点配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
server {
listen 80;
listen [::]:80;
server_name _;

root /usr/share/nginx/html;
index index.html;

# 隐藏 Nginx 版本号
server_tokens off;
charset utf-8;

# 开启内核零拷贝传输
sendfile on;
tcp_nopush on;

# 开启静态文本 Gzip 压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_comp_level 5;
gzip_types
text/plain
text/css
application/json
application/javascript
text/xml
application/xml
application/atom+xml
image/svg+xml;

# 1. HTML 文档、RSS 订阅源与全文检索索引:每次协商验证 ETag,确保发版后即时更新
location ~* \.(?:html|xml|json|txt)$ {
add_header Cache-Control "no-cache" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
try_files $uri $uri/ =404;
}

# 2. 图像、字体、矢量图与静态脚本:启用 30 天公共缓存
location ~* \.(?:css|js|svg|webp|png|jpg|jpeg|gif|ico|woff2)$ {
add_header Cache-Control "public, max-age=2592000" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
try_files $uri =404;
}

# 3. 常规目录路由与真实 404 状态码回退
location / {
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
try_files $uri $uri/ =404;
}

error_page 404 /404.html;
location = /404.html {
internal;
}
}

关于 add_header 的继承规则:在 Nginx 中,一旦某个子 location 块定义了自己的 add_header(例如 Cache-Control),它就不会再自动继承上层 server 块里的 add_header。因此在每个细分 location 中显式写出安全响应头(或抽取为 include snippets/security-headers.conf;),才能保证所有静态资源都带有 X-Content-Type-Options: nosniff。

编写 Docker Compose 文件并启动容器

在 /srv/blog/compose.yaml 中固定 nginx:1.27-alpine 镜像版本,并配置内存上限与自动重启策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
services:
blog-web:
image: nginx:1.27-alpine
container_name: blog-web
restart: unless-stopped
ports:
- "80:80"
volumes:
- ./releases/current:/usr/share/nginx/html:ro
- ./conf/default.conf:/etc/nginx/conf.d/default.conf:ro
- ./logs:/var/log/nginx
deploy:
resources:
limits:
memory: 128M

首次启动前,先将软链接 /srv/blog/releases/current 指向已同步的静态版本目录,随后启动容器并检查配置语法:

1
2
3
4
5
6
7
# 1. 创建或更新指向最新静态产物版本目录的软链接
ln -sfn /srv/blog/releases/<版本号> /srv/blog/releases/current

# 2. 启动容器并在容器内执行 nginx -t 语法检查
cd /srv/blog
docker compose up -d
docker exec blog-web nginx -t

容器托管与 HTTP 状态码核对表

服务启动后,可在服务器终端用 curl -I 核对以下四项响应指标:

检查场景 验证命令 预期输出
首页正常响应 curl -sI http://127.0.0.1/ | head -n 10 返回 HTTP/1.1 200 OK,含 X-Content-Type-Options: nosniff
文章固定链接 curl -sI http://127.0.0.1/posts/01-blog-planning-architecture/ 返回 HTTP/1.1 200 OK
真实 404 状态码 curl -sI http://127.0.0.1/posts/not-found-page/ 返回 HTTP/1.1 404 Not Found(而非 200 OK)
容器只读挂载验证 docker exec blog-web sh -c "touch /usr/share/nginx/html/test" 返回 Read-only file system,确认只读挂载生效