ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Docker+Nginx实战:Python Web应用从本地到服务器稳定部署全攻略

Docker+Nginx实战:Python Web应用从本地到服务器稳定部署全攻略 把Python Web应用从“本地能跑”变成“服务器上稳如老狗”是每个开发者迟早要趟过去的一道坎。你可能遇到过这种情况本地开发环境一切正常一部署到服务器就各种玄学报错或者直接用python app.py裸奔过两天进程莫名其妙没了连个日志都没留下。这篇文章就用一个完整的实操案例讲讲我用Docker Nginx这套组合拳部署Python Web应用的完整过程从方案选型到容器构建再到反向代理配置和问题排查全部覆盖到。文章适合正在学习部署、或者准备把自己的项目上线但对运维流程还不太熟的开发者看完可以直接照着抄作业。1. 方案选型为什么偏偏是Docker加Nginx1.1 部署Python应用的三条路先说大背景。Python Web应用部署到服务器常见方案其实就三条路第一条路是最原始的直接在服务器上装Python环境、装依赖、然后nohup python app.py 挂后台跑。这条路在只有一台机器、一个应用、跑着玩的前提下没问题但只要涉及到多台服务器、多版本Python、依赖冲突或者要频繁发布更新马上就会头大。我早期吃过这个亏——服务器上同时跑着两个项目一个要Python 3.8一个要Python 3.11还有个系统服务需要Python 3.6的环境环境变量、pip装包路径乱成一锅粥折腾到怀疑人生。第二条路是虚拟环境virtualenv / venv加进程管理工具systemd / supervisor。这条路相比第一条路科学了很多至少环境隔离了进程挂了能自动重启。systemd配上Restartalways进程挂了大不了几秒钟自动拉起来。但问题在于换一台新服务器又得把这套环境搭建流程从头走一遍。而且虚拟环境只是在Python包层面的隔离操作系统层面的兼容性照样是你自己负责。第三条路就是Docker容器化。Docker把应用和它依赖的整个运行环境Python解释器、系统库、项目代码、配置文件打包成一个镜像然后镜像跑到哪都是同一个行为。换服务器就三件事装Docker、拉镜像、跑容器。这就完全解决了“在我电脑上是好的”这个永恒难题。1.2 为什么还要加个Nginx在前面按我的经验经常有人问Docker容器本身不就能对外提供服务吗为什么还要多搞一层Nginx反向代理这个问题很关键直接回答因为Python应用服务器比如Gunicorn、Uvicorn在设计上就不是让你直接暴露在公网上的。Django、Flask、FastAPI这类Python Web框架自带的开发服务器runserver性能差且不安全生产环境一般用Gunicorn或Uvicorn这类WSGI/ASGI服务器来跑应用。它们确实可以直接监听端口对外服务但有几个硬伤第一并发连接处理能力相对有限。Gunicorn是基于Worker进程模型的一个Worker同一时间只能处理一个请求。虽然可以用--workers 4这种方式开多个进程但面对静态文件轰炸、慢连接攻击、或者大量并发请求时nginx基于事件驱动、异步非阻塞模型的抗压能力和资源利用效率明显更高。第二缺少很多HTTP层的实用功能。Nginx自带静态文件服务、缓存、请求头改写、访问控制、限流、TLS终止处理HTTPS证书这些能力而让Gunicorn去实现这些你得装额外的中间件、写不少代码还不一定够稳定。第三也是很多人容易忽略的就是安全隔离。Nginx作为反向代理可以过滤掉大量恶意请求只把正常的业务请求转发给后端的容器。云厂商的Web应用防火墙WAF即便接了第一道网关永远是Nginx。所以标准的生产部署架构就是外部流量先进NginxNginx按配置规则将动态请求转发给后端的Docker容器容器里跑着Gunicorn/Uvicorn静态资源则直接由Nginx从磁盘上读取返回不经过Python进程。这套架构的逻辑用一句话概括就是Docker负责让应用跑起来Nginx负责让应用跑得好、跑得稳、跑得安全。两者是分工协作的关系不是一个替代另一个。2. 环境准备服务器、Docker和基础网络2.1 服务器选型与基础配置既然是讲部署默认你手上已经有一台服务器了。如果没有各大云厂商的按量付费实例随便开一台2核4G的配置对于中小型Python Web应用来说绰绰有余。系统推荐Ubuntu 22.04 LTS或Debian 12这两者对Docker的支持最友好遇到问题也最容易搜到解决方案。新服务器到手建议先做几件事更新系统软件包apt update apt upgrade -y创建一个部署用的普通用户强烈不建议用root直接跑应用配置SSH密钥登录关闭密码登录配置基础防火墙ufw或云平台的安全组放行22SSH、80HTTP、443HTTPS端口注意这里说的防火墙规则要小心。Docker安装后默认会操作iptables可能会绕过ufw规则直接暴露容器的端口。关于这个坑后面展开讲先记住一点如果你跑Docker容器时映射了端口别只依赖ufw最好配合安全组或云平台防火墙双重把控。2.2 Docker安装与加速配置Docker的安装有两种方式。一种是直接用系统自带的包管理器装apt install docker.io简单但版本往往偏旧。另一种是官方推荐的安装方式先添加Docker官方GPG密钥和APT源再安装。我个人更推荐第二种因为能保证拿到最新稳定版后续升级也方便。以Ubuntu为例安装步骤大致是# 安装依赖工具 sudo apt install ca-certificates curl gnupg lsb-release # 添加Docker官方GPG密钥 curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg # 添加稳定版仓库 echo \ deb [arch$(dpkg --print-architecture) signed-by/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable | sudo tee /etc/apt/sources.list.d/docker.list /dev/null # 安装Docker引擎及相关组件 sudo apt update sudo apt install docker-ce docker-ce-cli containerd.io docker-compose-plugin安装完成后用docker version验证一下Client和Server都正常输出。如果看到docker: Cannot connect to the Docker daemon八成是没启动守护进程执行sudo systemctl enable --now docker即可。国内服务器拉取镜像往往很慢甚至超时这里有个通用做法配置镜像加速器。编辑/etc/docker/daemon.json把镜像源地址填进去重启Docker生效。不同云厂商有自己的加速地址这里不多展开这个文件本身很灵活你按照自己实际情况填就行{ registry-mirrors: [https://你选择的加速地址] }2.3 网络方案的小讲究Docker容器和主机、容器和容器之间的网络通信模式值得提前想清楚。Docker默认有三种网络模式bridge桥接默认、host主机、none。对多数部署场景直接用默认的bridge模式就行。但有个细节我建议你提前处理好不要在启动容器时随意使用-p参数把端口映射到主机。举个例子如果后端容器端口是8000有些人图省事直接-p 8000:8000于是主机的8000端口对外暴露了。这意味着如果你忘了配Nginx或者Nginx挂了别人依然能绕过Nginx直接打到你的应用容器上。更稳妥的做法是应用容器的服务端口只在Docker内部网络中暴露Nginx容器和它处于同一个自定义bridge网络中通过容器名互相访问。对外只让Nginx监听80/443。这样安全边界更清晰也方便后面做容器间的服务发现。创建一个自定义网络很简单docker network create webapp-net后面跑容器的时候只要挂到这个网络里容器之间就能通过互相的容器名解析到对方IP了。这个设计在后面编排的时候会反复用到。3. 应用容器化Dockerfile与镜像构建实战3.1 一个规范的Dockerfile应该怎么写拿一个典型的FastAPI项目举例。项目结构大概是这样的myapp/ ├── app/ │ ├── __init__.py │ ├── main.py │ └── ... ├── requirements.txt ├── Dockerfile ├── .dockerignore └── ...对应的Dockerfile可以这样写以Python 3.11为基础镜像# 基础镜像 FROM python:3.11-slim # 设置环境变量避免Python产生字节码缓冲区文件并保证标准输出不被缓冲 ENV PYTHONDONTWRITEBYTECODE1 \ PYTHONUNBUFFERED1 # 设置工作目录 WORKDIR /app # 安装系统依赖 RUN apt-get update \ apt-get install -y --no-install-recommends gcc libpq-dev \ rm -rf /var/lib/apt/lists/* # 先拷贝requirements.txt并安装依赖充分利用Docker的层缓存 COPY requirements.txt /app/ RUN pip install --no-cache-dir -r requirements.txt # 拷贝项目代码 COPY . /app/ # 创建非root用户运行应用 RUN useradd -m -s /bin/bash appuser USER appuser # 暴露应用端口 EXPOSE 8000 # 启动Gunicorn CMD [gunicorn, app.main:app, -w, 4, -k, uvicorn.workers.UvicornWorker, -b, 0.0.0.0:8000]这里有几个细节值得说道说道。第一选择python:3.11-slim而不是python:3.11。slim版本剔除了大量用不到的软件包镜像从1个多G缩到200多M构建更快占用的磁盘和内存都更少。如果项目需要用到编译型依赖比如pandas、numpy、lxml这些那就得保留build-essential之类的编译工具链。gcc和libpq-dev是因为我的项目要用到PostgreSQL属于具体情况具体安装不需要就别加。第二先拷贝requirements.txt再拷贝代码。这是利用Docker的层缓存机制。Docker构建镜像时每一条COPY或RUN指令都会生成一个新的镜像层如果某一步的内容没有变化Docker会直接复用缓存。把第三方依赖的安装放在最前面这样以后每次修改代码重新构建镜像时pip安装依赖的那一步几乎不会变动构建速度会快很多。我见过不少人一上来就COPY . /app然后再RUN pip install结果是每次改一行代码都要重新把依赖装一遍纯粹浪费生命。第三用非root用户运行应用。容器默认以root身份运行一旦应用被攻击利用攻击者就直接获得了容器内最高权限。虽然容器有隔离限制但能降级还是尽量降级。创建用户、切换用户这不能说杜绝风险但至少是个最基本的保险。3.2 依赖管理与版本锁定requirements.txt的生成方式有个讲究。如果你是手动往里面一条条加依赖名那版本往往没锁定过几天重新构建镜像pip可能拉到一个不兼容的新版本应用就跑不起来了。更好的做法是在本地开发环境用pip freeze导出pip freeze requirements.txt这样导出的是当前环境中所有包的确切版本号构建时能最大程度保证可复现。但注意pip freeze会把传递依赖也全部列出来比较臃肿。更推荐的方式是用pipreqs按项目实际导入的模块来生成pipreqs ./ --force它会扫描项目代码里所有import语句只生成直接依赖列表干净清爽。当然这两种方式各有适用场景项目大了我建议还是用虚拟环境加pip freeze稳妥。如果项目比较正式甚至可以再进一步用锁定版本的约束文件pip install -r requirements.txt这个很简单不多说了但版本锁定的原则一定要记住——我踩过最惨的一次坑就是某个依赖库在某个深夜默默发了个新版本然后我第二天凌晨发布新版本应用服务直接崩了连个像样的报错日志都没留下来。3.3 .dockerignore镜像瘦身的第一步很多新手会漏掉.dockerignore文件。它的作用跟.gitignore类似在构建时忽略指定的文件或目录不把它们发往Docker构建上下文。__pycache__/ *.pyc *.pyo .env .git/ .venv/ venv/ *.md .gitignore Dockerfile .dockerignore这个文件不是可有可无的。比如.venv目录可能有几百兆的体积如果忘了排除每回构建镜像都得把这些文件先传到Docker守护进程既慢又让镜像膨胀。还有.env文件如果在构建上下文里被COPY . /app复制进镜像那等于把数据库密码、密钥这些敏感信息打包进了镜像严重安全隐患。3.4 镜像构建与验证一切就绪后在项目根目录执行docker build -t myapp:latest .看到Successfully built和Successfully tagged就说明构建成功了。构建完成先别急着部署在本地先跑起来验证docker run --rm -p 8000:8000 myapp:latest如果本地浏览器能通过http://localhost:8000正常访问说明镜像内部的基本逻辑没问题。这里提个经验本地验证环境用的Python版本最好和服务器基础镜像保持一致不然会出现本地跑得好好的、镜像里各种报错的情况。4. 用docker-compose编排整个服务栈4.1 为什么不用裸docker run一个完整的部署架构不会只有一个应用容器。以我的项目为例还需要一个PostgreSQL数据库容器再加上后面的Nginx容器。如果用docker run一条条命令去启动输入参数很长不说整个启动顺序、网络关系、环境变量全靠人脑记忆重新部署一次简直是灾难。因此强烈建议从一开始就用docker-compose.yml来做编排。它把这些容器的配置固化成代码启动整个服务栈只需要一条命令docker compose up -d先来看一份完整的docker-compose.yml然后拆开讲每个部分services: db: image: postgres:16 container_name: myapp-db restart: unless-stopped environment: POSTGRES_USER: myapp POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_DB: myapp volumes: - db_data:/var/lib/postgresql/data healthcheck: test: [CMD-SHELL, pg_isready -U myapp] interval: 5s timeout: 5s retries: 5 networks: - webapp-net app: build: . container_name: myapp-app restart: unless-stopped depends_on: db: condition: service_healthy environment: DATABASE_URL: postgresql://myapp:${DB_PASSWORD}db:5432/myapp volumes: - static_data:/app/staticfiles networks: - webapp-net nginx: image: nginx:1.27-alpine container_name: myapp-nginx restart: unless-stopped ports: - 80:80 - 443:443 volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - ./nginx/conf.d:/etc/nginx/conf.d:ro - static_data:/var/www/static:ro - ./ssl:/etc/nginx/ssl:ro depends_on: - app networks: - webapp-net volumes: db_data: static_data: networks: webapp-net: external: true4.2 服务间的依赖与健康检查这里有几个关键点。depends_on在docker compose v3里是可以配置条件的。上面例子中app服务会等待db服务通过健康检查后才启动避免出现“应用先启动连不上数据库直接崩溃退出”的经典问题。数据库容器的healthcheck也很简单用PostgreSQL自带的pg_isready命令探测数据库是否就绪。如果不写condition: service_healthy默认的depends_on只是控制启动顺序并不能保证数据库真的可用。启动顺序对了不代表依赖就绪了这个细节经常坑人——容器A比容器B先启动但B可能还要花好几秒做初始化。restart: unless-stopped也是一个很容易被忽略但很实用的配置。有了它容器因为意外崩溃退出后Docker会自动把它拉起来。配合Gunicorn的多Worker模型日常使用中即使某个Worker进程异常退出服务整体也几乎感觉不到中断。4.3 环境变量的管理docker-compose.yml里使用了${DB_PASSWORD}这种写法这是从.env文件读取变量。.env文件放在项目根目录内容大致是DB_PASSWORDyour_strong_password这个文件要被.gitignore忽略掉千万别提交到代码仓库。权限建议设为600只有部署用户能读。这样做的好处是敏感信息不写死在docker-compose.yml里也不进到Docker镜像里全部集中在部署时按需注入。5. Nginx配置剖析反向代理、静态文件与性能调优5.1 主配置结构Nginx容器化部署时习惯上把配置通过Volume挂载进容器。我通常把配置放在服务器上项目的nginx/conf.d/default.conf然后在项目里完整写一份。一个适配上面服务栈的Nginx配置大体如下upstream myapp_backend { server app:8000; keepalive 32; } server { listen 80; server_name example.com www.example.com; # 静态文件直接由Nginx处理 location /static/ { alias /var/www/static/; expires 30d; add_header Cache-Control public, immutable; } # 后端动态请求反向代理 location / { proxy_pass http://myapp_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; proxy_connect_timeout 60s; proxy_read_timeout 120s; proxy_send_timeout 60s; proxy_buffering on; proxy_buffer_size 8k; proxy_buffers 8 32k; } }5.2 upstream与proxy_pass核心机制upstream块定义了一组后端服务器。这里的server app:8000中的app是docker-compose里应用服务的服务名。Nginx容器内是通过Docker内置DNS解析到这个服务名的前提是Nginx和应用容器在同一个自定义网络中。这比写死容器IP的方式灵活得多——容器重建IP变了也不影响。keepalive 32是用来开启Nginx对后端应用的HTTP长连接复用。如果没有这行每个请求Nginx都会跟后端新建立一条TCP连接握手开销非常大。在高并发场景下连接数会变成严重瓶颈。加了这个之后同一批TCP连接可以反复复用来转发请求性能提升非常明显。proxy_pass http://myapp_backend就是把请求转发给upstream里定义的服务器组。这里有个容易踩的坑proxy_pass的URL末尾是否带路径比如http://myapp_backend/会影响转发的URI拼接规则。比如在location /api/下带不带斜杠的行为是有区别的。我自己的习惯是只写http://myapp_backend不带路径让后端从原始URI开始处理行为最好预测。5.3 请求头设置的玄机proxy_set_header三件套几乎是必须的Host $host把原始请求的Host头传给后端。如果缺失这行后端Django或FastAPI在做域名校验时可能会拒绝请求。X-Real-IP $remote_addr把真实客户端IP传给后端。Nginx转发请求时后端的REMOTE_ADDR是Nginx的IP没有这个头后端记录的日志全是内网IP排查问题会很痛苦。X-Forwarded-For保留整个代理链上的客户端IP列表。X-Forwarded-Proto $scheme告诉后端客户端是用http还是https访问的。如果Missing这个后端在生成绝对URL链接时可能会把所有链接都生成成http导致你明明开了HTTPS页面里却出现http的跳转。后端Django或者FastAPI侧还需要配合配置PROXIES或TRUSTED_HOSTS等选项来信任这些头否则它们默认只信任来自Nginx127.0.0.1的转发。FastAPI的Uvicorn启动时通常要加上--forwarded-allow-ips*或者设置为Nginx容器IP才能让request.client.host拿到真实IP。5.4 静态文件处理的优化静态文件交给Nginx直出是一个巨大的性能优化点。一个图片、CSS或JS文件如果每次都打进Python进程由Gunicorn处理会白白占用Worker进程。Nginx处理静态文件是底层的sendfile系统调用效率高好几个数量级。配置里location /static/的alias /var/www/static/是把URL路径映射到容器内路径。静态文件的数据卷static_data同时挂载到了应用容器在/app/staticfiles和Nginx容器在/var/www/static这样Django或FastAPI在容器内执行collectstatic收集静态文件时文件就落在共享卷里Nginx直接就能读到。expires 30d和Cache-Control是给静态文件设置长缓存。这类文件内容通常带有哈希指纹文件名变了说明内容变了所以可以放心让浏览器缓存30天。5.5 代理缓冲与超时配置proxy_buffering和缓冲大小相关配置我之前一直没怎么注意直到有一次线上出现大响应体传输极慢才去研究。默认情况下Nginx是有开启代理缓冲的响应先被Nginx读入缓冲区再发给客户端。好处是后端处理速度慢没关系客户端随时可以从Nginx缓冲区取数据后端Worker可以尽快释放出来处理下一个请求。如果项目里有下载大文件场景比如导出报表、传视频proxy_buffering off反而更合适相当于Nginx当个管道直接透传避免大文件全部缓冲到磁盘把临时目录塞爆。超时设置需要注意我遇到过WebSocket长连接被Nginx在60秒后切断的问题。如果是WebSocket应用Nginx配置还需要加上proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade;否则默认的Connection头设置会干扰WebSocket的升级握手。5.6 开启HTTPS普通场景现在部署Web应用基本都要上HTTPS。用certbot这类工具申请Lets Encrypt免费证书是很成熟的做法申请完的证书文件放下来server { listen 443 ssl http2; server_name example.com; ssl_certificate /etc/nginx/ssl/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; # 其余location配置与80端口保持一致 } server { listen 80; server_name example.com; return 301 https://$host$request_uri; }证书续期是个需要定时处理的事。我现在的做法是在宿主机上装certbot用--webroot模式或者直接走DNS插件做续期然后把新证书通过Volume映射给Nginx容器续期后执行docker exec myapp-nginx nginx -s reload让配置热加载。6. 部署发布全流程从构建到上线的完整操作记录6.1 首次上线的完整命令序列所有配置就绪后首次上线的操作流程大体是# 1. 在服务器上创建项目目录 mkdir -p /opt/myapp cd /opt/myapp # 2. 把项目代码上传git clone或者scp都可以 git clone https://your-repo-url/myapp.git . # 3. 准备环境变量文件 cp .env.example .env vim .env # 4. 创建Docker自定义网络 docker network create webapp-net # 5. 构建并启动所有服务 docker compose up -d --build # 6. 确认服务状态 docker compose ps docker compose logs -f app第一次执行时up -d前加上--build是强制构建镜像后面日常更新代码时如果镜像内容有变化也要带上--build。6.2 更新发布的滚动流程日常迭代发布我总结了一套固定的操作序列基本不会出意外# 1. 拉取最新代码 git pull origin main # 2. 重新构建受影响的镜像并启动 docker compose up -d --build # 3. 观察日志确认服务正常 docker compose logs --tail100 app # 4. 确认无误后清理旧镜像 docker image prune -f这里有个小坑需要特别注意docker compose up -d --build在发现镜像变了后会重新创建容器但这个过程中老容器是停止到新容器启动之间有几十秒的间隙对用户来说会感觉到短暂的不可用。如果对可用性有要求可以改用蓝绿发布机制也就是先启动一组新容器等健康检查通过后再切换Nginx转发。作为个人博客或中小型应用上面的简单流程已经足够了。6.3 日志管理的最佳实践容器日志默认由Docker接管用docker compose logs就能看。但容器长期运行日志文件会越来越大。Docker自带的日志驱动默认是json-file支持配置轮转策略在daemon.json里加上{ log-driver: json-file, log-opts: { max-size: 10m, max-file: 5 } }这样单个日志文件超过10M就会自动轮转最多保留5个历史文件避免磁盘被日志撑爆。同理Nginx的访问日志和错误日志在容器内默认是打到stderr和stdout的会被Docker日志驱动统一接管所以不用再配置写文件了。6.4 数据库的备份与恢复应用跑到数据重要级别上升后数据库备份就是必须项了。容器内的PostgreSQL备份其实很直接# 备份 docker exec myapp-db pg_dump -U myapp myapp /opt/backups/myapp_$(date %Y%m%d_%H%M%S).sql # 恢复 cat backup.sql | docker exec -i myapp-db psql -U myapp myapp定时备份用cron在宿主机上跑一个脚本就行。备份是小事但忘记备份导致的数据丢失是大事。我个人的习惯是每天一次全量备份保留最近7天的备份文件。7. 高频故障排查50多个为什么里的关键20个7.1 容器起不来、退出循环这类问题最常出现在首次部署阶段。常规排查思路是看日志docker compose logs app最常见的四种原因我都遇到过端口冲突。容器要绑定8000端口但宿主机上已经有别的进程占用。报错信息通常是bind: address already in use。排查用ss -tlnp | grep 8000。数据库连不上。应用启动时报OperationalError: could not connect to server。如果你看了db健康检查发现db容器自己先崩了要看db的日志通常是数据目录的权限或环境变量配置问题。依赖缺失。启动时报ModuleNotFoundError这说明镜像里打包的依赖跟代码需要的依赖不一致。要么是requirements.txt漏了包要么是缓存了旧的依赖层导致没装上新的。ENTRYPOINT和CMD写错。用Dockerfile的CMD启动gunicorn如果命令本身拼写错了容器会立即退出。日志里会有exec gunicorn: not found之类的提示注意看。7.2 502 Bad Gateway排查这个状态码意味着Nginx转发到后端连接失败。用排除法逐层看后端容器还活着吗docker compose ps看状态。日志里有没有报错docker compose logs --tail50 app。Nginx能解析到app吗docker exec myapp-nginx ping app。端口对不对容器内应用监听的是8000Nginx转发也写的8000吗我曾经犯过一个错误应用改成了监听8080Nginx配置没更新502挂了半小时。注意Gunicorn启动时-b 0.0.0.0:8000的0.0.0.0是必须的。如果写成127.0.0.1:8000容器外部包括Nginx容器就无法访问到它。这是新手最容易犯的错之一本地跑没问题一上容器就不通。7.3 504 Gateway Timeout排查后端处理请求超过Nginx设置的时间阈值返回了超时。常规做法是加大proxy_read_timeout但更根本的问题是后端确实慢。先看后端日志里对应请求的执行时间确认是单次请求本身慢比如查询了很大数据量还是整体负载太高所有请求都慢。前者优化SQL和加缓存后者增加Gunicorn worker数。Gunicorn的worker数有个经验公式2 * CPU核数 1。比如2核服务器就开5个worker4核就开9个。7.4 静态文件404部署Django项目的经典坑。先确认collectstatic是否真的执行了且输出到了两个容器都能访问的共享卷。Django设置里STATIC_ROOT要指向容器内的/app/staticfiles执行docker compose exec app python manage.py collectstatic --noinput如果Django项目还要在Nginx里检查location /static/和容器内/var/www/static路径是否对得上。路径末尾的斜杠很容易出问题alias /var/www/static/和alias /var/www/static行为差别很大前者匹配后会把/static/前缀替换成/var/www/static/并拼上后面的文件名后者则可能产生路径拼接错误。7.5 502误报Nginx日志乱码乱时间如果Nginx容器内日志时间不对多半是容器时区不是Asia/Shanghai。可以在部署时给容器设置TZAsia/Shanghai环境变量或挂载/etc/localtime。不然排查问题的时候日志时间跟实际时间是8个小时的偏差非常容易误导。7.6 应用日志里看到的IP全是容器IP这个在5.3小节已经说了需要配置X-Real-IP头。后端框架侧FastAPI/Uvicorn要用--forwarded-allow-ips或等价配置Django要在ALLOWED_HOSTS里加上域名并且设置USE_X_FORWARDED_HOST True。配置完访问一次再看应用日志确认已经拿到真实IP。8. 运维日常安全加固与性能观察8.1 宿主机安全注意事项这里是从运维角度必须做的几个加固动作更新系统。服务器上跑的系统和软件包要定期升级很多安全漏洞补丁都靠这个。SSH安全。禁用root密码登录改用密钥登录考虑更换默认的22端口虽然治标不治本但能少挨很多扫描。云平台安全组。即使Nginx容器只映射了80/443端口也要检查一下云安全组是否只放行这两个端口。Docker在iptables层面的操作机制可能让映射端口穿透到公网。Docker守护进程安全。别把Docker socket挂载进容器那等于把Docker的root权限给容器了。这种操作在CI/CD之外的地方都不建议用。8.2 磁盘与内存的观察命令每天看一眼服务器状态是个还算不错的习惯# 总体资源占用 top # 磁盘空间 df -h # Docker系统占用 docker system dfdocker system df能显示镜像、容器、卷、构建缓存各自占用多少空间。构建缓存会越积越大我习惯定期执行docker builder prune -f清理。8.3 一套简单的健康监控如果不想上Prometheus那一整套重家伙可以用最简单的办法在宿主机写个cron脚本定时探测应用HTTP状态码#!/bin/bash if curl -fsS http://127.0.0.1/health /dev/null; then echo OK else echo FAIL, restarting... docker compose -f /opt/myapp/docker-compose.yml restart app fi虽然简单粗暴但对付中小规模应用足够了。更重要的是要让后端应用实现一个/health端点返回200和容器内关键依赖的健康状态比如能不能ping通数据库这样监控才有意义。9. 一些想跟你单独聊的运维心得写到这儿核心内容差不多都讲完了。最后说几句没什么系统逻辑的经验之谈吧。第一部署层面的事最怕两个字手生。哪怕你把这个教程看完、把配置抄走真正的坑永远在你自己的项目里。所以我的建议是你先在本地虚拟机上把整套流程完整跑一遍从Dockerfile、docker-compose、Nginx配置到整个部署发布顺了再上真实服务器操作。在本地练熟悉了能少熬好几个夜。第二出问题的时候先冷静看日志再动手改配置。没有日志依据的瞎改是在给生产环境埋雷。改完一项测一项一次只改动一个变量这样出了问题你总是能精确定位到是哪一步引入的。第三尽量保证环境和配置的可复现性。Docker镜像打上版本标签比如myapp:20241101docker-compose.yml和Nginx配置纳入git管理。你永远不知道哪一天需要快速回滚到某个旧版本好的版本记录能让你灰溜溜的时候有条退路。最后持续学习和积累自己的checklist。运维这件事你能踩的坑永远是踩不完的但踩过一次的坑就不要再踩第二次。这篇内容覆盖的是我这几年在Python应用部署上踩过的绝大部分坑希望对你有用。你只要能跑通一次后面就顺了。
返回列表