把 Nginx 作为整机的统一入口容器:80/443 收口,后端 Web 服务各自跑在独立 compose 栈里,通过外部共享网络按容器名反代;配置、证书、站点目录、日志全部 bind mount 到宿主机。
一、目录结构
/data/nginx/
├── docker-compose.yml
├── conf/
│ └── nginx.conf
├── conf.d/
│ ├── 00-default.conf
│ └── 10-app.conf
├── ssl/ # 证书(.pem / .key),key 权限 600
├── html/ # 静态站点、acme challenge
└── logs/
二、docker-compose.yml
# 统一入口 nginx:镜像锁定具体版本,不用 latest
services:
nginx:
image: nginx:1.27.2
container_name: nginx
restart: always
ports:
- "80:80"
- "443:443"
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- /data/nginx/conf/nginx.conf:/etc/nginx/nginx.conf
- /data/nginx/conf.d:/etc/nginx/conf.d
- /data/nginx/ssl:/etc/nginx/ssl:ro
- /data/nginx/html:/usr/share/nginx/html
- /data/nginx/logs:/var/log/nginx
networks:
- edge-net # 与各后端应用栈共享
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
networks:
edge-net:
name: edge-net
driver: bridge
说明:
- 后端容器如果跑在宿主机网络或直接发布宿主端口(如
127.0.0.1:8080),proxy_pass 用host.docker.internal:8080。 - 后端也是容器时,让应用栈
external: true加入同一个edge-net,直接proxy_pass http://容器名:端口,不发布宿主端口。 - 证书目录挂
:ro;私钥宿主上chmod 600、属主 root。
三、主配置 nginx.conf
conf/nginx.conf:
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 10240;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" rt=$request_time';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
keepalive_timeout 65;
server_tokens off;
client_max_body_size 50m;
gzip on;
gzip_types text/plain text/css application/json application/javascript
text/xml application/xml application/xml+rss text/javascript
application/font-woff2 image/svg+xml;
gzip_min_length 1k;
include /etc/nginx/conf.d/*.conf;
}
四、站点配置示例
conf.d/00-default.conf——HTTP 跳转 HTTPS,并给证书续签留 challenge 路径:
server {
listen 80 default_server;
server_name _;
location ^~ /.well-known/acme-challenge/ {
root /usr/share/nginx/html;
default_type text/plain;
}
location / {
return 301 https://$host$request_uri;
}
}
conf.d/10-app.conf——反向代理到同网络的应用容器(示例应用名 app-web,监听 8080):
upstream app_upstream {
server app-web:8080;
keepalive 16;
}
server {
listen 443 ssl;
http2 on; # nginx >= 1.25.1 用这行;1.24 及更老写 listen 443 ssl http2;
server_name app.example.com;
ssl_certificate /etc/nginx/ssl/app.example.com.pem;
ssl_certificate_key /etc/nginx/ssl/app.example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
add_header Strict-Transport-Security "max-age=31536000" always;
location / {
proxy_pass http://app_upstream;
proxy_http_version 1.1;
proxy_set_header Connection "";
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;
proxy_read_timeout 60s;
}
}
纯静态站点则更简单:
server {
listen 443 ssl;
http2 on;
server_name static.example.com;
root /usr/share/nginx/html/static;
index index.html;
ssl_certificate /etc/nginx/ssl/example.com.pem;
ssl_certificate_key /etc/nginx/ssl/example.com.key;
location / { try_files $uri $uri/ =404; }
location ~* \.(js|css|png|jpg|jpeg|gif|svg|woff2)$ {
expires 7d;
add_header Cache-Control "public";
}
}
五、证书
任选其一在宿主机上签发,证书落到 /data/nginx/ssl/:
- acme.sh(推荐,纯脚本):
~/.acme.sh/acme.sh --issue -d app.example.com -w /data/nginx/html ~/.acme.sh/acme.sh --install-cert -d app.example.com \ --fullchain-file /data/nginx/ssl/app.example.com.pem \ --key-file /data/nginx/ssl/app.example.com.key \ --reloadcmd "docker exec nginx nginx -s reload" - certbot:
certbot certonly --webroot -w /data/nginx/html -d app.example.com,再把/etc/letsencrypt/live/...链接或复制进 ssl 目录并配 deploy-hook reload。
记得在域名 DNS 把 A 记录指向服务器,并在云安全组放行 80/443。
六、启动与验证
# 先建网络(应用栈也用它)
docker network create edge-net
cd /data/nginx
docker compose config -q
docker compose up -d
# 容器内校验配置,再热加载
docker exec nginx nginx -t
docker exec nginx nginx -s reload
docker compose logs --tail=50
验证:
curl -I http://127.0.0.1/ # 301
curl -kI https://127.0.0.1/ # 443 正常
ss -ltnp | grep -E ':80|:443' # 宿主监听
七、上线顺序的关键坑:先应用,后入口
Nginx 启动(或 reload)时会立刻解析 upstream 中的主机名。如果被代理的容器还没加入 edge-net,worker 会直接 host not found in upstream 启动失败并重启循环。 正确顺序:
- 先起应用栈并确认它已接入共享网络:
docker network connect edge-net app-web 2>/dev/null || true docker exec nginx wget -qO- http://app-web:8080/ | head -1 - 再加 vhost 配置、
nginx -t && nginx -s reload。只改配置不需要重建 Nginx 容器。
如果后端容器确实需要晚于 Nginx 启动,用 Docker 内置 DNS 做惰性解析,避免启动硬失败:
location / {
resolver 127.0.0.11 valid=30s;
set $upstream app-web;
proxy_pass http://$upstream:8080;
}
变量形式会强制每次请求走 127.0.0.11 解析,后端容器重建换 IP 也不受影响。
八、其他常见问题
| 现象 | 处理 |
|---|---|
| 改了 conf.d 文件没反应 | bind mount 是实时的,但要 docker exec nginx nginx -t && nginx -s reload;用 scp/sed -i 更新证书若换了 inode,必要时 docker compose restart |
老版本 Nginx(1.21/1.24)报 unknown directive http2 |
用 listen 443 ssl http2;,不要用独立的 http2 on; |
| 502 Bad Gateway | 进 nginx 容器 wget -qO- http://app-web:8080/;常见原因:后端未加入同一网络、端口写错、后端只监听 127.0.0.1 |
| 外部打不开但本机能 curl | 云安全组没放行,或本机防火墙拦截;安全组在主机之外,主机内放行无效 |
| 后端拿到的都是内网 IP | 已带 X-Forwarded-For;若 Nginx 前面还有云 LB,按需 set_real_ip_from 配置 LB 网段 |
| 日志增长太快 | compose 里已配 json-file 轮转;Nginx 自身 access_log 也可按日切分(logrotate 或 map 采样) |
此处评论已关闭