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
排查思路
验证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,看是否能正常访问接口
- 检查
确认Nest.js服务器监听地址:
- 检查Nest.js代码中
listen方法的参数,确保不是只监听127.0.0.1,必须监听0.0.0.0才能让容器外部(包括同一网络的Nginx容器)访问 - 在server容器内执行
netstat -tulpn,确认8080端口是否绑定在0.0.0.0而不是本地回环地址
- 检查Nest.js代码中
检查Docker网络连通性:
- 确认所有相关容器(server、nginx、database)都加入了同一个自定义网络(
docker-compose.yml中定义的<network>) - 用
docker inspect <network-name>查看网络内的容器列表,确认server和nginx都在其中
- 确认所有相关容器(server、nginx、database)都加入了同一个自定义网络(
排查容器启动顺序与健康检查:
depends_on仅保证容器启动顺序,不保证服务就绪。检查server容器的启动命令是否真的完成了所有初始化步骤(prisma db push、服务启动)- 为server服务添加
healthcheck配置,确保Nginx只有在server服务就绪后才开始转发请求
验证CORS头在错误响应中的传递:
- 当前Nginx的
add_header在502错误时可能不会生效(默认只有2xx/3xx响应会添加),需要确保always参数被正确应用到所有CORS头配置(包括主location块和if块) - 可以临时修改Nginx配置,在错误页面也添加CORS头,或者直接在Nest.js中添加CORS配置作为兜底
- 当前Nginx的
检查EC2实例内部防火墙:
- 除了EC2安全组,还要检查实例本身的防火墙(如iptables、ufw)是否限制了容器之间的8080端口访问
内容的提问来源于stack exchange,提问作者Nikita Lafinskiy
相关产品推荐
相关产品推荐

