You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

EC2上Nginx无法连接Docker中Nest.js服务器的问题排查求助

问题排查请求:CORS报错+Nginx 502/连接拒绝错误

已排查两周仍未解决,现有两台EC2实例:

  • 实例1:托管运行服务器、Nginx和数据库的Docker容器
  • 实例2:运行基于React的客户端应用

已在NGINX配置文件中设置CORS策略,NGINX容器日志显示预检请求成功,但客户端控制台仍报错:

Access to XMLHttpRequest at 'https://api.<DOMAIN-NAME>/user/get' from origin 'https://<DOMAIN-NAME>.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

核心问题是NGINX尝试连接服务器时返回连接拒绝(111)错误和502网关错误:

GET /user/get HTTP/2.0" 502 559 "https://<domain-name>.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) 
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"
2023/12/01 12:43:00 
[error] 29#29: *34 connect() failed (111: Connection refused) while connecting to upstream, 
client: 162.158.103.162, server: api.<domain-name>.com, 
request: "GET /user/get HTTP/2.0", 
upstream: "http://<container-port>:8080/user/get", 
host: "api.<domain-name>.com", 
referrer: "https://<domain-name>.com/"

Nest.js服务器容器运行正常,未崩溃,日志显示:

Environment variables loaded from .env
Prisma schema loaded from src/database/schema.prisma
Datasource "db": PostgreSQL database "db", schema "public" at "database:5432"
...
[Nest] Nest application successfully started ...

配置文件

docker-compose.yml

version: '3.7'
services:
  database:
    image: postgres:latest
    ports:
      - '5433:5432'
    environment:
      POSTGRES_USER: <user>
      POSTGRES_PASSWORD: <password>
      POSTGRES_DB: <db>
    volumes:
      - data:/var/lib/postgresql/data
    networks:
      - <network>

  server:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - '8080:8080'
    networks:
      - <network>
    depends_on:
      - <db>
    command: sh -c "while ! nc -z <db> 5432; do sleep 1; done && npx prisma db push && npm run start:prod"

  nginx:
    image: nginx:latest
    ports:
      - '80:80'
      - '443:443'
    restart: always
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./certbot/conf:/var/cert
    depends_on:
      - server
    networks:
      - <network>

  certbot:
    image: certbot/certbot:latest
    volumes:
      - ./certbot/www:/var/www/certbot/:rw
      - ./certbot/conf:/etc/letsencrypt/:rw

networks:
  network:

volumes:
  data:

Dockerfile

FROM node:alpine

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm install

COPY . .

COPY ./src/database/schema.prisma ./prisma/ 

RUN npx prisma generate

RUN npm run build

CMD ["npm", "run", "start:prod"]

nginx.conf

events {
    worker_connections 1024;
}

http {
    server {
        listen 80;
        listen [::]:80;

        server_name api.<domain-name>.com;

        location /.well-known/acme-challenge/ {
        root /var/www/certbot;
        }

        location / {
        return 301 https://api.<domain-name>.com$request_uri;
        }
    }


    server {
        listen 443 default_server ssl;
        listen [::]:443 ssl;
        http2 on;

        server_name api.<domain-name>.com;

        ssl_certificate /var/cert/live/api.<domain-name>.com/fullchain.pem;
        ssl_certificate_key /var/cert/live/api.<domain-name>.com/privkey.pem;

        location / {
           add_header 'Access-Control-Allow-Credentials' 'true';
           add_header 'Access-Control-Allow-Origin' 'https://<domain-name>.com' always;
           add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
           add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';             
           add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;

           if ($request_method = 'OPTIONS') {
                add_header 'Access-Control-Allow-Credentials' 'true';
                add_header 'Access-Control-Allow-Origin' 'https://<domain-name>.com';
                add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
                add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';

                add_header 'Access-Control-Max-Age' 1728000;
                add_header 'Content-Type' 'text/plain; charset=utf-8';
                add_header 'Content-Length' 0;
                return 204;
           }

           if ($request_method = 'POST') {
                add_header 'Access-Control-Allow-Credentials' 'true' always;
                add_header 'Access-Control-Allow-Origin' 'https://<domain-name>.com' always;
                add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
                add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';             
                add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
            }

           if ($request_method = 'GET') {
                add_header 'Access-Control-Allow-Credentials' 'true' always;
                add_header 'Access-Control-Allow-Origin' 'https://<domain-name>.com' always;
                add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
                add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';               
                add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
            }

            proxy_pass http://<docker-container-name>:8080;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection 'upgrade';
            proxy_set_header Host $host;
            proxy_cache_bypass $http_upgrade;
        }
    }
}

补充环境信息:

  • 未在服务器端重复配置CORS
  • 客户端应用已启用HTTPS
  • EC2实例防火墙策略:仅个人IP通过22端口SSH,所有IP通过80、443端口HTTP/HTTPS,私有IP通过8080、5433端口TCP

排查思路

  1. 验证Nginx上游地址正确性:

    • 检查nginx.conf中proxy_pass的<docker-container-name>是否与docker-compose.yml中server服务的名称完全一致(Docker Compose默认服务名就是容器网络内的主机名)
    • 在Nginx容器内执行ping <docker-container-name>,确认能解析到正确的容器IP;再执行curl http://<docker-container-name>:8080/user/get,看是否能正常访问接口
  2. 确认Nest.js服务器监听地址:

    • 检查Nest.js代码中listen方法的参数,确保不是只监听127.0.0.1,必须监听0.0.0.0才能让容器外部(包括同一网络的Nginx容器)访问
    • 在server容器内执行netstat -tulpn,确认8080端口是否绑定在0.0.0.0而不是本地回环地址
  3. 检查Docker网络连通性:

    • 确认所有相关容器(server、nginx、database)都加入了同一个自定义网络(docker-compose.yml中定义的<network>)
    • 用docker inspect <network-name>查看网络内的容器列表,确认server和nginx都在其中
  4. 排查容器启动顺序与健康检查:

    • depends_on仅保证容器启动顺序,不保证服务就绪。检查server容器的启动命令是否真的完成了所有初始化步骤(prisma db push、服务启动)
    • 为server服务添加healthcheck配置,确保Nginx只有在server服务就绪后才开始转发请求
  5. 验证CORS头在错误响应中的传递:

    • 当前Nginx的add_header在502错误时可能不会生效(默认只有2xx/3xx响应会添加),需要确保always参数被正确应用到所有CORS头配置(包括主location块和if块)
    • 可以临时修改Nginx配置,在错误页面也添加CORS头,或者直接在Nest.js中添加CORS配置作为兜底
  6. 检查EC2实例内部防火墙:

    • 除了EC2安全组,还要检查实例本身的防火墙(如iptables、ufw)是否限制了容器之间的8080端口访问

内容的提问来源于stack exchange,提问作者Nikita Lafinskiy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 08:30:59