把 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 启动失败并重启循环。 正确顺序:

  1. 先起应用栈并确认它已接入共享网络:
    docker network connect edge-net app-web 2>/dev/null || true
    docker exec nginx wget -qO- http://app-web:8080/ | head -1
  2. 再加 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 采样)
最后修改:2026 年 09 月 20 日
如果觉得我的文章对你有用,请随意赞赏