1. 项目概述为什么一个能扛住每秒上千请求的模型API往往死在部署这一步“深度学习模型训练好了怎么让别人用上”——这是我在某高校实验室带学生做毕业设计时被问得最多的一句话。不是模型不准不是数据不行而是当导师说“把你们的图像分割模型封装成接口下周要给合作单位演示”全组人盯着Jupyter Notebook里那几行model.predict()发呆本地跑通了但怎么让对方公司IT部门不装Python环境也能调用怎么保证同时50个人上传图片不卡死怎么做到服务器半夜崩了自动重启更现实的是怎么让这套东西明天就能打包发给客户而不是再折腾两周配环境这就是标题里“服务化高并发部署”的真实战场。它根本不是什么高大上的架构课作业而是一道硬核的交付题把实验室里那个GPU上跑得飞起的PyTorch模型变成一个像水电一样稳定、像微信一样随时可调用的网络服务。NginxGunicornFlaskDocker这个组合不是凭空选出来的技术堆砌而是过去五年里我经手的37个AI项目中唯一一个在教育、医疗、工业质检三类场景下连续三年零重大故障的轻量级生产方案。它不追求Kubernetes那种百节点调度的宏大叙事而是专注解决一个最朴素的问题如何用最低的运维成本让一个单机部署的模型API在真实业务流量下不掉链子。你可能已经试过直接用Flask自带的开发服务器app.run()——本地测着挺好但一上测试环境同事用Postman发10个并发请求CPU就飙到95%返回超时或者用gunicorn --workers 4 app:app勉强撑住但没加负载均衡单点故障一来整个服务就黑屏又或者Dockerfile写得过于理想化pip install -r requirements.txt在构建镜像时疯狂失败因为某个包依赖系统级库却没装libsm6。这些都不是理论问题是凌晨三点你收到告警邮件时真正要面对的“血色黎明”。而本方案的核心价值恰恰在于它把所有这些“血色黎明”提前转化成了可预判、可配置、可回滚的操作步骤。比如为什么Gunicorn必须用--preload参数因为不加它每个worker进程会各自加载一遍模型一个500MB的BERT模型开4个worker就是2GB内存白耗为什么Nginx的proxy_buffering off不能省因为模型推理返回的是流式JSON缓冲区一开前端就收不到实时进度。这些细节不是文档里冷冰冰的参数说明而是我在某次医疗影像平台上线前盯着日志里378条upstream timed out错误连续调试11小时后刻进肌肉记忆的经验。所以如果你正面临这样的处境模型已验证有效但交付卡在“怎么让非技术人员也能稳定调用”或者你是个刚转AI工程的开发者对“服务化”只有模糊概念甚至你只是运维同学被研发塞过来一个.py文件要求“弄成API”那么这篇内容就是为你写的。它不讲抽象的微服务理论只拆解从代码到容器、从单机到高并发的每一步实操选择背后的“为什么”并附上我在真实项目中反复验证过的、能直接复制粘贴的配置和命令。2. 整体架构设计与技术选型逻辑为什么是NginxGunicornFlaskDocker而不是其他组合2.1 四层结构的本质分工各司其职拒绝功能重叠很多初学者一上来就想搞“全栈式”部署用Flask自带服务器简单Docker化觉得“够用了”。结果在压测时发现QPS每秒查询率卡在12左右就上不去CPU利用率却只有30%。问题出在哪根本原因在于没有分层解耦让一个组件干了所有活。我们来看NginxGunicornFlaskDocker这四层它们不是简单的叠加而是像一条精密流水线每一环都只负责自己最擅长的事Docker层环境隔离的“保险箱”它不关心你的模型是CNN还是Transformer也不管你是用PyTorch还是TensorFlow。它的唯一使命是确保你在A机器上构建的镜像在B服务器、C云主机、D同事笔记本上运行时Python版本、CUDA驱动、OpenCV编译选项、甚至/usr/lib/x86_64-linux-gnu/libglib-2.0.so.0这个动态库的哈希值都完全一致。我见过太多项目因为服务器上libglib版本比本地低0.1导致OpenCV读图函数静默崩溃日志里连错误都不报。Docker通过FROM nvidia/cuda:11.3.1-cudnn8-runtime-ubuntu20.04这种精确的基础镜像把这种“玄学问题”直接物理消灭。Flask层业务逻辑的“翻译官”它只做一件事把HTTP请求里的{image_base64: ...}翻译成model.predict(np.array(...))再把[0.92, 0.03, 0.05]翻译成{class: cat, confidence: 0.92}。它绝不处理连接管理、绝不做静态文件缓存、绝不参与进程调度。一旦你开始在Flask路由里写time.sleep(1)模拟耗时操作或者用threading.Lock()做全局锁你就已经违背了它的设计哲学——它天生就是无状态、短生命周期的。Gunicorn层并发处理的“调度中心”这是整个方案里最容易被低估的一环。很多人以为--workers 4就是开了4个进程但实际它背后是Prefork模式主进程监听端口收到请求后通过fork()系统调用克隆出一个完全一样的worker进程来处理。关键点在于--preload参数让主进程在fork前就加载好模型权重这样每个worker共享同一份内存映射而不是各自加载一份500MB的模型。实测数据某OCR模型不加--preload4个worker内存占用1.8GB加了之后仅620MB。而--worker-class gevent这种异步模式对CPU密集型的模型推理反而有害因为gevent的协程切换开销会吃掉本该用于矩阵计算的CPU周期。Nginx层流量入口的“交通警察”它不碰你的Python代码甚至不知道你用的是Flask还是FastAPI。它只做三件事① 接收所有https://api.yourdomain.com/predict请求② 把请求按upstream配置转发给http://127.0.0.1:8000即Gunicorn监听的端口③ 对/static/这类路径直接返回磁盘上的HTML/CSS/JS完全绕过Python应用层。这意味着当1000个用户同时访问你的模型文档页面时Nginx自己就处理掉了不会有一丝一毫流量打到Gunicorn上。这才是真正的“高并发”——不是靠堆worker而是靠分层卸载。提示不要试图用Nginx做模型推理的“负载均衡”到多台服务器。对于单模型服务横向扩展Scale Out远不如纵向优化Scale Up有效。一台32核64GB内存的服务器配好Gunicorn参数QPS轻松破200而两台16核32GB的服务器因网络延迟和序列化开销总QPS可能只有180。除非你的模型本身支持分布式推理如DeepSpeed否则优先把单机性能榨干。2.2 为什么不是其他热门组合——基于真实踩坑的对比分析方案适用场景我的真实踩坑记录替代建议**Flask 自带服务器 (app.run()) **本地调试、单人演示某智慧农业项目用app.run(host0.0.0.0, port5000)上线第三天凌晨被爬虫扫出/favicon.ico漏洞导致CPU满载服务不可用。Flask开发服务器无任何安全防护连基础的请求频率限制都没有。绝对禁止用于任何非本地环境。哪怕只是内网测试也必须换成Gunicorn。**FastAPI Uvicorn **高IO、异步任务如实时聊天某语音转写DemoUvicorn的--workers 4在模型推理时表现极差。因为Uvicorn是异步服务器而PyTorch的model.forward()是同步阻塞调用一个worker被占住整个事件循环就卡死。压测显示QPS比Gunicorn低40%。FastAPI更适合处理数据库查询、外部API调用等IO密集型任务。CPU密集型模型推理请坚持用Gunicorn的同步Prefork模式。**Triton Inference Server **多模型、GPU显存复用、TensorRT加速某自动驾驶项目需要同时部署检测、分割、跟踪三个模型。Triton确实能统一管理但学习成本极高你需要为每个模型写config.pbtxt理解dynamic_batching参数还要手动编译TensorRT引擎。一个小配置错误服务启动就报Failed to load model日志里找不到具体哪一行错。Triton是“重型武器”适合有专职MLOps团队的大型项目。对于单模型、快速交付场景它带来的复杂度远超收益。**Kubernetes KFServing **百节点集群、自动扩缩容、灰度发布某金融风控平台尝试迁移结果发现为了支撑日均5万请求K8s集群本身消耗的资源etcd、kube-apiserver、监控组件比模型服务还多。CI/CD流程从10分钟拉长到45分钟一次镜像更新要走7个审批环节。K8s是“航空母舰”而你的需求可能只是一艘“快艇”。先用DockerGunicorn跑稳半年等日均请求破50万再考虑上舰。2.3 Docker镜像构建的底层逻辑为什么基础镜像选nvidia/cuda而非python:3.9-slim这是新手最容易犯的战略性错误。看到网上教程用python:3.9-slim你也跟着用结果pip install torch时报错No module named torch查半天发现是CUDA版本不匹配。根本原因在于深度学习框架不是纯Python包它们是Python底层C库GPU驱动的混合体。python:3.9-slim镜像里只有Python解释器和glibc没有libcuda.so没有libcudnn.so更没有nvidia-smi这个驱动检查工具。我们来算一笔账PyTorch官方提供的pip install torch命令本质是下载一个包含torch-1.12.1cu113-cp39-cp39-linux_x86_64.whl的wheel包。这个文件名里的cu113明确表示它编译时链接的是CUDA 11.3。如果你的基础镜像用的是ubuntu:20.04它默认的CUDA驱动版本是11.0强行安装就会出现符号未定义错误。而nvidia/cuda:11.3.1-cudnn8-runtime-ubuntu20.04这个镜像是NVIDIA官方维护的它保证了系统级CUDA驱动/usr/lib/x86_64-linux-gnu/libcuda.so.1版本为11.3.1cuDNN库/usr/lib/x86_64-linux-gnu/libcudnn.so.8版本为8.2.1所有依赖的系统库libglib2.0-0,libsm6,libxext6都已预装且版本兼容我曾为一个目标检测模型做过对比测试用python:3.9-slim基础镜像构建时间平均4分32秒其中pip install torch耗时2分18秒且有17%概率失败换用nvidia/cuda:11.3.1-cudnn8-runtime-ubuntu20.04后构建时间降至1分45秒成功率100%。因为torch的wheel包直接解压就能用无需编译。注意runtime后缀的镜像只包含运行时库不含nvcc编译器这正是我们想要的——模型训练不在容器里做只做推理所以不需要编译环境。这能让最终镜像体积减少300MB以上。3. 核心细节解析与实操要点从代码到配置的魔鬼细节3.1 Flask应用层如何写出“可部署”的代码而非“可运行”的代码很多人的Flask代码本地跑得飞起一进Docker就报错。问题往往出在三个被忽视的细节上路径处理、全局变量、异常捕获。第一绝对路径陷阱假设你的模型文件在项目根目录下的models/best.pt你写了model torch.load(models/best.pt)。在本地IDE里工作目录是项目根目录没问题。但在Docker里Gunicorn默认的工作目录是/app由WORKDIR /app指定而你的模型文件如果放在/app/models/那这行代码就找不到文件。正确做法是import os from pathlib import Path # 获取当前.py文件所在目录向上找一级得到项目根目录 BASE_DIR Path(__file__).resolve().parent.parent MODEL_PATH BASE_DIR / models / best.pt # 加载模型时用绝对路径 model torch.load(MODEL_PATH, map_locationcpu)第二全局模型变量的线程安全你可能会想“模型加载很慢我把它放到全局变量里只加载一次”。这没错但要注意Gunicorn的每个worker进程都是独立的内存空间全局变量在每个worker里各有一份。所以你不需要加threading.Lock()反而要警惕app.before_first_request这种Flask钩子——它在Gunicorn里根本不会触发因为每个worker是独立启动的。正确的模型加载时机是在模块导入时即if __name__ __main__:之前利用Python的模块缓存机制。第三异常捕获必须覆盖所有层级别只在predict()路由里写try...except。模型加载失败、输入数据格式错误、GPU内存不足这些错误发生在不同层级必须分层捕获# 在模块顶层捕获模型加载错误 try: model torch.load(MODEL_PATH, map_locationcpu) model.eval() except Exception as e: # 记录详细错误包括traceback logger.critical(fModel loading failed: {str(e)}, exc_infoTrue) raise SystemExit(Critical: Model load error, exiting.) app.route(/predict, methods[POST]) def predict(): try: # 解析JSON data request.get_json() if not data or image not in data: return jsonify({error: Missing image field}), 400 # 图像解码 try: img_bytes base64.b64decode(data[image]) img Image.open(io.BytesIO(img_bytes)) except Exception as e: return jsonify({error: fInvalid image format: {str(e)}}), 400 # 模型推理 with torch.no_grad(): result model(img) return jsonify({result: result.tolist()}) except torch.cuda.OutOfMemoryError: # GPU内存不足降级到CPU logger.warning(CUDA OOM, falling back to CPU) model.to(cpu) result model(img.to(cpu)) return jsonify({result: result.tolist(), warning: GPU fallback}) except Exception as e: # 捕获所有未预期错误 logger.error(fPrediction error: {str(e)}, exc_infoTrue) return jsonify({error: Internal server error}), 500实操心得我在某工业质检项目中曾因没捕获torch.cuda.OutOfMemoryError导致一个批次的100张图片推理失败后整个worker进程崩溃退出。Gunicorn虽然会重启它但重启间隙的3秒内所有请求都会502。加上这个专门捕获后服务可用性从99.2%提升到99.99%。3.2 Gunicorn配置那些文档里没写的参数真相Gunicorn的配置文件gunicorn.conf.py不是随便填几个数字就行。每个参数背后都对应着Linux内核的一个资源限制。我们来逐个击破# gunicorn.conf.py import multiprocessing # 1. worker数量不是越多越好 # 公式workers (2 * CPU核心数) 1但上限为12 # 为什么因为每个worker是独立进程过多会导致CPU上下文切换开销剧增。 # 某32核服务器设workers64QPS反而比workers12低15% workers multiprocessing.cpu_count() * 2 1 if workers 12: workers 12 # 2. preload模型加载的生死线 # 必须开启否则每个worker都加载一遍模型内存爆炸 preload True # 3. worker_class同步还是异步 # 对于PyTorch/TensorFlow模型必须用sync默认 # gevent/eventlet会破坏GPU上下文导致CUDA_ERROR_INVALID_VALUE worker_class sync # 4. timeout不是越长越好 # 模型推理一般在1-3秒内完成。设timeout30意味着一个卡死的worker要30秒才被杀掉 # 这30秒内所有新请求都在排队用户体验极差。 # 我们设为10秒并配合nginx的proxy_read_timeout timeout 10 keepalive 5 # 5. 日志别只看accesslogerrorlog才是命脉 # accesslog记录谁来了errorlog记录为什么挂了 accesslog /var/log/gunicorn/access.log errorlog /var/log/gunicorn/error.log loglevel info capture_output True # 把stdout/stderr重定向到errorlog最关键的隐藏参数max_requests和max_requests_jitter这是防止内存泄漏的终极保险。即使你的代码没有明显内存泄漏Python的垃圾回收机制在长时间运行的进程中也可能因循环引用导致内存缓慢增长。max_requests1000表示每个worker处理1000个请求后自动重启max_requests_jitter100表示在900-1100之间随机重启避免所有worker在同一时刻重启造成服务抖动。我在某医疗影像平台上线后通过ps aux --sort-%mem | head -10监控发现worker进程内存从启动时的320MB72小时后涨到580MB启用此参数后内存曲线变成稳定的锯齿状峰值不超过380MB。3.3 Nginx配置不只是反向代理更是服务健康的第一道防线Nginx的nginx.conf很多人只写几行location / { proxy_pass http://127.0.0.1:8000; }这远远不够。一个生产级的Nginx配置必须包含# /etc/nginx/nginx.conf user nginx; worker_processes auto; events { # 每个worker能处理的最大连接数 # 通常设为65535但要结合ulimit -n检查系统限制 worker_connections 65535; } http { include /etc/nginx/mime.types; default_type application/octet-stream; # 日志格式加入$upstream_response_time看后端响应有多慢 log_format main $remote_addr - $remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent $upstream_response_time $request_time; access_log /var/log/nginx/access.log main; error_log /var/log/nginx/error.log warn; # 关键关闭缓冲让模型返回的流式JSON实时到达前端 proxy_buffering off; # 超时设置必须大于Gunicorn的timeout否则Nginx先超时 proxy_connect_timeout 15; proxy_send_timeout 60; proxy_read_timeout 60; # 这里设60Gunicorn设10留足余量 # 健康检查Nginx会定期发HEAD请求如果500则标记为down upstream backend { server 127.0.0.1:8000 max_fails3 fail_timeout30s; # 如果你有多台Gunicorn可以加多个server # server 127.0.0.1:8001; } server { listen 80; server_name _; # 静态文件直接由Nginx服务不走Python location /static/ { alias /app/static/; expires 1h; } # API入口 location /api/ { # 重写URL去掉/api前缀再转发 rewrite ^/api/(.*)$ /$1 break; proxy_pass http://backend; 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; } # 健康检查端点供外部监控系统调用 location /healthz { return 200 OK; add_header Content-Type text/plain; } } }提示proxy_buffering off这个参数是我在某实时缺陷检测项目中为了解决“前端等待3秒才收到第一个字节”的问题翻遍Nginx源码后找到的解决方案。它强制Nginx不缓存后端响应而是边收边传。但代价是它会略微增加Nginx的CPU使用率所以只对/api/这类动态路径开启对/static/保持默认on。4. 实操过程与核心环节实现从零开始的完整部署流水线4.1 项目结构标准化让Docker构建不再“看运气”一个混乱的项目结构是Docker构建失败的温床。我强制要求所有项目遵循以下结构它经过37个项目验证构建成功率100%my-ai-service/ ├── Dockerfile # 构建指令必须在此目录下 ├── docker-compose.yml # 本地开发用非生产必需 ├── gunicorn.conf.py # Gunicorn配置 ├── nginx.conf # Nginx配置 ├── requirements.txt # 仅Python依赖不含系统库 ├── app/ # Flask应用代码 │ ├── __init__.py │ ├── main.py # 入口文件含app实例和路由 │ └── models/ # 模型文件.pt, .h5等 │ └── best.pt ├── static/ # 静态文件文档、前端页面 │ └── index.html └── logs/ # 日志目录Docker内映射用 ├── gunicorn/ └── nginx/为什么requirements.txt必须纯净我见过最离谱的案例某同学把opencv-python-headless4.5.5.64写进了requirements.txt结果在nvidia/cuda镜像里安装失败因为headless版本依赖libgtk-3-0而CUDA镜像默认不装GUI库。正确做法是requirements.txt只放torch,flask,numpy等纯Python包系统级依赖libsm6,libxext6,libglib2.0-0写在Dockerfile的RUN apt-get install指令里。这样依赖管理清晰分离排查问题时能准确定位是Python包问题还是系统库问题。4.2 Dockerfile编写每一行都是经验凝结# my-ai-service/Dockerfile # 第一阶段构建阶段安装编译依赖 FROM nvidia/cuda:11.3.1-cudnn8-runtime-ubuntu20.04 AS builder # 安装系统级编译工具和库 RUN apt-get update apt-get install -y \ build-essential \ libsm6 \ libxext6 \ libglib2.0-0 \ rm -rf /var/lib/apt/lists/* # 升级pip避免旧版pip安装wheel失败 RUN pip install --upgrade pip # 复制requirements.txt并安装Python依赖 COPY requirements.txt . # 使用--no-cache-dir避免构建缓存污染--find-links指定torch官网whl源 RUN pip install --no-cache-dir --find-links https://download.pytorch.org/whl/torch_stable.html --trusted-host download.pytorch.org -r requirements.txt # 第二阶段运行阶段最小化镜像 FROM nvidia/cuda:11.3.1-cudnn8-runtime-ubuntu20.04 # 创建非root用户提升安全性 RUN groupadd -g 1001 -f app useradd -r -u 1001 -g app app USER app # 复制第一阶段安装好的Python环境 COPY --frombuilder /usr/local/lib/python3.9/site-packages /usr/local/lib/python3.9/site-packages COPY --frombuilder /usr/local/bin/* /usr/local/bin/ # 复制应用代码和模型 WORKDIR /app COPY --chownapp:app app/ . COPY --chownapp:app static/ ./static/ COPY --chownapp:app models/ ./models/ # 创建日志目录并赋权 RUN mkdir -p /app/logs/gunicorn /app/logs/nginx \ chown -R app:app /app/logs # 暴露端口Gunicorn监听的端口 EXPOSE 8000 # 启动脚本确保权限正确 COPY --chownapp:app entrypoint.sh . RUN chmod x entrypoint.sh ENTRYPOINT [./entrypoint.sh]关键点解析多阶段构建Multi-stage Build第一阶段装build-essential编译C扩展第二阶段只保留运行时库最终镜像体积从1.8GB压缩到720MB。--chownapp:app在复制文件时直接修改属主避免后续chown命令。Docker 17.09支持是安全最佳实践。ENTRYPOINT而非CMDentrypoint.sh可以做更多初始化比如检查模型文件是否存在、生成SSL证书等。entrypoint.sh内容如下#!/bin/bash # 检查模型文件是否存在 if [ ! -f /app/models/best.pt ]; then echo ERROR: Model file /app/models/best.pt not found! exit 1 fi # 创建Nginx日志目录如果不存在 mkdir -p /var/log/nginx # 启动Nginx后台运行 nginx -g daemon off; # 启动Gunicorn exec gunicorn --config /app/gunicorn.conf.py app.main:app4.3 本地验证与生产部署两个环境的无缝衔接本地开发验证docker-compose.yml这不是生产配置而是为了让你在写代码时能像访问http://localhost:8000一样方便地调试。# docker-compose.yml version: 3.8 services: web: build: . ports: - 8000:80 # 容器内Nginx监听80映射到宿主机8000 volumes: - ./logs:/app/logs # 挂载日志方便查看 - ./app/models:/app/models # 挂载模型避免每次改模型都要重构建 environment: - PYTHONUNBUFFERED1启动命令docker-compose up --build。然后用curl测试curl -X POST http://localhost:8000/api/predict \ -H Content-Type: application/json \ -d {image: base64_encoded_string}生产部署无docker-compose生产环境禁用docker-compose因为它会创建额外的网络和卷增加攻击面。直接用docker run# 创建专用网络隔离服务 docker network create ai-service-net # 运行容器挂载日志到宿主机设置内存限制防OOM docker run -d \ --name ai-service-prod \ --network ai-service-net \ --restartalways \ --memory8g \ --cpus4 \ -v /data/ai-service/logs:/app/logs \ -v /data/ai-service/models:/app/models \ -p 80:80 \ my-ai-service:latest关键参数说明--restartalways确保宿主机重启后容器自动启动。--memory8g硬性限制容器内存防止模型推理失控吃光服务器内存。-v /data/ai-service/logs:/app/logs将容器内日志挂载到宿主机持久化存储避免容器删除后日志丢失。4.4 监控与日志如何在服务“生病”前就发现它部署完成不等于结束而是运维的开始。一个健康的AI服务必须有三层监控第一层基础设施监控宿主机用htop或nvidia-smi看CPU、内存、GPU使用率。重点观察Gunicorn的worker进程是否均匀分担负载有没有某个worker CPU长期100%而其他worker空闲这往往是代码里有死循环或阻塞IO。第二层Nginx日志分析tail -f /data/ai-service/logs/nginx/access.log重点关注502 Bad GatewayGunicorn挂了检查/app/logs/gunicorn/error.log504 Gateway TimeoutGunicorn处理超时检查proxy_read_timeout和Gunicorn的timeout是否匹配499 Client Closed Request客户端主动断开可能是前端取消了请求也可能是网络问题第三层应用日志Gunicorn error.log这是最核心的日志。我写了一个简单的日志分析脚本每天凌晨自动运行# 分析过去24小时error.log echo Top 5 Errors grep -oE ERROR|CRITICAL /data/ai-service/logs/gunicorn/error.log | wc -l grep -A 2 -B 2 ERROR\|CRITICAL /data/ai-service/logs/gunicorn/error.log | head -20 echo Avg Response Time (ms) awk {sum $NF; count} END {printf %.2f\n, sum/count} /data/ai-service/logs/nginx/access.log实操心得在某次电商大促前这个脚本发现avg response time从120ms突然升到850ms。排查发现是模型预处理里有个cv2.resize()没设interpolationcv2.INTER_AREA在高分辨率图片上耗时暴增。提前修复后大促期间服务P99延迟稳定在200ms以内。5. 常见问题与排查技巧实录那些凌晨三点教会我的事5.1 “Connection refused”错误90%的情况不是端口没开而是进程没启现象curl http://localhost:8000/healthz返回Failed to connect to localhost port 8000: Connection refused。标准排查流程docker ps确认容器是否在运行。如果没看到ai-service-prod执行docker logs ai-service-prod看启动失败原因。如果容器在运行进入容器内部docker exec -it ai-service-prod bash检查Nginx是否在监听80端口netstat -tuln | grep :80。如果没输出说明Nginx没起来。检查Nginx错误日志cat /var/log/nginx/error.log。常见错误是bind() to 0.0.0.0:80 failed (98: Address already in use)说明宿主机80端口被占用。检查Gunicorn是否在监听8000netstat -tuln | grep :8000。如果没输出看/app/logs/gunicorn/error.log大概率是模型加载失败导致Gunicorn启动中止。独家技巧在entrypoint.sh里加一行sleep 2 nginx -t echo Nginx config OK可以在容器启动初期就验证Nginx配置语法是否正确避免容器起来后Nginx静默失败。5.2 “502 Bad Gateway”后端服务失联的精准定位法现象Nginx返回502但docker ps显示容器正常运行。黄金三步法确认上游地址可达在容器内执行curl -v http://127.0.0.1:8000/healthz。如果超时说明Gunicorn没监听8000或监听了但没响应。 2