我最早部署Python Web应用的时候,还是老老实实在服务器上装Python、建虚拟环境、再写一个systemd服务去托管进程。后来发现这套流程实在太脆弱了:服务器上同时跑两三个项目时,Python版本互相打架,依赖包经常冲突,一次系统升级就能把环境搞坏,迁移到新服务器更是从头再来一遍。所以我从某个阶段开始,新项目部署基本统一改成“Docker + Nginx”这套组合:应用打成镜像,环境彻底隔离,再通过Nginx统一对外提供访问入口。这篇文章整理的就是这套部署流程的完整实操记录,从Dockerfile编写、docker-compose编排,到Nginx反向代理配置,再加上我在真实环境里踩过的各种坑。如果你手头正好有一个准备上线的Python Web应用,或者刚拿到一台新服务器想建立一套规范的部署方式,这篇内容可以直接拿来当操作手册。
1. 部署方案选型:为什么选择 Docker + Nginx 的组合
1.1 裸机部署踩过的坑
先说说我为什么放弃裸机部署。很多开发者第一次上线Python应用时,习惯沿用本地开发的方式:在服务器上装Python、执行pip install -r requirements.txt、然后用nohup或者systemd把Gunicorn拉起来。看起来简单,但实际维护起来问题一个接一个。
最典型的是Python版本冲突。当时我负责的一个服务器上,老项目依赖Python 3.6,新项目要用Python 3.11,系统自带的Python版本又不能随便动,最后只能从源码编译了一套新解释器。更麻烦的是依赖包,A项目的某个第三方库要1.x版本,B项目却必须用2.x版本,放在同一个环境里根本没法共存。为了隔离,我试过virtualenv、试过pyenv,但每次换服务器都要重新装一遍系统依赖、编译工具链,光环境搭建就能耗掉大半天。
进程管理也是痛点。用systemd管理Gunicorn,写service文件倒不难,但服务崩溃后的重启策略、开机自启、多实例端口分配,每个细节都在消耗精力。遇到一个接口把内存吃满,进程被OOM Killer干掉,如果没写好Restart=always,服务就悄悄挂了,用户访问直接超时。这种“半夜服务挂掉”的经历,我相信很多运维过的朋友都有体会。
这些问题的本质,是应用和运行环境强耦合。裸机上的环境是共享的、脆弱的、不可重复的,任何一次变更都可能波及其他项目。所以我后来的选择很明确:用容器把应用连同运行环境一起打包。
1.2 Docker 与传统部署方式的核心差异
Docker解决的核心问题,可以概括成一句话:让应用和环境一起交付。
传统部署方式里,开发者交付的是代码,运行环境由服务器临时拼凑。Docker交付的是一个镜像,镜像里不仅有代码,还有Python解释器、系统依赖库、配置文件、启动命令。不管镜像在什么机器上运行,跑起来的行为都是一致的。这个特性在项目迁移时尤其有用,旧服务器退役,新服务器只要装上Docker,把镜像拉过去再启动,应用就回来了。
我习惯用“搬家”来类比:以前部署应用就像带着一箱锅碗瓢盆搬到新房子,但新房子有没有煤气灶、水龙头接口对不对得上都不确定;Docker相当于把整个厨房都打包成了集装箱,到了新地方直接接上水电就能做饭。
当然Docker不是没有代价。镜像会占用额外的磁盘空间,容器多了一层文件系统,排障时也要适应新的命令体系。但相比它带来的环境一致性、进程隔离、快速伸缩这些收益,这点代价完全值得。尤其是在服务器上同时运行多个项目时,每个项目一个容器,互不干扰,端口冲突、依赖冲突这类问题从根源上消失。
1.3 Nginx 在这套架构里扮演什么角色
容器把应用跑起来了,但还需要一个统一的入口,这就是Nginx的活。
Python Web应用通常用Gunicorn或uvicorn监听某个端口,比如8000。如果直接把8000端口暴露给公网,会有一堆问题:一个服务器上可能有多个应用,80端口只能给一个用;Python进程直接处理静态文件请求效率很低;HTTPS证书的配置和续期也缺少一个统一入口。Nginx作为反向代理,正好把这些事情都接了过去。
在我的部署架构里,Nginx负责四个核心任务:
- 接收外部的HTTP/HTTPS请求,转发给后端的Python应用容器;
- 把
/static/、/media/这类静态资源请求直接返回文件,完全不经过程序进程; - 终结SSL证书,统一处理HTTPS加密和跳转;
- 作为流量的统一入口,后续如果要加负载均衡、限流,也都在这一层实现。
关于Nginx放在宿主机还是容器里,我见过两种做法。如果服务器上只有一个Web应用,把Nginx放进docker-compose里一起编排最省心;如果一台服务器上已经装了Nginx并管理着多个站点,那宿主机Nginx反向代理到容器端口会更合适。本文后面的配置按Nginx容器方案写,因为这个方式结构清晰,新增服务时也容易扩展。
2. 部署前的准备:环境、目录与应用改造
2.1 服务器环境初始化
部署前先把服务器的底层环境准备好。我这里以Ubuntu/Debian系为例,其他发行版思路类似。
首先安装Docker和Compose插件:
sudo apt update sudo apt install -y docker.io docker-compose-plugin sudo systemctl enable --now docker sudo usermod -aG docker $USER安装完记得重新登录一次,让用户组权限生效。我习惯创建专门的部署用户,而不是直接用root跑服务,减少误操作风险。
接下来确认防火墙。如果是云服务器,安全组里放行80和443端口;如果是本机ufw,可以执行:
sudo ufw allow 'Nginx Full'数据库、Redis这类基础服务只在内网使用,不要暴露到公网,避免不必要的安全隐患。另外用nproc看一下服务器核心数,后面配置Gunicorn worker数会用到。
我在服务器上的项目目录一般按这个结构规划:
/opt/apps/example/ ├── app/ # 项目代码 │ ├── Dockerfile │ ├── docker-compose.yml │ ├── requirements.txt │ └── src/ ├── data/ # 应用产生的数据,挂载进容器 ├── logs/ # 宿主机的日志备份目录 └── nginx/ # Nginx 配置 └── default.conf代码放在app/下,运行产生的数据放data/,Nginx配置单独建目录。这样备份时只需备份data/和nginx/,代码随时能用git重新拉取,数据不丢,配置也好找。
2.2 应用代码需要做哪些适配
不是所有本地能跑的Python应用都能直接容器化,上线前需要做几个小改造。
第一,监听地址改成0.0.0.0。本地开发时经常监听127.0.0.1,这个地址在容器里意味着只有容器内部能访问,宿主机和其他容器都访问不到。Gunicorn启动参数里的-b 0.0.0.0:8000就是这个作用。
第二,配置项要从代码里剥离出来,通过环境变量注入。比如数据库连接串、SECRET_KEY、DEBUG这些,不同环境的值不一样,写死在代码里既不安全,也不利于容器化。我通常会在项目里写一个读取环境变量的配置模块:
import os DEBUG = os.getenv("DEBUG", "false").lower() == "true" SECRET_KEY = os.getenv("SECRET_KEY", "change-me") DB_HOST = os.getenv("DB_HOST", "localhost")第三,日志输出到标准输出,而不是写入文件。容器里写日志文件不是不行,但维护麻烦,还是依赖Docker的日志机制更干净。后面会用docker compose logs统一查看日志。
第四,静态文件处理。Django项目要执行collectstatic把静态文件收集到指定目录,Flask项目要明确静态目录的路径,后面再配置Nginx直接托管这些文件。
还有一个容易忽略的点:如果应用要连接数据库,容器环境里的数据库地址不是localhost,而是docker-compose里定义的服务名。比如数据库服务叫db,那连接地址就是db:5432。这个细节在容器化初期很容易让人卡住。
2.3 本地能跑,不代表容器能跑
这句话是我踩坑踩出来的。本地依赖是直接装在系统Python里的,容器里使用的是全新环境,requirements.txt里漏掉任何一个间接依赖,容器启动就报ModuleNotFoundError。
为了减少这类问题,我建议在项目里维护一份完整且固定的依赖列表。用pip freeze生成虽然简单,但会带上很多无关包,更推荐在requirements.in里写顶层依赖,然后用工具锁定完整版本,或者直接手动维护并定期更新。镜像构建时采用固定版本号,避免某天第三方库发布新版本导致行为变化。
基础镜像的选择也有讲究。我目前统一用python:3.11-slim,它在体积和兼容性之间比较平衡。纯python:3.11镜像太大,alpine版本虽然更小,但基于musl libc,遇到需要编译C扩展的包时,经常要现装编译工具链,反而拖慢构建。如果你的应用依赖Pillow、lxml这类重量级库,slim版本会更省心。
3. Docker 镜像构建与容器编排
3.1 编写优雅的 Dockerfile
Dockerfile写得够不够好,直接决定镜像大小、构建速度和安全程度。我目前的模板长这样:
FROM python:3.11-slim AS builder WORKDIR /build COPY requirements.txt . RUN pip install --upgrade pip && \ pip install --no-cache-dir --prefix=/build/deps -r requirements.txt FROM python:3.11-slim ENV PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 \ TZ=Asia/Shanghai WORKDIR /app COPY --from=builder /build/deps /usr/local COPY src/ /app/src/ COPY manage.py ./ RUN useradd --system --uid 10001 appuser && chown -R appuser:appuser /app USER appuser EXPOSE 8000 CMD ["gunicorn", "config.wsgi:application", "-b", "0.0.0.0:8000", "-w", "2"]这里有几个关键点值得展开说。
多阶段构建是我特别强调的。第一阶段builder负责安装Python依赖,把结果装在/build/deps目录;第二阶段只拷贝依赖和代码,不带编译工具链。这样做的好处很明显:最终镜像体积更小,也更安全。很多初学者直接把pip install放在最终镜像里,如果项目里有需要编译的C扩展,最终镜像就会残留编译器,白白增加体积和攻击面。
依赖缓存顺序也需要注意。COPY requirements.txt和RUN pip install放在前面,后面才复制代码。Docker构建时每一层都会缓存,只要requirements.txt没变,重新构建就不会重复安装依赖;如果先复制全部代码,再安装依赖,那随便改一行代码都会触发依赖重新安装,构建速度会慢很多。
另外两个环境变量值得养成习惯:PYTHONUNBUFFERED=1让Python输出不缓冲,docker compose logs能实时看到日志;PYTHONDONTWRITEBYTECODE=1防止生成__pycache__文件,保持容器文件系统干净。
容器内的用户处理,我从某次安全扫描建议后改成了非root运行。创建appuser用户并切换到它,万一容器被攻破,攻击者得到的也不是root权限。这句USER appuser看起来简单,但很多人一开始都不当回事。
3.2 用 gunicorn 还是 uvicorn 启动应用
启动命令是整个Dockerfile的入口,值得单独说明。Gunicorn和Uvicorn的选择,主要取决于应用框架。
Django和Flask这类传统WSGI应用,默认用Gunicorn就够了。Gunicorn是成熟的进程管理器,worker崩溃会自动拉起,配合docker-compose的restart策略,稳定性有保障。如果项目用了FastAPI、或者Flask/Django里加了异步视图,那就要用Uvicorn来跑ASGI,或者用Gunicorn配合uvicorn.workers.UvicornWorker,既保留Gunicorn的进程管理,又支持异步协议。
worker数量不是越多越好。Gunicorn官方给的参考值是CPU核心数*2+1,但这只是起点,实际要看应用是CPU密集还是IO密集。我见过一个内存吃紧的小服务器,配置了8个worker,结果每个worker占几百MB内存,服务器直接OOM。我的习惯做法:先用nproc查核心数,再给一个保守的worker数量跑起来,观察内存和响应时间,慢慢往上加。起步阶段-w 2通常够用,压力大了再调整。
如果用了python:3.11-slim镜像,注意系统里可能没有curl等调试工具。想要在容器里做健康检查,要么在最终镜像里安装curl,要么直接用Python发起请求,看项目需求自行取舍。
3.3 用 docker-compose 定义服务拓扑
单容器部署只能算第一步,一个完整的Web应用通常还要数据库、对象存储这些配套服务。docker-compose正是用来把这套服务串起来的工具。
我的一份标准docker-compose配置大致如下:
version: "3.8" services: web: build: . restart: unless-stopped env_file: - .env volumes: - ./data:/app/data - ./static:/app/static depends_on: db: condition: service_healthy networks: - appnet expose: - "8000" db: image: postgres:15-alpine restart: unless-stopped env_file: - .env volumes: - dbdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER}"] interval: 5s timeout: 5s retries: 5 networks: - appnet nginx: image: nginx:1.27-alpine restart: unless-stopped depends_on: - web ports: - "80:80" - "443:443" volumes: - ./nginx:/etc/nginx/conf.d:ro - ./static:/srv/static:ro networks: - appnet volumes: dbdata: networks: appnet:注意web服务没有映射宿主机端口,只用expose声明容器内端口。这意味着外界无法直接访问8000端口,只能通过Nginx容器进入。我特别推荐这个设计:应用服务不直接暴露公网,减少攻击面,也让架构更清晰。
depends_on加上condition: service_healthy是我后期改进的。单纯的depends_on只控制启动顺序,不保证数据库已经就绪。如果Web应用启动得比数据库快,数据库连接就会失败。给db服务加上healthcheck后,Web容器会等到数据库健康检查通过再启动,这个坑就彻底避免了。
Nginx容器挂载了配置目录和静态目录,都是只读挂载。配置文件放宿主机的好处是改完配置马上生效,不需要重新构建镜像;静态目录直接挂载进去,Nginx才能高效地返回静态文件。
3.4 构建与启动的实操记录
环境准备就绪后,部署就变成了几条命令的事:
cd /opt/apps/example/app docker compose config # 校验配置语法 docker compose build # 构建镜像 docker compose up -d # 后台启动所有服务 docker compose ps # 查看服务状态第一次构建通常比较慢,因为要下载基础镜像和装依赖。后续构建如果requirements.txt没变,会直接命中缓存,几分钟就能完成。
启动后先验证Nginx能否正常返回页面:
curl -I http://127.0.0.1如果返回502,先别急,按下面的顺序排查:先看容器状态是不是Up,再看Web容器日志有没有报错,最后进容器里直接用curl访问Gunicorn端口:
docker compose exec web curl 127.0.0.1:8000这一步能快速定位问题到底出在应用层还是Nginx层。我见过太多人一看到502就疯狂改Nginx配置,最后发现是Gunicorn压根没起来,白白浪费几小时。
4. Nginx 配置:反向代理、静态文件与 HTTPS
4.1 最简反向代理配置
Nginx配置是整个部署中的关键一环。我在nginx/default.conf里写的最简配置如下:
server { listen 80; server_name app.example.com; client_max_body_size 20m; location / { proxy_pass http://web:8000; 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_http_version 1.1; proxy_set_header Connection ""; proxy_read_timeout 60s; } }proxy_pass http://web:8000;里的web就是docker-compose里的服务名。Docker内置DNS会把服务名自动解析成对应的容器IP,所以Nginx容器和Web容器能在同一网络里通过服务名互相访问。
proxy_set_header Host $host;这行容易被忽略,但它非常重要。Django的ALLOWED_HOSTS校验依赖Host请求头,如果Nginx转发时不带上真实的Host,Django会直接拒绝请求。X-Real-IP和X-Forwarded-For则是为了让应用能拿到用户真实IP,否则日志里看到的所有来源IP都是Nginx容器的地址。
proxy_http_version 1.1和Connection ""这两行是为了支持长连接。默认Nginx转发给后端的HTTP版本是1.0,每次请求都要重新建立连接,性能受影响;改成1.1并清空Connection头,可以让Gunicorn的keep-alive生效,减少连接开销。
client_max_body_size默认只有1m,如果应用有上传功能,不改这个配置就会遇到上传文件直接报413的错误。20m是相对通用的设置,具体按业务调整。
4.2 静态文件与上传文件的托管
让Nginx处理静态文件,是部署Python Web应用时性价比最高的一项优化。Python进程处理静态请求既慢又浪费资源,Nginx返回文件的速度快好几个量级。
我在Nginx配置中通常会加一段:
location /static/ { alias /srv/static/; expires 7d; access_log off; add_header Cache-Control "public, max-age=604800"; } location /media/ { alias /srv/media/; }这里有个高频坑:alias和root的区别。如果写成root /srv/static;,用户访问/static/css/app.css时,Nginx会在/srv/static/static/css/app.css里找文件,多了一层目录,结果就是404。用alias /srv/static/;并保证路径末尾带斜杠,才能把URL里的/static/部分直接替换成/srv/static/。
静态文件的缓存也需要注意。expires 7d给带版本号的静态资源一个7天的缓存时间,能显著减少重复请求。但如果项目发布时没有给静态文件名追加版本号,缓存太激进反而会导致用户看到旧文件,这点要结合前端发布策略来定。
挂载目录的问题我提一下:docker-compose.yml里Nginx容器要把静态目录挂载进去,否则Nginx容器里根本没有这些文件。我在实际操作中不止一次忘记在compose文件里加挂载,结果是Nginx日志一直报404,应用却一切正常,排查了半小时才发现是挂载问题。
4.3 配置 HTTPS 与自动跳转
生产环境不配置HTTPS,相当于把用户的数据明文裸奔在网络上。证书申请我用的是自动续期的方案,申请下来的证书存放在宿主机/opt/apps/example/certs/,然后挂载进Nginx容器。
443端口的SSL server配置:
server { listen 443 ssl; server_name app.example.com; ssl_certificate /etc/nginx/certs/fullchain.pem; ssl_certificate_key /etc/nginx/certs/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; location / { proxy_pass http://web:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; } location /static/ { alias /srv/static/; } }HTTP访问直接跳转到HTTPS:
server { listen 80; server_name app.example.com; return 301 https://$host$request_uri; }ssl_protocols建议至少保留TLSv1.2以上,低版本协议存在已知漏洞,没有理由为了兼容老浏览器而牺牲安全性。证书续期后记得docker compose exec nginx nginx -s reload让新证书生效。
4.4 长连接与上传限制等附加配置
如果应用里有WebSocket或者轮询接口,Nginx需要额外做升级头转发:
location /ws/ { proxy_pass http://web:8000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; }这段配置的目的,是让浏览器与Nginx之间、Nginx与后端之间的连接都保持HTTP/1.1升级协议,WebSocket握手才能通过。如果只是普通HTTP请求,加不加无所谓;一旦涉及到实时通信,缺失这段配置会出现页面能打开、连接却在几十秒后自动断开的现象。
还有一个容易踩的配置坑:如果应用不是挂在域名的根路径,而是某个子路径,比如app.example.com/myapp/,那Nginx的proxy_pass写法会有微妙差异。proxy_pass http://web:8000;会把完整的URL路径原样转发;而proxy_pass http://web:8000/;末尾带斜杠时,会去掉location匹配到的前缀再转发。这个区别需要细心测试,不然很容易得到一堆404或路由错乱。
5. 容器运维:数据持久化、日志与升级
5.1 数据持久化方案
容器本身是无状态的,容器一删,里面的文件就全没了。所以只要是有保存价值的数据,比如数据库内容、用户上传文件、应用生成的临时数据,都必须通过volume挂载到宿主机。
docker-compose里有两种挂载方式,我分别用在了不同地方:
| 场景 | 挂载方式 | 我这么用的原因 |
|---|---|---|
| 数据库数据 | named volume(如dbdata) | 不依赖宿主机目录,方便备份和迁移 |
| 应用产生的文件 | bind mount(如./data:/app/data) | 直接在宿主机上能看到文件,方便排查 |
| 代码和配置 | bind mount(只读) | 修改后不用重新构建镜像,开发调试效率高 |
bind mount有一个权限问题经常让人头疼。容器里通常以非root用户运行,但宿主机目录的属主可能不是那个UID。我在compose里把./data挂载进容器后,经常遇到“Permission denied”。解决方法是把宿主机目录的属主改成容器内运行的用户UID,比如前面Dockerfile里创建的用户是10001,那执行:
sudo chown -R 10001:10001 /opt/apps/example/data就能让应用正常读写数据目录。数据库的named volume则不需要手动权限管理,镜像会自动处理。
5.2 日志收集与轮转
这个坑几乎所有跑过容器的人都会遇到。如果不在compose里配置日志轮转,Docker默认会用json-file驱动无限保存日志,时间一长,某个错误循环刷日志就能把磁盘填满。
我目前在web、db、nginx服务上都配置了统一的日志限制:
logging: driver: json-file options: max-size: "10m" max-file: "3"意思是单个日志文件超过10MB就轮转,最多保留3个文件。这个配置足够应对大部分小型项目,既保证了日志可查,又不会侵占磁盘空间。
日志量确实很大的场景,后续可以考虑接入集中日志系统,但单机部署阶段,docker-compose加日志轮转已经够用。应用本身的日志处理也需要配合:日志库的handler要输出到标准输出,docker compose logs才能看到。我遇到过好几次应用把日志写进了容器内文件,而容器日志里什么都没有,排障时完全无从下手。
5.3 更新发布流程
有了Docker之后,发布流程变得非常标准化。我的日常操作大致是:
cd /opt/apps/example/app git pull origin main docker compose build web docker compose up -d docker image prune -f先拉最新代码,再构建新镜像,然后up -d更新容器。如果项目有数据库迁移脚本,发布前先在命令里执行一遍迁移,再重启Web服务。Django项目的常见命令是:
docker compose run --rm web python manage.py migrate这里强调一下迁移的时机。应该先执行迁移,再发布新版本代码,否则新代码还没上去,数据库已经改成新结构,旧代码和旧库一旦有兼容性问题就会直接把服务打挂。反过来,如果先发布代码再迁移,新代码可能直接访问到旧库结构。顺序这件事,最好在发布脚本里固化下来。
我还会在正式发布前把旧镜像保留一个tag,方便回滚:
docker tag example-web:latest example-web:backup-$(date +%Y%m%d%H%M%S)真出问题时回滚到旧tag,比重新拉代码、重新构建快得多。
6. 常见问题与排查实录
6.1 502 Bad Gateway
502是部署Nginx+Python应用后最常见的报错。Nginx返回502,本质上就是它无法从后端拿到正常响应。常见的根因有这么几类:
| 现象 | 原因 | 处理方式 |
|---|---|---|
connect() failed (111: Connection refused) | 后端容器没起来或端口不对 | 检查Gunicorn启动参数、查看web容器日志 |
upstream timed out | 接口响应时间超出Nginx等待时长 | 调大proxy_read_timeout或优化接口 |
| 容器一直重启 | Gunicorn worker异常退出 | 看启动日志里的异常栈,定位代码或依赖问题 |
我的排查顺序很固定:先docker compose ps看容器状态,再docker compose logs web看应用日志,然后进容器手动curl后端端口确认监听正常。整套流程十分钟内基本能定位。这里想提醒的是,502的根因九成在后端,不要一上来就反复检查Nginx配置文件。
6.2 静态文件 404/403
静态文件相关的问题,部署时出现率也很高。404的情况,我总结下来主要是这几种:
alias和root用混,导致查找路径多了一层目录;- 静态文件没有挂载进Nginx容器,容器里根本找不到目录;
- 项目没有执行
collectstatic,静态文件压根不存在; - Nginx容器内进程对挂载目录没有读取权限,报403。
排查时可以先用这个命令确认Nginx容器里到底能不能看到文件:
docker compose exec nginx ls -l /srv/static/如果目录为空,回到宿主机执行collectstatic再刷新。如果是权限问题,把宿主机目录设为可读或调整属主,一般就能解决。
6.3 容器里连不上数据库
这个坑对第一次容器化的人尤其致命。应用日志里出现could not connect to server: Connection refused或者Name or service not known,基本都是数据库连接配置问题。
最典型的原因:连接配置里还写着localhost。容器里的localhost指的是容器自己,不是宿主机,更不是数据库容器,所以连不上。正确做法是把DB_HOST环境变量设成docker-compose的服务名,比如db。
另一种情况是Web容器启动太快,数据库还没就绪,连接失败后应用直接退出。前面提过,用depends_on加condition: service_healthy配合数据库的healthcheck可以彻底解决。
排查时先确认环境变量是否传对:
docker compose exec web env | grep DB_ docker compose exec db pg_isready看到DB_HOST=db并且pg_isready返回正常,再稍微等几秒让应用重启,问题通常就消失了。
6.4 日志把磁盘写满
有一次我负责的项目部署了一个多月,某天突然收到磁盘告警。上去一看,/var/lib/docker/containers/下面某个容器的json.log文件已经到了几十GB,全是某个接口反复打错误日志撑起来的。
发现后处理两步:先清理现有日志文件,再给所有服务加上轮转限制。清理已有日志时,不要直接用rm删除,日志文件正被容器进程占用着,删掉并不会立即释放空间。应该用truncate把文件内容清空:
sudo truncate -s 0 /var/lib/docker/containers/<container-id>/*-json.log然后按前面的配置给compose的每个服务加上logging限制。此外,在服务器/etc/docker/daemon.json里也可以设置默认的日志轮转参数,但compose里的配置覆盖性更强,也更容易随项目同步。
6.5 重启后服务没有自动拉起
排队重启、服务器重启,结果容器没一起恢复,这种情况一旦遇到就是应用无法访问的线上事故。最常见的原因是compose里的restart策略没配置,或者配置成了no。
检查已有容器的策略:
docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' 容器名如果结果是空或者no,执行命令修改:
docker update --restart unless-stopped 容器名在compose里面,我给每个服务都写了restart: unless-stopped,服务器重启后容器会自动恢复;除非手动执行docker stop,否则容器不会被正常关闭。另外别忘了确认Docker服务本身设了开机自启,sudo systemctl enable docker。
还有一个小细节:如果容器启动后依赖另一个服务,而那个服务还没就绪,restart: unless-stopped会反复重启直到成功。配合健康检查后,这个重试过程是正常的,不要一看日志在报拒绝连接就惊慌。
这套部署流程我前后用过好几个项目,印象最深的不是某个配置细节,而是“可预期”这三个字。以前换一台服务器,光是搭Python环境、装依赖、调系统服务就要折腾一整天;现在镜像还保留着,新机器装好Docker,配置拉过去,一条docker compose up -d就能跑起来。最后分享几个我每天在用的习惯:改Nginx配置前先复制一份备份;新版本上线前先在测试环境跑一遍collectstatic和migrate;docker compose up -d之后别急着离开,等到日志里出现启动成功的标志再休息。部署这种事确实要多踩几次坑才能熟练,但如果一开始就用对工具链,后面的路会平坦很多。