☰
Docker+Nginx实战:Python Web应用从本地到服务器稳定部署全攻略
2026/10/8 2:35:51 网站建设 项目流程

把Python Web应用从“本地能跑”变成“服务器上稳如老狗”,是每个开发者迟早要趟过去的一道坎。你可能遇到过这种情况:本地开发环境一切正常,一部署到服务器就各种玄学报错;或者直接用python app.py裸奔,过两天进程莫名其妙没了,连个日志都没留下。这篇文章就用一个完整的实操案例,讲讲我用Docker + Nginx这套组合拳部署Python Web应用的完整过程,从方案选型到容器构建,再到反向代理配置和问题排查,全部覆盖到。文章适合正在学习部署、或者准备把自己的项目上线但对运维流程还不太熟的开发者,看完可以直接照着抄作业。

1. 方案选型:为什么偏偏是Docker加Nginx

1.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配上Restart=always,进程挂了大不了几秒钟自动拉起来。但问题在于:换一台新服务器,又得把这套环境搭建流程从头走一遍。而且虚拟环境只是在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。

所以标准的生产部署架构就是:外部流量先进Nginx,Nginx按配置规则将动态请求转发给后端的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或云平台的安全组),放行22(SSH)、80(HTTP)、443(HTTPS)端口

注意:这里说的防火墙规则要小心。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 PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 # 设置工作目录 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: true

4.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_PASSWORD=your_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等选项来信任这些头,否则它们默认只信任来自Nginx(127.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这类工具申请Let's 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 --tail=100 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转发到后端连接失败。用排除法逐层看:

  1. 后端容器还活着吗?docker compose ps看状态。
  2. 日志里有没有报错?docker compose logs --tail=50 app。
  3. Nginx能解析到app吗?docker exec myapp-nginx ping app。
  4. 端口对不对?容器内应用监听的是8000,Nginx转发也写的8000吗?我曾经犯过一个错误:应用改成了监听8080,Nginx配置没更新,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个worker,4核就开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。可以在部署时给容器设置TZ=Asia/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 df

docker 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:20241101),docker-compose.yml和Nginx配置纳入git管理。你永远不知道哪一天需要快速回滚到某个旧版本,好的版本记录能让你灰溜溜的时候有条退路。

最后,持续学习和积累自己的checklist。运维这件事,你能踩的坑永远是踩不完的,但踩过一次的坑就不要再踩第二次。这篇内容覆盖的是我这几年在Python应用部署上踩过的绝大部分坑,希望对你有用。你只要能跑通一次,后面就顺了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询