在 Docker 中使用 Nginx 的最佳实践
虽然已经有可视化配置 Nginx 的项目了,比如 nginx-proxy-manager 和 Zoraxy ,但是遇到问题难以排查,而且占用资源相比纯 Nginx 更多。对于 IT 从业者来说,学会配置 Nginx 还是很有用的。
假定域名为 your.domain ,想要运行 cloudreve、code-server、ntfy
首先展示最终的目录结构:
root@debian:~/container/nginx# tree
.
├── acme
│ ├── account.conf
│ ├── ca
│ │ └── acme.zerossl.com
│ │ └── v2
│ │ └── DV90
│ │ ├── account.json
│ │ ├── account.key
│ │ └── ca.conf
│ ├── your.domain_ecc
│ │ ├── backup
│ │ │ └── key.bak
│ │ ├── ca.cer
│ │ ├── your.domain.cer
│ │ ├── your.domain.conf
│ │ ├── your.domain.csr
│ │ ├── your.domain.csr.conf
│ │ ├── your.domain.key
│ │ └── fullchain.cer
│ └── http.header
├── certs
│ ├── your.domain.key
│ └── your.domain.pem
├── conf.d
│ ├── 00-default-http.conf
│ ├── cloudreve.your.domain.conf
│ ├── code.your.domain.conf
│ ├── ntfy.your.domain.conf
│ └── snippets
│ ├── proxy-common.conf
│ └── ssl-common.conf
├── docker-compose.yml
├── html
├── logs
│ ├── access.log
│ ├── cloudreve_access.log
│ ├── cloudreve_error.log
│ ├── code_access.log
│ ├── code_error.log
│ ├── error.log
│ ├── ntfy_access.log
│ └── ntfy_error.log
└── README.md
Nginx 配置文件
创建目录:mkdir -p acme certs conf.d html logs
创建 Nginx 配置文件:
conf.d/00-default-http.conf
# HTTP 默认配置 - 处理所有 your.domain 的子域名
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name *.your.domain your.domain;
# 可选:保留 HTTP 验证路径(作为备用方案)
# location /.well-known/acme-challenge/ {
# root /var/www/acme-challenges;
# }
# 强制跳转 HTTPS
location / {
return 301 https://$host$request_uri;
}
}
这是一个默认的配置,用于处理所有 your.domain 的子域名,比如 api.your.domain cloudreve.your.domain code.your.domain ntfy.your.domain 。
通配符证书(*.your.domain)强制要求使用 DNS 验证(DNS-01 Challenge),无法通过 HTTP 文件验证(HTTP-01 Challenge)申请。
所以 00-default-http.conf 里的
location /.well-known/acme-challenge/ {
root /var/www/acme-challenges;
}
对于通配符证书是多余的,可以删掉。
conf.d/00-map.conf
# 动态决定 Connection 头的值
# $http_upgrade 是客户端发来的 Upgrade 头的值
# 这个文件会自动被加载到 http {} 块中
map $http_upgrade $connection_upgrade {
# 如果客户端发来了 Upgrade (例如 "websocket"),则变量值为 "upgrade"
default upgrade;
# 如果客户端没发 Upgrade (为空),则变量值为空字符串 (为了启用 Keep-Alive)
'' "";
}
解释:这是动态判断请求,当客户端发来的请求包含 Upgrade 头(WebSocket 握手)时,转发给后端的 Connection 也是 Upgrade;否则,转发空的 Connection 头(激活 HTTP/1.1 长连接)。
- 如果是 WebSocket 请求:将 Connection 头设为 Upgrade。
- 如果是普通 HTTP 请求:将 Connection 头设为 "" (清空),从而开启后端 Keep-Alive。
00-map.conf 绝对不能 放在 conf.d/snippets/ 下,它必须直接放在 conf.d/ 目录下,不然不会被 Nginx 自动加载。
conf.d/snippets/proxy-common.conf
# 公共代理配置,所有反向代理都可以引用
# 允许客户端上传无限大小的请求体(解决 413 Request Entity Too Large)
client_max_body_size 0;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket 支持
# 默认情况下 Nginx 使用 HTTP/1.0 连接后端,而 WebSocket 必须使用 HTTP/1.1
# 必须指定 HTTP 1.1,否则 WebSocket 无法握手
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
# 使用刚才在 00-map.conf 里定义好的全局变量
# 既能自动处理 WebSocket 握手,又能让普通请求保持长连接
proxy_set_header Connection $connection_upgrade;
conf.d/snippets/ssl-common.conf
# 公共 SSL 配置,所有 HTTPS server 都可以引用
ssl_certificate /etc/nginx/certs/your.domain.pem;
ssl_certificate_key /etc/nginx/certs/your.domain.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# 安全响应头
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
conf.d/cloudreve.your.domain.conf 示例
这是一个 Cloudreve 的配置文件,可以参考。
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name cloudreve.your.domain;
include /etc/nginx/conf.d/snippets/ssl-common.conf;
access_log /var/log/nginx/cloudreve_access.log;
error_log /var/log/nginx/cloudreve_error.log warn;
location / {
# 你的后端服务地址
# 如果是本机其他容器,请使用 IP 或 Docker 网络名
proxy_pass http://cloudreve:5212;
include /etc/nginx/conf.d/snippets/proxy-common.conf;
# =========================================
# 1. 缓存控制 (全部关闭)
# =========================================
# 禁止 Nginx 将后端响应存入磁盘缓存 (防止权限泄露和磁盘爆满)
proxy_cache off;
# 关闭响应缓冲 (针对下载优化)
# 让 Nginx 收到数据就立即发给客户端,而不是存满内存缓冲区再发
# 解决下载大文件时内存飙升或卡顿的问题
proxy_buffering off;
# =========================================
# 2. 上传控制 (针对上传优化)
# =========================================
# 关闭请求缓冲 (核心配置)
# 允许 Cloudreve 边接收边处理,支持超大文件流式上传
proxy_request_buffering off;
# 解除上传大小限制
client_max_body_size 0;
# =========================================
# 3. 连接保活 (防止断连)
# =========================================
# 针对极差网络环境,给予充足的时间
proxy_connect_timeout 86400s;
proxy_send_timeout 86400s;
proxy_read_timeout 86400s;
}
}
conf.d/code.your.domain.conf 示例
这是 code-server 的配置文件,可以参考。
重点:修改 server_name 以匹配泛域名
这样 code.your.domain 和 3000.your.domain 都会进到这里
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
# 你的域名
server_name code.your.domain;
# 复用 SSL 配置
include /etc/nginx/conf.d/snippets/ssl-common.conf;
access_log /var/log/nginx/code_access.log;
error_log /var/log/nginx/code_error.log warn;
location / {
# LinuxServer 镜像默认容器内监听 8443
# 且在同一网络下,直接用服务名访问
proxy_pass http://code-server:8443;
# 复用通用 Header (包含 Host, Real-IP, WebSocket Upgrade)
include /etc/nginx/conf.d/snippets/proxy-common.conf;
# =========================================
# code-server 专属优化
# =========================================
# 1. WebSocket 核心 (终端必须)
# 你的 proxy-common.conf 已经包含了 Upgrade 和 Connection 头
# 所以这里不需要重复写,但下面的超时必须加
# 2. 长连接超时设置
# 如果不设置,你在网页终端里写代码,过一会不动就会断开连接
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
# 3. 关闭缓冲
# 保证终端输入输出的实时性
proxy_buffering off;
proxy_request_buffering off;
}
}
code-server 的 docker-compose.yml :
services:
code-server:
image: lscr.io/linuxserver/code-server:latest
container_name: code-server
environment:
- PUID=1000
- PGID=1000
- TZ=Asia/Shanghai
- PASSWORD=YourPassword
- SUDO_PASSWORD=YourSudoPassword
#- PROXY_DOMAIN=your.domain
volumes:
- ./config:/config
#ports:
#- 8443:8443
restart: unless-stopped
networks:
default:
external: true
name: scoobydoo
如果在 docker-compose.yml 中的 environment 配置了 - PROXY_DOMAIN=your.domain ,
那么 code-server 支持通过子域名访问项目的端口,例如 https://3000.your.domain/ 。
但是不推荐这样做,既然我总归是要为项目配置一条反向代理,那么何必配置 - PROXY_DOMAIN=your.domain 呢
为项目配置一条新的反向代理,而不是使用 https://code.your.domain/proxy/3000/ 有以下好处:
- 此时项目运行在域名的根目录
/ 下,不再需要剥离 /proxy/3000/ 前缀。
- 不需要更改项目配置,所有的懒编译、静态资源、路由都默认工作正常。
- 不需要输入 code-server 的密码就能访问项目,给他人展示时不需要告诉他 code-server 的密码。
- 能够利用
*.your.domain 泛域名证书,不用再单独为 *.code.your.domain 创建新的证书。
另外注意在使用 code-server 的时候,如果开发的网页不能及时更新,请打开 Cloudflare 的开发模式。
Cloudflare API Token
在 Cloudflare API Token 点击创建令牌,点击 编辑区域 DNS 后面的模板,权限分别选择 区域 DNS 编辑 ,区域资源选择 包括 特定区域 你的域名 ,点击底部的 继续以显示摘要 ,将会显示 API Token,复制下来,关闭页面就再也不会显示了。
docker-compose.yml
services:
nginx:
image: nginx:alpine
container_name: nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./conf.d:/etc/nginx/conf.d
- ./certs:/etc/nginx/certs
- ./html:/usr/share/nginx/html
- ./logs:/var/log/nginx
command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"
acme:
image: neilpang/acme.sh:latest
container_name: acme
restart: unless-stopped
command: daemon
volumes:
- ./acme:/acme.sh # acme.sh 工作目录
- ./certs:/certs # 证书输出目录
environment:
# Cloudflare API Token(替换成你的)
# 在 https://dash.cloudflare.com/profile/api-tokens 生成
- CF_Token=_your_cloudflare_api_token
networks:
default:
external: true
name: scoobydoo
由于容器隔离,acme 无法直接通知 nginx 重载,所以采用了 Nginx 容器内部每 6 小时自动重载配置的策略,以确保读取到最新的证书。
注意底部的
networks:
default:
external: true
name: scoobydoo
默认加入外部网络 scoobydoo,当然名字可以自定义。这样每个加入该网络的容器都可以互相访问到了。
如果没有该网络,则需手动创建该网络:
docker network create scoobydoo
申请通配符证书
docker compose up -d 启动容器后,Nginx 会不断重启,这是正常现象,因为还没配置证书,acme.sh 能用就行。
注意下面的代码仅需执行一次,以后 acme.sh 会自动更新证书。
注册账户
ZeroSSL(acme.sh 现在的默认 CA)强制要求注册邮箱
docker exec acme acme.sh --register-account -m youremail@gmail.com
申请通配符证书
执行:
docker exec acme --issue \
--dns dns_cf \
-d your.domain \
-d '*.your.domain' \
--keylength ec-256 \
--force
来申请证书。加了 --force 是为了如果之前执行失败了,则覆盖掉之前失败的空记录
部署证书到 nginx 目录
docker exec acme --install-cert -d your.domain \
--key-file /certs/your.domain.key \
--fullchain-file /certs/your.domain.pem \
--reloadcmd "echo 'Certificate updated via acme.sh'"
最后执行 docker compose restart 重启容器,Nginx 就会自动加载证书。大功告成啦。
新建反向代理
- 在
./conf.d/ 新建 名字.your.domain.conf 并配置。
- 检查配置:
docker compose exec nginx nginx -t
- 重载 Nginx:
docker compose exec nginx nginx -s reload
进阶:隐藏源站 IP 防止网络测绘(Security)
在使用 CDN(如 Cloudflare)时,我们希望攻击者无法直接通过 IP 访问到源站。但如果 Nginx 配置不当,网络空间测绘引擎(如 Fofa、Shodan)扫描全网 IP 的 443 端口时,Nginx 会默认返回包含你真实域名的 SSL 证书。这样,攻击者就能通过“IP 反查域名”找到你的源站 IP。
为了解决这个问题,我们需要利用 Nginx 的 default_server 机制,配合一个“假证书”来欺骗扫描器。
1. 生成自签名“假证书”
我们需要生成一个与真实域名无关的自签名证书。
在宿主机创建的 certs 目录下执行以下命令(注意:CN 我们填了 127.0.0.1,千万不要填真实域名)
生成有效期 10 年的自签名假证书(注意在 certs 的父目录下执行):
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
-keyout certs/fake.key \
-out certs/fake.crt \
-subj "/C=US/ST=Fake/L=Fake/O=Fake/CN=127.0.0.1"
确保 certs 目录已正确挂载到 Docker 容器内的 /etc/nginx/certs。
2. 升级默认配置文件
我们将原来的 00-default-http.conf 改造为 00-default-protection.conf。这个配置文件的作用是:当有人直接访问 IP,或者访问未绑定的域名时,直接拒绝连接或返回假证书。
conf.d/00-default-protection.conf :
# --------------------------------------------------------
# 1. 默认捕获:直接 IP 访问 HTTP (80) -> 直接拒绝
# --------------------------------------------------------
server {
listen 80 default_server;
listen [::]:80 default_server;
# 匹配所有未定义的域名或直接 IP 访问
server_name _;
# 返回 444 代表 Nginx 直接关闭连接,不返回任何 HTTP 头部,节省资源
return 444;
}
# --------------------------------------------------------
# 2. 默认捕获:直接 IP 访问 HTTPS (443) -> 防止测绘的关键
# --------------------------------------------------------
server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
server_name _;
# 【关键】使用刚才生成的假证书
# 扫描器扫描 IP 时,只会看到这张假证书,无法关联到你的真实域名
ssl_certificate /etc/nginx/certs/fake.crt;
ssl_certificate_key /etc/nginx/certs/fake.key;
# 握手完成后直接关闭连接
return 444;
}
# --------------------------------------------------------
# 3. 正常业务:域名访问 HTTP (80) -> 强制跳转 HTTPS
# --------------------------------------------------------
server {
listen 80;
listen [::]:80;
# 只有明确匹配到你的域名时,才进行跳转
server_name *.your.domain your.domain;
location / {
return 301 https://$host$request_uri;
}
}
3. 重载 Nginx
检查配置:
docker compose exec nginx nginx -t
重载 Nginx:
docker compose exec nginx nginx -s reload
4. 核心注意事项(非常重要)
配置了上述 default_server 后,必须遵循一条铁律:
所有其他的业务配置文件(如 cloudreve.your.domain.conf、code.your.domain.conf)中,listen 指令绝对不能包含 default_server 标记。
Nginx 规定对于同一个端口(如 443),只能有一个 default_server。如果其他配置文件里写了 listen 443 ssl default_server;,Nginx 启动时会报错 duplicate default_server。
正确的业务配置示例 (app.your.domain.conf):
server {
# 这里的 listen 不要加 default_server
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
# 你的域名
server_name app.your.domain;
# 复用 SSL 配置
include /etc/nginx/conf.d/snippets/ssl-common.conf;
# 日志
access_log /var/log/nginx/app_access.log;
error_log /var/log/nginx/app_error.log warn;
location / {
# 在同一网络下,直接用服务名加端口号访问
proxy_pass http://app:8443;
# 复用通用 Header (包含 Host, Real-IP, WebSocket Upgrade)
include /etc/nginx/conf.d/snippets/proxy-common.conf;
# 其他配置
}
}
配置生效后,你可以尝试直接访问 https://你的IP,浏览器会警告证书错误,且证书信息显示为 Fake,证明你的真实域名已被成功隐藏。
附:WebSocket 代理
假定把代理服务放在 code.your.domain 下,路径为 /your_secret_path ,转发到 s-ui 的 10000 端口。
conf.d/code.your.domain.conf :
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
# 你的域名
server_name code.your.domain *.your.domain;
# 复用 SSL 配置
include /etc/nginx/conf.d/snippets/ssl-common.conf;
access_log /var/log/nginx/code_access.log;
error_log /var/log/nginx/code_error.log warn;
location / {
# LinuxServer 镜像默认容器内监听 8443
# 且在同一网络下,直接用服务名访问
proxy_pass http://code-server:8443;
# 复用通用 Header (包含 Host, Real-IP, WebSocket Upgrade)
include /etc/nginx/conf.d/snippets/proxy-common.conf;
# =========================================
# code-server 专属优化
# =========================================
# 1. WebSocket 核心 (终端必须)
# 你的 proxy-common.conf 已经包含了 Upgrade 和 Connection 头
# 所以这里不需要重复写,但下面的超时必须加
# 2. 长连接超时设置
# 如果不设置,你在网页终端里写代码,过一会不动就会断开连接
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
# 3. 关闭缓冲
# 保证终端输入输出的实时性
proxy_buffering off;
proxy_request_buffering off;
}
# ===============================
# 秘密通道 (s-ui 分流)
# ===============================
location /your_secret_path {
proxy_pass http://s-ui:10000;
# 1. 基础 WebSocket 必须头
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
# 2. 传递真实 IP (配合 s-ui 日志或审计)
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# =========================================
# 核心优化部分
# =========================================
# 3. 关闭缓冲 (极重要:降低延迟)
# Nginx 默认会缓存后端的数据。对于代理流量,我们希望数据来了立马转给客户端。
# 开启这个可以显著降低延迟,提升浏览网页和看视频的流畅度。
proxy_buffering off;
# 4. 延长超时时间 (极重要:防止断连)
# Nginx 默认 60秒 无数据传输就切断连接。
# 挂梯子时(特别是 SSH 或看长视频暂停时),容易因此意外断开。
# 设置为一天 (86400s) 确保连接极其稳定。
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
# 5. 支持 Xray/Sing-box 的 0-RTT (Early Data)
# 这可以让连接建立更快。如果不透传这些头,Early Data 功能会失效。
# 客户端需设置 Max Early Data > 0 才能生效。
proxy_set_header Sec-WebSocket-Key $http_sec_websocket_key;
proxy_set_header Sec-WebSocket-Extensions $http_sec_websocket_extensions;
proxy_set_header Sec-WebSocket-Accept $http_sec_websocket_accept;
proxy_set_header Sec-WebSocket-Protocol $http_sec_websocket_protocol;
# 6. 禁用重定向处理
# 代理隧道不需要 Nginx 去处理 301/302 跳转,直接透传即可。
proxy_redirect off;
}
}