资讯动态

Docker+Nginx+Gunicorn部署Python Web应用:从零到生产环境完整指南

发布时间:2026/10/15 20:05:34 来源:尧图企业网站定制
把 Python Web 应用部署到服务器几乎是每个写后端的人绕不开的一步。本地 debug 模式跑得风生水起一旦要挂到公网上各种问题就冒出来了依赖装不上、进程没人管、端口冲突、静态文件 404……我自己第一次部署一个后台管理面板 Demo 的时候就折腾到凌晨两点。后来把所有流程梳理成一套固定套路——Docker 做环境打包Nginx 做入口代理Gunicorn 跑 Python 进程之后每次上线基本半小时搞定。这篇文章就围绕这套组合把从服务器准备、镜像编写、容器编排到 Nginx 配置的完整过程拆开揉碎顺便把我踩过的那些坑全部交代清楚。适合已经把项目本地跑通、正准备把它搬到服务器上的朋友也适合部署过一次但总被各种小问题卡住的读者。1. 为什么要用 Docker Nginx 这套组合1.1 先搞明白一次请求进来以后发生了什么在写配置之前我建议先在脑子里过一遍整个链路。用户浏览器访问你的域名或 IP这个请求先到达服务器上的 NginxNginx 根据配置把请求转发给后端的 Python 应用进程Python 应用处理完以后把响应原路返回。这里有一个关键点Python 框架自带的开发服务器并不能直接扛生产流量。拿最常见的 Flask 举例它自带的app.run()启动的是 Werkzeug 开发服务器这个服务器设计目标就是在本地调试时用的单进程、性能弱、还带着调试模式的安全风险。生产环境里我们一般用 Gunicorn 或者 uWSGI 这类专门的 WSGI 服务器来跑 Python 应用。所以完整链路是 Nginx → Gunicorn → 你的 Flask/FastAPI 应用而不是浏览器直接访问 Python 进程。用 Docker 之后这条链路会变成两个或三个容器一个容器跑 Gunicorn 和你的应用代码一个容器跑 Nginx。两个容器通过一个内部网络互相通信外部请求只经过 Nginx。理解这个结构之后后面所有配置都不会懵。1.2 Docker 解决了环境一致性这个老大难我以前在服务器上部署 Python 项目最头疼的就是环境问题。本地用的是 Python 3.11服务器上是 3.8某个依赖编译不过去或者本地装好了 libpq服务器上没有导致 psycopg2 安装失败。这类问题浪费的时间比写代码还多。Docker 的解决思路很粗暴把 Python 环境、依赖、代码、启动命令全部打包进镜像镜像在哪个机器上跑环境就一模一样。镜像本身是不可变的所以回滚也方便。新代码出问题把容器换成上一个镜像启动就完事不用在服务器上卸载重装各种包。而且 Docker 的容器和宿主机是隔离的你在容器里装什么都不会污染服务器本身的系统环境。对只有一台云服务器、要跑好几个项目的人来说这个隔离性价值很大。1.3 Nginx 为什么要挡在最前面既然 Gunicorn 能直接监听端口对外提供服务为什么前面还要加一层 Nginx我在本地实验的时候也想过这个问题后来在公网环境跑过就明白了。第一是静态文件的处理效率差异非常大。Gunicorn 作为一个 Python 进程处理图片、CSS、JS 这类静态文件的能力远不如 Nginx。Nginx 处理静态文件用的是 C 实现的事件驱动模型几万并发静态请求都不太喘气Python 这边一旦静态请求多了动态接口的响应速度就被拖下来。第二是 Nginx 可以做很多 HTTP 层的事请求体大小限制、超时控制、Gzip 压缩、SSL 证书终结、请求日志记录。这些如果用 Python 代码自己实现要写不少逻辑而且性能还比不上 Nginx 的原生实现。第三是进程管理上的优势。Gunicorn 暴露在公网上意味着任何人都可以直接打你的 Python 进程端口一些恶意扫描、HTTP 头攻击都没有缓冲层。Nginx 在中间可以配置各种过滤规则而且即使 Nginx 后面挂的服务重启了Nginx 本身不会断用户得到的只是一个临时 502恢复后请求又能继续转发体验上好很多。2. 部署前的准备服务器环境与本地项目打磨2.1 服务器的初始设置不管你用哪家云服务商拿到一台新服务器后我建议先做几件基础的事。系统我推荐 Debian 系比如 Ubuntu 22.04 或 Debian 12。不是其他系统不好而是 Debian 系的软件包版本比较新、社区资料多遇到问题搜索一下基本都有答案。拿到服务器先更新系统包sudo apt update sudo apt upgrade -y然后设置时区这个特别容易漏。我遇到过容器里的日志时间比北京时间慢 8 个小时排查半天才发现是系统时区没设置。直接统一设置sudo timedatectl set-timezone Asia/Shanghai新建一个普通用户来操作不要一直用 root。即使你是个人服务器也建议养成这个习惯。然后在服务器上安装基础工具比如curl、git、vim这些看个人习惯。2.2 安装 Docker 与 Compose 插件Docker 的安装方式官方那边有多种途径。我这里给出一套在 Debian/Ubuntu 上比较通用的方法。先安装依赖让 apt 可以通过 HTTPS 拉取软件包sudo apt install -y ca-certificates curl gnupg lsb-release添加 Docker 的官方 GPG 密钥和软件源。注意不同系统的代号不一样下面的/etc/os-release可以自动识别sudo mkdir -p /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg echo \ deb [arch$(dpkg --print-architecture) signed-by/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable | sudo tee /etc/apt/sources.list.d/docker.list /dev/null然后安装sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin安装完以后验证一下sudo docker run hello-world docker compose version如果docker compose version能输出版本号说明 Compose 插件装好了。我给个建议不要单独去装那种 Python 写的旧版docker-compose直接用现在 Docker 自带的docker compose插件命令更统一也不会有语法版本不一致的问题。把当前用户加入 docker 组免得每次都要 sudosudo usermod -aG docker $USER newgrp docker这里提醒一句加入 docker 组实际上相当于给了这个用户 root 级别的权限因为 docker 可以挂载宿主机目录。个人服务器问题不大但心里要有数。2.3 本地项目在部署前要解决的三件事线上部署踩坑很多时候不是服务器环境的问题而是本地项目本身就没准备好。我在部署前一定会过三关。第一关依赖锁定。如果项目里只有一个requirements.txt里面的包名没有锁版本号比如requests2.0那在服务器上构建镜像时可能装到完全不同的版本导致应用行为不一致。更好的做法是用 pip 生成精确的锁文件pip freeze requirements.txt但pip freeze会把一些间接依赖也列进去稍微有点脏。也可以用 pip-tools 这类工具管理写一个requirements.in再编译出requirements.txt这样可控性更高。总之目标是同一份依赖文件在任何机器上装出来的环境都一致。第二关配置不要写死在代码里。数据库地址、密钥、调试开关这些东西一定要从环境变量或外部配置文件读取。因为在 Docker 环境下代码打进镜像后是没法临时改文件的所有可变配置都得通过环境变量传进去。一个简单但可靠的做法是在代码里读os.environ.get()本地开发时加载.env文件线上由 docker compose 注入。第三关本地用 Gunicorn 先试跑一下生产模式。不要等到服务器上才发现应用没法在多进程下正常工作。特别是项目里用了全局对象、内存缓存、文件锁这类东西的多进程模式下经常出问题。本地先跑一遍 Gunicorn 能提前暴露掉一大部分。pip install gunicorn gunicorn --workers 2 --bind 0.0.0.0:8000 wsgi:app这里wsgi:app指你的 WSGI 入口文件是wsgi.py里面定义了一个名为app的 Flask/FastAPI 实例。跑通以后关掉接着进入镜像构建环节。3. 编写 Dockerfile每个细节都将影响线上稳定性3.1 基础镜像怎么选Dockerfile 的第一行是FROM这里的选择直接决定镜像的体积和兼容性。我见过很多新手一上来用python:latest这个标签看着省事但有两个问题版本不固定今天构建和三个月后构建可能拿到不同的 Python 版本而且默认的python镜像是基于 Debian 完整版体积动辄几百 MB传到服务器上也要时间。我的常用选择是固定版本的 slim 镜像比如FROM python:3.11-slimslim版本基于 Debian 精简版体积小不少同时又保留了 Debian 的包管理工具遇到编译依赖可以使用 apt 临时装。为什么不选 alpine因为 alpine 用的是 musl libc和常用的 glibc 有差异一些 Python 依赖本身就是编译好的二进制轮子在 glibc 环境直接能装到了 alpine 要从源码编译经常因为缺头文件报错。能用 alpine 的项目不少但对新手来说slim 是少踩坑的选择。镜像构建过程中如果网络条件不理想记得给 pip 配置国内镜像源构建速度和成功率提升不是一点半点。3.2 依赖安装顺序与构建缓存Dockerfile 里各行命令的执行顺序直接关系到构建效率和失败排查。掌握一个原则把不常变的操作放在前面把经常要改的操作放在最后。建议写成这样COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . .这样设计的原因在于 Docker 构建时会按照指令逐层执行如果某一层没有变化就会直接复用缓存。requirements.txt只有在依赖变更时才发生变化而项目代码几乎每次部署都会变。如果把COPY . .写在pip install前面那么任何代码改动都会导致依赖层缓存失效每次都重新安装所有依赖构建速度会慢很多。另外我习惯在pip install时加上--no-cache-dir这个参数能让镜像体积更小因为 pip 默认会缓存下载的安装包而这些缓存对运行完全没用。3.3 非 root 用户、时区与健康检查默认情况下容器内是用 root 用户运行的这不符合最小权限原则。一旦容器被攻破攻击者拿到的就是 root 权限可以操作宿主机挂载进去的文件。创建专用用户很简单RUN useradd --create-home appuser USER appuser注意顺序必须先复制代码、安装依赖、做完所有需要写文件系统的操作最后再切换用户否则后续操作可能会因为权限问题失败。时区问题在 slim 镜像里也需要处理ENV TZAsia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime echo $TZ /etc/timezoneslim 镜像里默认带不带 tzdata 不一定如果没有先apt-get install -y tzdata再执行上面的命令。这个操作会把日志时间、定时任务时间都纠正过来否则排错时看到的全是 UTC 时间很难对得上真实问题发生的时间点。健康检查很容易被忽略但它在容器编排里非常有用。在 Dockerfile 里可以这样声明基础探活逻辑HEALTHCHECK CMD python -c import urllib.request; urllib.request.urlopen(http://localhost:8000/healthz) || exit 1对应的在应用里加一个/healthz路由返回 200 即可。这样编排系统能自动感知容器状态挂了就自动重启。3.4 一个完整可用的 Dockerfile 模板把上面这些整合起来一份适合大多数 Flask/FastAPI 项目的 Dockerfile 大概是这样的FROM python:3.11-slim ENV PYTHONDONTWRITEBYTECODE1 \ PYTHONUNBUFFERED1 \ TZAsia/Shanghai WORKDIR /app RUN apt-get update apt-get install -y --no-install-recommends \ tzdata \ rm -rf /var/lib/apt/lists/* COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . RUN useradd --create-home appuser USER appuser EXPOSE 8000 CMD [gunicorn, --workers, 4, --threads, 2, --timeout, 60, --bind, 0.0.0.0:8000, wsgi:app]这里启动命令里的参数值得说一下。--workers的取值一般参考 CPU 核数公式是2 * CPU核数 14 核机器上取 4 个 worker 是合理的起点。--threads是为每个 worker 增加并发线程处理能力适合 IO 密集型的 Web 应用。--timeout 60指定 worker 在 60 秒内没有响应就会被主进程杀掉重启防止某个慢请求把整个 worker 卡死。4. 使用 docker compose 编排服务4.1 服务拆分的最小合理单元我见过有人把 Python 应用、Nginx、数据库全部塞进一个容器里这种方案万万不可取。容器本身的理念是一个容器只跑一个进程塞多个服务会导致进程管理混乱而且日志、重启策略都分不清。一个最小合理的拆分是web服务以 Gunicorn 运行 Python 应用nginx服务作为反向代理和静态文件服务器如果项目需要数据库和缓存再额外加db、redis服务以应用加 Nginx 为例写一份 compose 文件version: 3.8 services: web: build: . restart: always expose: - 8000 environment: - DATABASE_URLpostgresqlpsycopg2://user:passdb:5432/appdb - SECRET_KEY${SECRET_KEY} volumes: - static_data:/app/static depends_on: - db nginx: image: nginx:1.25-alpine restart: always ports: - 80:80 - 443:443 volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - static_data:/app/static:ro depends_on: - web db: image: postgres:15-alpine restart: always environment: - POSTGRES_USERappuser - POSTGRES_PASSWORD${POSTGRES_PASSWORD} - POSTGRES_DBappdb volumes: - db_data:/var/lib/postgresql/data volumes: db_data: static_data:有几个设置我特别想提醒。web服务用的是expose而不是ports。这两个词的区别人容易搞混ports是把端口映射到宿主机外部可以直接访问expose只是声明容器对外提供这个端口只有同一个 Docker 网络里的其他容器能访问。我们的设计里外部流量只应该经过 Nginx所以web服务不需要暴露到宿主机只用expose反而更安全。restart: always是让容器在异常退出、服务器重启之后自动拉起这个配置对于线上服务几乎是必备的。没有它服务器一重启你的服务就全挂了还得手动一个个起。static_data:/app/static这个挂载点值得单独拿出来说。如果我们的方案决定让 Nginx 直接服务静态文件那么 Python 容器写出的静态文件需要被 Nginx 容器共享。用同一个命名卷挂载到两个容器就实现了文件共享。但这里有一个大坑如果你把static_data卷挂载到/app/static而镜像构建时/app/static里面本来有文件比如 Django 的collectstatic产物那么卷会覆盖镜像里这个目录的内容容器里看到的可能是个空目录。处理方法是要么在应用启动时先把静态文件同步到卷里比如启动命令里先执行 collectstatic要么不用共享卷让 Nginx 和后端容器共用宿主机的一个目录但这样要多一层 bind mount 权限管理。我常用的是后者host 目录同时挂进两个容器简单直接不用考虑卷初始化的问题services: web: volumes: - ./static:/app/static nginx: volumes: - ./static:/usr/share/nginx/static:ro4.2 环境变量管理与配置隔离compose 文件里的${POSTGRES_PASSWORD}、${SECRET_KEY}这些变量是从哪里来的答案是同目录下的.env文件。docker compose 启动时会自动读取.env文件把变量注入到 compose 配置中。这个机制特别方便因为.env文件不要提交到代码仓库只保留在服务器上。每次部署时服务器上的.env就是一份独立的配置不同环境配置不同、互不干扰。这里强烈建议项目代码仓库里只放一个.env.example作为模板里面写上所有需要的环境变量名和示例值。服务器上复制一份改成.env填入真实配置。这样任何人拿到项目都能快速知道需要配置哪些变量真实密码又不会泄露。4.3 首次启动构建、拉起、看日志把项目代码传到服务器上通常我用 git 拉代码然后进入项目目录执行docker compose up -d --build--build参数表示构建镜像后再启动这样每次部署新代码时这条命令都会重新构建镜像。首次执行会下载基础镜像这个过程时间取决于网络。启动完先别急着访问先看状态docker compose ps所有服务状态都应该是running如果某个服务卡在restarting或者直接exited那大概率是启动失败。这时看日志docker compose logs -f web docker compose logs -f nginx日志输出是定位问题最直接的手段。Python 应用启动时报错、配置文件写错、端口绑定失败这些都能从日志里找到线索。后面专门有一个章节讲常见问题这里先不展开。5. Nginx 反向代理与静态资源的正确配置5.1 什么请求交给 Nginx什么交给 GunicornNginx 的角色可以理解为一个前台接待收到访问请求以后先自己判断这个请求是不是静态文件比如图片、CSS如果是直接在 nginx 自己这边处理根本不用惊动后端的 Python如果不是就把请求转交给后台的 Gunicorn 去处理。这样做的好处前面已经提过静态文件请求在高并发下很消耗资源让 Nginx 用 C 的高性能事件模型处理Python 就能专心干它擅长的事——跑业务逻辑、查数据库、渲染动态页面。如果你的应用主要是 API 服务那基本没有静态文件存在Nginx 就纯粹做反向代理和负载均衡。5.2 配置文件的骨架与关键参数下面是一份适合大多数场景的 Nginx 配置容器里的 Nginx 主配置是/etc/nginx/nginx.conf。为了简单我们直接挂载一份配置到这个路径。我给的配置是已经精简过、能直接用的版本worker_processes auto; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; sendfile on; keepalive_timeout 65; upstream app { server web:8000; } server { listen 80; server_name example.com; client_max_body_size 50m; location /static/ { alias /app/static/; access_log off; expires 7d; } location / { proxy_pass http://app; 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 60s; } } }一个容易被忽略的坑藏在upstream app { server web:8000; }这一行。这里的web不是 IP 地址也不是localhost而是 docker compose 里定义的 web 服务名。Docker 的容器编排网络会自动做 DNS 解析把服务名解析成对应的容器 IP。如果你写成server localhost:8000那 nginx 容器会尝试连接自己容器里的 8000 端口结果必然是 502。这一点新手踩坑率极高我在这里特意强调。proxy_set_header那一组配置也要解释一下。当 Nginx 把请求转发给 Gunicorn 时Python 应用看到的源 IP、请求协议都来自 Nginx 自己而不是真实用户。如果你的应用里需要根据用户 IP 做限流、风控、日志统计就必须依靠这些 header 来还原真实 IP。Host头也很关键如果你的应用用request.host生成绝对链接少了这个头生成出来的链接就是错的。client_max_body_size是另一个经常让人困惑的配置。默认值是 1m也就是说如果用户上传超过 1MB 的文件Nginx 会直接返回 413 错误即使后端的 Gunicorn 完全能处理大文件。需要上传功能的应用一定记得调大这个值。5.3 静态文件路径别搞混了静态文件配置里的alias /app/static/;是指外部 URL 路径/static/对应到容器内的/app/static/目录。这里的路径是 nginx 容器内的路径不是宿主机路径也不是 Python 容器路径。如果 nginx 容器里挂载了项目静态目录到/app/static那这个配置就是对的。alias和root的区别也很关键。location /static/ { alias /app/static/; }的含义是请求/static/css/style.css时直接去/app/static/css/style.css找文件如果用root /app;那 nginx 会去/app/static/css/style.css效果是一样的前提是你知道两者拼接逻辑的差异。用alias更直观但记得路径末尾的斜杠要一致。expires 7d是让浏览器对静态文件做 7 天缓存下次访问直接从本地缓存读取不再请求服务器。这是提高页面加载速度非常有效的低成本优化。5.4 上线后验证配置是否生效配置完成后重启 nginx 容器docker compose restart nginx然后测试几个点curl -I http://服务器IP/ curl -I http://服务器IP/static/css/style.css curl -I http://服务器IP/healthz第一个应该返回 200第二个应该直接由 Nginx 返回 200 且响应头里有缓存相关的头信息第三个应该转发到 Gunicorn 并由应用返回 200。如果这三个都是预期结果恭喜你整个链路通了。到这里还没有完域名解析和防火墙检查也不能省。域名方面在 DNS 服务商那里把记录指向服务器 IP防火墙方面云服务商的安全组和服务器自身的 ufw 都需要放行 80 端口和 443 端口否则外部访问不到 Nginx。后面如果想上 HTTPS在 Nginx 里加一个端口为 443 的 server 块用证书工具申请免费证书并配置自动续期。这一步不是必须的但现在很多平台的接口要求 HTTPS有条件还是尽早配好。6. 常见故障排查实录与速查表6.1 502 Bad Gateway后端没起来或者连错地址502 这个状态码解释起来很简单Nginx 作为代理发现自己连接上游后端失败。你的第一反应应该是起一个终端查看 web 容器的状态和日志docker compose ps docker compose logs web如果看到 Gunicorn 根本没有启动打开日志多半会有错误提示比如 Python 模块导入失败、数据库连不上导致启动时崩溃。我在一个实际项目里遇到过一次 502日志显示address already in use原因是本地起了两个 web 容器抢占同一个端口。这种问题把多余的容器停掉就好了。如果 web 容器看起来一切正常但 nginx 还是 502那几乎可以断定是 Nginx 配置里上游地址写错了。检查一下upstream块里的server web:8000;是否写成了localhost或者端口是否和 Gunicorn 的--bind参数一致。端口不一致是新手最容易遇到的问题Gunicorn 监听 8000nginx 转发到 5000那不通就是理所当然的。6.2 504 Gateway Timeout请求太慢被掐断了504 表示 Nginx 成功连上了后端但后端在规定时间内没有返回响应。两种最典型的原因一是某个接口本身执行时间就长比如导出报表、批量计算超过默认的 60 秒超时二是后端进程已经卡死没有及时处理请求。排查方法先看是只有慢接口触发 504还是所有请求都 504。如果只是个别慢接口那就把 Nginx 的proxy_read_timeout调大同时把 Gunicorn 的--timeout调大两个时间参数要协调。注意 Gunicorn 的 timeout 如果小于接口实际耗时worker 会被主进程杀死重启表现为接口偶尔 500 或连接中断。如果是所有请求都很慢甚至卡死那大概率是后端进程资源耗尽用docker stats看容器 CPU、内存占用同时检查数据库连接数是不是被打满了。6.3 静态文件 404路径拼接与挂载目录不一致静态文件 404 是让我当初最抓狂的问题因为前端页面能打开、接口都正常就是图片和样式全部挂掉。排查思路其实很直接既然 Nginx 返回 404那就是 Nginx 在容器内找不到文件。在 nginx 容器里直接看目录内容docker compose exec nginx ls -la /app/static如果目录为空说明挂载没有生效或者挂到了错误的位置。这时回到 compose 文件里检查挂载路径注意挂载的是宿主机路径还是卷以及目标路径是否和 Nginx 配置里的alias路径完全对应。还有那个卷覆盖镜像目录的坑如果镜像构建时/app/static里本来有文件但启动时被一个空的命名卷挂上了你看到的就是空目录。查一圈下来90% 的静态文件 404 都是路径不匹配导致的。6.4 容器启动即退出靠日志定位最快容器一直处于重启循环docker compose ps里显示restarting这种问题如果光盯着 compose 看是看不出原因的。直接看日志docker compose logs web --tail 50日志里如果是ModuleNotFoundError那就是 requirements.txt 缺依赖如果是PermissionError那就是权限问题检查是否在 Dockerfile 里切换用户后还要对某些目录写权限如果是启动命令不存在检查 CMD 里的可执行文件是否安装了。一个非常隐蔽的坑是我当时在CMD里用了 shell 变量但跑的是 exec 格式。写CMD [gunicorn, --bind, 0.0.0.0:8000, wsgi:app]和写CMD gunicorn --bind 0.0.0.0:8000 wsgi:app在命令解析上不一样前者不会经过 shell环境变量之类的不会自动展开。如果启动命令里需要用到环境变量的值是 shell 格式更方便扩展。但 shell 格式会额外启动一个 shell 进程容器里 PID 1 就不是 gunicorn 了信号处理会受到影响。生产环境建议用 exec 格式。6.5 常见问题速查表现象可能原因排查命令解决思路502 Bad Gateway后端未启动、端口不一致、upstream配错docker compose logs web确认Gunicorn监听端口与nginx上游一致504 Gateway Timeout后端处理超时、配置时间太短查看nginx error.log调大proxy_read_timeout与gunicorn timeout静态文件404alias路径错误、卷覆盖了已有文件docker compose exec nginx ls核对挂载路径与alias路径容器重启循环启动命令崩溃、依赖缺失docker compose logs --tail 50根据日志修复启动参数或依赖端口被占用多个容器或进程占用了同一个端口sudo ss -ltnp停掉冲突进程或修改映射端口日志时间差8小时容器时区未设置date查看容器内时间Dockerfile中设置TZ并安装tzdata这个表格里列的每一个问题基本都是我实际操作中踩过或者帮别人排查过的。排查经验就一句话不要瞎猜先看日志日志会告诉你 90% 的真相。7. 部署还不是终点后续还有这些小事情要处理7.1 镜像体积优化省下的不只是磁盘部署完成以后如果你回头看镜像列表可能会发现一个项目镜像占了五六百 MB。这里面有很多是无用的内容比如 apt 缓存、pip 缓存、构建工具甚至还有测试代码。镜像体积大带来的问题不只是占磁盘还有每次构建后推送到私有仓库和从仓库拉取的时间成本。个人服务器可能感受不明显但项目一多、镜像一大磁盘和带宽的压力会同时出现。一个立竿见影的做法是用多阶段构建。比如有些 Python 依赖需要编译你可以用一个带编译工具的基础镜像安装依赖然后把装好的依赖复制到一个只有运行时的精简镜像里。这样可以丢掉大部分编译工具链。还有一个轻量级的做法在apt-get install后清理缓存加一行rm -rf /var/lib/apt/lists/*在 pip 安装时加--no-cache-dir。这些前面已经提到这里再强调一遍积累到位以后镜像体积能缩小一半以上。7.2 日志处理不要让它无限增长容器如果不配置日志轮转Docker 默认的 json-file 驱动会把所有标准输出落成 JSON 日志文件时间长了磁盘会被打满。尤其是一些打印频率高的应用可能一两天就产生几个 GB 日志。推荐用 Docker 内置的日志轮转services: web: logging: driver: json-file options: max-size: 10m max-file: 3这样每个容器的日志文件到了 10MB 就会自动切割最多保留 3 个文件。Nginx 容器也要做同样的配置因为 Nginx 访问日志在高流量下增长非常快。这是很多线上事故的隐患早点配置早安心。7.3 备份与回滚给自己留一条后路数据库备份是另一个经常被忽略的点。如果你用 bind mount 把 PostgreSQL 数据目录挂到宿主机备份可以直接备份那个目录。更稳妥的方式是使用数据库自己的逻辑备份工具定期生成 SQL 文件然后存到服务器外部存储。回滚方面Docker 给了很好的天然优势每次部署前给当前镜像打一个 tag部署新代码出问题时只需要把 compose 文件里的镜像 tag 指回上一个版本重启容器即可整个回滚不超过一分钟。这个习惯在个人项目里可能不太起眼但在项目服务越来越多的阶段能够省去很多救火的时间。写在最后一次部署完成后的体会整套流程走到这里你已经拥有了一套能自动拉起的 Web 服务Nginx 挡在前面做代理Gunicorn 在后面跑应用容器自动重启日志有轮转回滚有备份。我第一次完整跑通这套方案的时候最大的感受是“终于不用再折腾服务器环境了”。之后新项目上线只需要把同样的 compose 模式复制一份改一改环境变量和域名基本十分钟就能让项目在线运行。我个人最想分享的技巧不是某个具体配置而是部署这件事的思路把每次从零开始的部署流程固定成模板把踩过的坑记成清单下次出问题直接在清单里找答案。服务器环境这个东西越折腾越容易出岔子越稳定越省心。Docker 和 Nginx 的组合本质上就是在帮你把不稳定因子一个个隔离掉、固定住。如果你现在还在手动往服务器上拷贝代码、手动装依赖、手动起进程我建议花一个周末把这套方案搭起来之后每一次上线都会轻松很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑