我在企业里做过不少 Python Web 项目的上线部署从最早直接拿python3 manage.py runserver顶着跑到后来用 systemd、Gunicorn、Nginx 手动搭环境再到最近两年全面切到 Docker Nginx 这套方案。中间踩过的坑加起来能写一本小册子。这篇就结合我实际部署的经验把Python Web 应用容器化 Nginx 反向代理这条路从头到尾捋一遍。这篇文章适合谁看两类人。一类是刚接触云服务器、第一次要把自己写的 Flask / Django / FastAPI 项目从本机能跑变成服务器上稳定跑另一类是已经用 Docker 部署过简单服务、但没搞明白 Nginx 到底在里面扮演什么角色、以及容器间网络该怎么配的开发者。不管你是哪类读完应该都能照着操作把一台干净的服务器变成一个能扛住真实流量的 Python Web 应用节点。1. 为什么是 Docker Nginx这套组合到底解决了什么问题先说结论Docker 解决的是应用环境一致性和进程托管问题Nginx 解决的是外部流量接入和静态资源高效分发问题。两者各管一段配合起来刚刚好。1.1 服务器上部署 Python Web 的三种主流姿势对比先说第一种也是不少人刚开始用的方式——直接在你的云服务器上装 Python 环境、用虚拟环境装依赖、再用 systemd 托管 Gunicorn / uWSGI。这套方案的优点是直观、排查问题简单但缺点很致命——环境隔离差。你在这台服务器上还要跑 Redis、跑数据库、跑其他任务Python 依赖一旦互相冲突处理起来就是一场灾难。而且换一台新服务器环境配置要重来一遍。第二种是直接上 Docker Compose把 Web 应用、数据库、缓存全部容器化。这也是我目前生产环境在用的方式它最核心的价值是可复现性——你本地能跑服务器上就一定能跑因为整个应用运行环境Python 版本、系统依赖、环境变量都封在了镜像里。同时多服务之间的启动顺序、网络连通性也能交给 Compose 编排管理。第三种是把 Kubernetes 也拉进来适合多节点集群、需要自动扩缩容的场景。但说实话大部分 Python Web 项目的流量规模一台 4C8G 的服务器足够了直接上 K8s 反而复杂度爆炸不适合作为团队或个人首个上线方案。至于 Nginx它在整个链路中做得事情其实不像名字看起来那么简单。很多初学者会问Nginx 不是应该处理静态文件吗为什么我动态请求也要走它这里面有一个核心的架构认知Python 的 WSGI 服务器Gunicorn / uWSGI / Uvicorn不应该直接暴露给公网。原因有三点一是它们对恶意请求的防护能力很弱比如慢速连接攻击、超大 Header 请求等二是它们不擅长处理静态资源让 Gunicorn 去返回图片和 CSS纯属浪费 CPU三是 SSL 证书配置、HTTP/2 支持、请求体大小限制这些 Web 服务器层面的能力Nginx 天然就比 Gunicorn 成熟得多。所以标准生产架构是外部请求 → Nginx监听 80/443→ 反向代理到容器内部的 Gunicorn 端口比如 8000。1.2 Docker 部署架构的基本单元拆解我常用的 Python Web 应用 Docker 部署架构可以拆成这几个部分来理解应用容器你写的业务代码配合 Gunicorn / Uvicorn 启动监听容器内的 8000 端口对外不暴露端口到宿主机。数据库容器PostgreSQL / MySQL数据目录通过 volume 持久化存到宿主机磁盘。缓存容器Redis如果 Web 应用有会话、缓存队列需求就用上。Nginx 容器作为整个架构的流量入口监听宿主机的 80 / 443 端口把请求转发到应用容器内部端口。构建上下文前端构建产物若有、Python 依赖、配置文件等通过 Dockerfile 或者 bind mount 传入容器。这里要特别强调一个设计原则不要把数据库和 Web 应用装在同一个容器里。数据库有自己的数据持久化需求、备份机制和故障恢复逻辑跟无状态的 Web 应用容器混在一起不管是容器升级还是数据恢复都会非常被动。1.3 部署流程的全景图我实际用的部署流程大致是这样一条链本地开发、提交代码到 Git 仓库包括 Dockerfile 和 docker-compose.yml。登录到服务器拉取代码。构建镜像并启动容器docker compose up -d --build。Nginx 容器通过 Docker 内部网络解析应用容器名完成反向代理。修改 DNS 解析到服务器 IP访问域名验证。后续升级就是拉代码 重建容器整个过程可以控制在几分钟内。下面我从环境搭建开始一步步往下讲。2. 服务器环境准备与 Docker 安装——最容易忽略的细节很多教程上来就直接写 Dockerfile把环境准备一笔带过。但根据我的实际经验环境准备阶段有几个点没做好后面排查问题会特别头痛。2.1 服务器选型与系统镜像选择部署 Python Web 应用服务器配置不需要太高。一个基础配置是 1 核 2G 内存跑小应用我个人更建议至少 2 核 4G因为 Docker 本身要占一点内存Gunicorn 多 worker 模式 数据库一起跑2G 内存会比较紧凑。系统镜像我目前在用 Ubuntu 22.04 LTS。选择它的原因一是 Docker 官方对 Ubuntu 的支持最完善、兼容性问题最少二是社区资料多遇到问题搜解决方案最容易三是 LTS 版本有长期维护支持不用频繁操心系统升级。CentOS 曾经也是很多人首选但现在维护周期变了新项目我都不太推荐再用了。2.2 用官方脚本安装 Docker 并解决权限问题在 Ubuntu 上安装 Docker 最省事的方式就是用官方安装脚本。这里贴一下我实际的执行过程# 更新系统基础包 sudo apt update sudo apt install -y ca-certificates curl gnupg lsb-release # 安装 Docker 官方脚本 curl -fsSL https://get.docker.com | bash -s docker # 查看安装版本确认安装成功 docker --version # 安装 docker compose 插件 sudo apt install -y docker-compose-plugin # 验证 compose 版本 docker compose version这里有一个细节很多人踩坑执行docker ps的时候会遇到 permission denied这是因为当前用户不在 docker 用户组里。用 root 身份或者加 sudo 当然能跑但对后续自动化部署不友好。正确的做法是把当前用户加入 docker 组sudo usermod -aG docker $USER # 重新登录 shell 或者执行以下命令使组权限生效 newgrp docker还有一点要提醒虽然 Docker 容器本身做了隔离但如果容器配置不当容器里的 root 用户确实可能影响宿主机。所以只在需要的时候给容器加--privileged权限我在生产环境里基本不用这个参数。2.3 Dockerfile 与 Compose 文件目录规划我强烈建议在项目根目录下直接把 Dockerfile 和 docker-compose.yml 建好这样整个部署配置和业务代码放在一起版本管理也是统一的。推荐的目录结构大致如下my_python_project/ ├── app/ │ ├── __init__.py │ ├── main.py # FastAPI 入口文件为例 │ ├── config.py │ └── routers/ ├── requirements.txt ├── Dockerfile ├── docker-compose.yml ├── nginx/ │ └── conf.d/ │ └── default.conf └── .env.example # 环境变量模板这个结构同时兼顾了构建上下文、Nginx 配置挂载和环境变量管理后面你会看到它怎么配合工作。3. 编写一份能直接上生产的 Dockerfile——多阶段构建与依赖缓存技巧Dockerfile 是整个容器化部署的核心它的编写质量直接影响镜像大小、构建速度和运行效率。我见过不少项目把 Dockerfile 写得极其随意——pip install直接全量拷贝代码再装依赖镜像体积直接奔着 1GB 就去了。下面给你看一份我实际在用的 Dockerfile然后拆开讲每个关键决策背后的原因。3.1 生产环境 Dockerfile 的参考模板假设这是一个 FastAPI 项目# 阶段一依赖解析与构建环境 FROM python:3.11-slim as builder WORKDIR /app # 先拷贝依赖文件这一步是为了利用 Docker 缓存层 COPY requirements.txt . RUN pip install --no-cache-dir --prefix/install -r requirements.txt # 阶段二精简运行环境 FROM python:3.11-slim RUN useradd --create-home appuser WORKDIR /app COPY --frombuilder /install /usr/local # 将项目代码拷入容器 COPY ./app ./app # 切换普通用户运行降低安全风险 USER appuser EXPOSE 8000 CMD [gunicorn, app.main:app, -k, uvicorn.workers.UvicornWorker, --bind, 0.0.0.0:8000, --workers, 3]这里有几个关键决策值得解释第一为什么用 python:3.11-slim 而不是 python:3.11slim 版本是基于 Debian 的精简版去了大量不必要的系统包和文档体积能小不少。但它仍然保留了 curl、gnupg 等基础工具用来排查容器问题够用了。python:alpine 更小但 Alpine 用了 musl libc和很多 Python 扩展包的编译环境不兼容比如某些需要 C 扩展的库就得额外装编译工具链反而得不偿失。第二多阶段构建的意义。第一阶段单独做依赖安装生成一个/install目录第二阶段把/install里的内容直接拷贝到运行镜像的系统路径。这样做的好处是构建镜像里编译和缓存产生的临时文件不会留在最终运行镜像里。尤其对于包含 pandas、numpy、psycopg2 这类带 C 扩展的库编译过程会产生大量中间文件多阶段构建可以把它们一次性隔离掉。第三为什么要先拷贝 requirements.txt 再拷贝代码这是 Docker 缓存机制里的一个经典技巧。Docker 在构建每一层时如果对应的指令和依赖的文件内容都没有变化就直接复用之前的缓存。如果你先 COPY 整个项目再装依赖那么只要项目代码有任何一行变动pip install 就会重新执行拉依赖的时间成本非常高特别是网络条件一般的时候。把依赖层放在代码层之前只要 requirements.txt 不变pip install 成功的那一层缓存就能一直复用代码迭代的构建速度会大幅提升。3.2 依赖安装的加速方式与镜像源选择在国内服务器上装 Python 依赖默认走官方 PyPI 源会慢到你怀疑人生几十个依赖装个十分钟很正常。实际生产环境中我一般会在 Dockerfile 里设置国内镜像源RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple这样在构建阶段就会自动走镜像源拉取。要注意的是如果基础镜像里的pip config粒度不够也可以直接在命令里指定RUN pip install --no-cache-dir --index-url https://pypi.tuna.tsinghua.edu.cn/simple --prefix/install -r requirements.txt3.3 关于健康检查的一个补充技巧Docker 自带 HEALTHCHECK 指令可以让 Docker 周期性地检查容器是否还处于健康状态方便后面编排和排查问题。对 FastAPI 这类项目可以加这样一个探针假设应用里已经实现了/healthz接口HEALTHCHECK --interval30s --timeout5s --retries3 \ CMD python -c import urllib.request; urllib.request.urlopen(http://localhost:8000/healthz) || exit 1不过这个探针是发给应用的对网络层不敏感。真正要帮 Nginx 或者上游负载均衡做判断的是下面要讲的 Compose 里的 healthcheck 配置。Dockerfile 里的 HEALTHCHECK 和 Compose 里的 healthcheck两者作用域不一样前者用于docker inspect展示容器状态后者用于编排器判断依赖服务是否可用。4. docker-compose.yml 编排细节——端口、网络、数据卷、环境变量的全链路设计单容器部署没太大意思真实项目几乎都是应用 数据库 缓存的联动组合。这时候 docker-compose.yml 就派上大用场了。4.1 一个能直接落地的编排文件参考下面是我常用的编排文件模板以 FastAPI PostgreSQL Redis Nginx 的组合为例version: 3.8 services: app: build: context: . dockerfile: Dockerfile container_name: my_app_app restart: unless-stopped expose: - 8000 environment: - DATABASE_URLpostgresqlpsycopg2://myuser:mypassworddb:5432/mydb - REDIS_URLredis://redis:6379/0 - SECRET_KEY${SECRET_KEY} depends_on: db: condition: service_healthy redis: condition: service_healthy networks: - backend db: image: postgres:15-alpine container_name: my_app_db restart: unless-stopped environment: - POSTGRES_USERmyuser - POSTGRES_PASSWORD${POSTGRES_PASSWORD} - POSTGRES_DBmydb volumes: - db_data:/var/lib/postgresql/data healthcheck: test: [CMD-SHELL, pg_isready -U myuser -d mydb] interval: 10s timeout: 5s retries: 5 networks: - backend redis: image: redis:7-alpine container_name: my_app_redis restart: unless-stopped command: [redis-server, --appendonly, yes] volumes: - redis_data:/data healthcheck: test: [CMD, redis-cli, ping] interval: 10s timeout: 5s retries: 5 networks: - backend nginx: image: nginx:1.25-alpine container_name: my_app_nginx restart: unless-stopped ports: - 80:80 - 443:443 volumes: - ./nginx/conf.d:/etc/nginx/conf.d:ro - ./static:/static:ro - ./media:/media:ro - certbot_webroot:/var/www/certbot - certbot_certs:/etc/letsencrypt depends_on: - app networks: - backend volumes: db_data: redis_data: certbot_webroot: certbot_certs: networks: backend: driver: bridge这个文件有几个容易忽略但特别关键的点我逐个说下。4.2 expose 和 ports 的区别为什么 Web 应用容器不暴露端口新手经常搞混expose和ports。expose只是声明容器内部有哪些端口可供其他容器访问并不会把端口映射到宿主机外部ports才会把容器端口映射到宿主机端口外部网络可以访问到。在我这个编排里app 容器只用了 expose 8000没有 ports。这意味着外界包括宿主机无法直接访问应用端口只有同一个 Docker 网络内的容器比如 Nginx才能访问到它。这是一层额外的安全隔离——你的 Gunicorn 接口不该暴露在公网上它只应该被 Nginx 转发到。depends_on里加了condition: service_healthy它保证了 Web 应用容器启动时数据库和 Redis 都已经处于健康状态。因为 Python 应用启动时一般会尝试连接数据库、初始化连接池如果数据库还没就绪容器很可能启动失败然后进入重启循环场面很难看。4.3 环境变量管理.env 文件与敏感信息处理编排文件里的${POSTGRES_PASSWORD}这种写法是从当前目录下的.env文件读取值的。.env文件不要提交到 Git 仓库里面放的是跟机器相关的敏感配置比如数据库密码、JWT 密钥、云服务密钥。提交到仓库的应该是一个.env.example内容用占位符方便其他人照着写自己的.env。我把这个做得更严一点.env文件权限设置为600只有文件所有者可读写避免同一个服务器上其他用户把它读走。4.4 数据卷的持久化与备份思路db_data和redis_data这两个命名卷是把容器内的数据目录映射到 Docker 管理的宿主机目录。这样即使容器挂了、重建了数据还在。PostgreSQL 和 Redis 都是自己的存储格式数据必须放在卷里容器本身千万不要存储任何重要数据。这里要提一个生产备份思路直接把宿主机的数据库数据目录打个包并不合适因为数据库运行时会写缓存和 WAL 日志裸文件拷贝容易产生不一致。正确方式是用pg_dump定期导出 SQL然后备份导出文件。我在服务器上放了个 2 行的 cron 任务0 3 * * * docker exec my_app_db pg_dump -U myuser mydb | gzip /backup/mydb_$(date \%F).sql.gz 0 4 * * * find /backup -name *.sql.gz -mtime 7 -delete每日凌晨 3 点导出数据库并保留 7 天版本这就够了。对大多数中小项目来说这种级别的备份已经完全满足恢复需求。4.5 Compose 网络原理解析默认情况下Compose 会为当前项目创建一个名为项目名_default的 bridge 网络。服务之间可以直接用服务名比如app、db、nginx作为主机名互相访问因为 Compose 已经替我们做了 DNS 解析。所以在应用代码里配置数据库地址时写的是db:5432而不是localhost:5432这个db就是 Postgres 在 compose 网络里的服务名。同一个网络里的容器互相访问走的是容器 IP跟宿主机 IP 无关。这种机制好就好在——你本地跑 compose 也能用db:5432访问数据库服务器上跑一模一样配置不需要改。5. Nginx 反向代理配置详解——从端口转发到静态资源托管Nginx 是整个架构中面向用户的最后一公里。它的配置不仅要实现反向代理还要兼顾性能和安全。5.1 核心反向代理配置模板还是以 FastAPI 项目为例nginx/conf.d/default.conf的参考配置如下upstream backend_app { server app:8000; keepalive 32; } server { listen 80; server_name example.com; # 静态文件直接由 Nginx 处理不走 Python location /static/ { alias /static/; expires 7d; add_header Cache-Control public, immutable; } location /media/ { alias /media/; expires 30d; add_header Cache-Control public; } # 动态请求全部转发到 Gunicorn location / { proxy_pass http://backend_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; # 显式设置 HTTP 版本为 1.1否则 proxy 的 keepalive 不生效 proxy_http_version 1.1; proxy_set_header Connection ; } }5.2 为什么上游要写backend_app而不是localhost:8000upstream里我写的是server app:8000;这里的app是 docker-compose.yml 里服务名。Nginx 容器和 Web 应用容器在同一个 Docker bridge 网络里Nginx 启动时会对app这个主机名做 DNS 解析解析到 Web 应用容器的内网 IP。也有人写成server http://localhost:8000这在 Docker 网络里是走不通的因为 Nginx 容器里的 localhost 是 Nginx 容器自己不是应用容器。这是新手最容易踩的坑之一。5.3 静态文件到底应该放哪这里有个架构细节要讲清楚Python Web 框架的 Static 文件处理能力非常弱Gunicorn 也不应该处理静态文件。所以生产环境的标准做法是静态文件CSS、JS、图片由 Nginx 直接提供请求根本不会进到 Python 应用里。实现方式是在 docker-compose.yml 里把宿主机的./static目录以只读方式挂载到 Nginx 容器内部/static同时也在构建镜像时把./static拷进 Web 应用容器用于本地调试或某些需要调用静态文件的场景。在 Nginx 配置里用alias /static/把路由前缀映射过去。如果你用 Django 的collectstatic管理静态文件建议在 CI/CD 流程里把收集好的staticfiles目录挂载给 Nginx。FastAPI / Flask 项目相对简单直接把静态资源目录丢过去就行。5.4 反向代理的 header 传递为什么重要proxy_set_header这些配置不是可有可无的。这里逐个解释Host $host把外部请求的 Host 头传给后端应用。如果省略后端看到的 Host 就是 Nginx 容器自身的 IP一些基于域名做逻辑判断的功能如站点 URL 生成、SEO 分站就会出错。X-Real-IP/X-Forwarded-For把用户真实 IP 传给后端。你的 Python 应用在记录访问日志、做 IP 限流、地理位置分析时取的就是这个头。如果不设置后端看所有请求都来自同一个 IP——就是 Nginx 容器的内网 IP那各家业务的 IP 相关功能就全部废掉了。X-Forwarded-Proto告诉后端用户是通过 HTTP 还是 HTTPS 访问。有些 Web 框架在生成重定向 URL 时依赖这个字段判断协议不设置的话即使外部是 HTTPS后端生成的链接也可能变成 HTTP。如果你的框架层面要读取真实 IP还需要在应用里配置信任反向代理。以 Gunicorn 为例它的--forwarded-allow-ips参数需要设置成信任来自 Docker 内网的转发请求否则框架会默认忽略这些头部以作安全防护。5.5 关于 SSL 证书从 HTTP 到 HTTPS 的平滑切换现在部署新站点没上 HTTPS 已经很不像话了。有两种主流的证书记置方式用 Certbot 在宿主机上直接申请证书然后把证书文件挂载给 Nginx 容器。用 certbot 容器配合 Nginx 做自动续期把证书目录挂到/etc/letsencrypt。我建议用第二种certbot容器 certbot_certs卷 Nginx 的定时续期。它能把证书的申请和续期完全自动化。首次申请可以通过一个 HTTP-01 的 challenge 完成Nginx 在/var/www/certbot目录下提供验证文件。证书签下来后再新增一个监听 443 的 server 块把 HTTP 请求 301 重定向到 HTTPS。HTTPS server 块的配置只需要在 5.1 节的 server 块基础上增加证书路径和 80 端口的重定向即可。等以后你要给站点上 HTTPS直接加上证书路径就行重新加载一下 Nginx 配置nginx -s reload就能生效。6. 把服务跑起来首次启动、日志排查与常见故障定位写完了 Dockerfile、Compose 文件和 Nginx 配置终于到了跑服务的环节。我对第一步启动的建议和很多教程不同——不要直接后台启动先前台跑一遍把所有输出看一遍再说。6.1 首次启动的前台调试与后台切换在你确认配置没有大问题时执行docker compose up这个命令会前台启动所有服务把日志实时刷到终端上。绝大多数配置问题都能从日志里看出来。如果一切正常能看到类似这样的输出app_1 | [INFO] Starting gunicorn 23.0.0 app_1 | [INFO] Listening at: http://0.0.0.0:8000 db_1 | PostgreSQL init process complete; ready for server start. nginx_1 | [warn] the listen ... http2 directive is deprecated确认没问题后CtrlC 停掉再用docker compose up -d把服务放后台跑。这里的-d是 detach 的意思让服务在后台运行。6.2 三个最常遇到的启动故障与排查路径故障一数据库连接失败。典型报错是应用容器启动几秒后退出日志里写着connection refused或者database xxx does not exist。排查链路是先docker compose ps看 db 容器是否已经 healthy再docker compose logs db确认数据库是否初始化完成最后确认应用代码里的DATABASE_URL用的是服务名db:5432而不是localhost:5432。故障二Nginx 502 Bad Gateway。这个一般出现在整体能启动但访问时。502 意味着 Nginx 没法连到上游应用。可能原因应用容器启动失败docker compose ps看到退出状态、应用监听端口跟 Nginx 上游配置不一致应用在 8000Nginx 转发到 8001、或者网络没连通。先docker compose logs nginx看错误日志里面会明确写connect() failed (111: Connection refused)这类信息。故障三静态文件 404。启动没问题、接口也通但页面图片和样式全都加载不出来。先确认 Nginx 容器里/static目录是否有内容可以docker exec my_app_nginx ls -l /static再检查 Nginx 配置里的location /static/是否和 docker-compose 挂载路径对得上。注意alias /static/和root /static的效果完全不同root会把前缀一起拼到路径后面导致路径变成/static/static/xxx这个细节写错就是 404。6.3 日常运维日志查看、容器重启与服务自愈服务跑起来后日志就是你的眼睛。快速查看实时日志的命令要记熟# 查看所有服务日志 docker compose logs -f # 只看某个服务的日志并限制输出行数 docker compose logs -f app --tail500 # 进入容器内部排查 docker exec -it my_app_app bash我的实践是所有日志都打到容器 stdout/stdout这样 Docker 会自动采集再结合一些轻量级的日志轮转配置比如使用json-file驱动并设置max-size: 50m可以避免日志文件无限膨胀logging: driver: json-file options: max-size: 50m max-file: 3restart: unless-stopped这个策略保证服务由于异常退出时Docker 会自动拉起新容器。但它不解决进程跑着但服务不可用的假死问题所以healthcheck的意义就在于主动探测应用的真实可用性而不只是看进程是否存活。7. 镜像构建与版本管理的进阶操作服务能稳定运行之后就会碰到怎么优雅地更新版本这个问题。直接把新代码推到服务器上重建容器当然可行但有一些细节能让你做得更顺。7.1 基于 Git 提交哈希打 tag实现快速回滚我个人比较认可的方式是每次发布都基于 Git 提交有点长直接用短哈希作为镜像 tagdocker build -t my_app:$(git rev-parse --short HEAD) . # 更新 compose 文件里的镜像版本 docker compose up -d这样每次部署的镜像跟代码提交完全对应一旦线上出问题只需要把镜像 tag 切回上一个可行的提交哈希重新up -d就能实现秒级回滚。除非你的数据库结构已经变更否则回滚到旧代码往往比现场改 bug 更稳也更符合现代发布理念。7.2 用 Registry 配合部署脚本实现一键发布如果只有你自己在维护直接在服务器上构建和运行也完全可行。但当你有多台服务器、或者开始有 CI/CD 的需求后把镜像推到私有 Registry或者云服务商的镜像仓库更合适。一个可以跑通的流程是# 在本地或者构建机上 docker build -t my_registry.example.com/my_app:1.2.3 . docker push my_registry.example.com/my_app:1.2.3 # 在服务器上 docker compose pull app docker compose up -d app配合 CI 平台比如在代码仓库提交标签后自动触发构建和推送整个流程可以做到完全无人值守。不过这些属于进阶玩法第一次部署不需要太焦虑先把单个服务器上的 Docker Compose 跑熟练CI/CD 后面再引入也不迟。7.3 定时清理无用镜像和构建缓存服务器磁盘被占满也是我实际遇到过的生产事故。Docker 在反复构建和更新后会积累很多dangling镜像和构建缓存。可以定期执行以下命令docker system prune -f docker builder prune -f我一般每周执行一次自动清理防止因磁盘满导致服务无响应。8. 迁移到生产之前配置清单与安全加固自查最后这段不是教你写代码而是一份我在项目上线前必过的自查清单。很多东西到了线上才发现再改成本会成倍增加。8.1 上线前的检查清单应用配置项是否全部通过环境变量注入有没有在代码里硬编码数据库地址、密码、密钥.env文件是否已经加入.gitignore密钥是否用足够强度的随机串生成数据库、Redis 端口是否暴露到宿主机如果只给 Web 应用用不需要暴露任何端口。Nginx 配置是否限制了请求体大小client_max_body_size防止用户上传大文件时把后端压垮。是否配置了基础安全相关响应头如X-Content-Type-Options、X-Frame-Options、Referrer-Policy8.2 容器安全的两个细节永远不要在容器内用 root 运行你的应用。在 Dockerfile 里通过useradd创建专门用户如样例里的appuser并在最后USER appuser切换。一旦容器被攻破攻击者至少不是在 root 权限下任意操作。给 Compose 服务通过networks做前后端隔离。如果你的架构里有 Redis 或者内部管理接口可以考虑拆分成多个网络层只有 Nginx 所在的入口层能对外暴露端口数据库、缓存的内部服务完全走封闭的 backend 网络不与公网有任何交集。8.3 最后的操作体会把我这几年的经验浓缩成一句给你的实在话部署链路其实就三段——构建镜像时保证环境的确定性编排运行时控制服务之间的依赖和网络入口层用 Nginx 把用户流量安全地接到应用上。每一段都有自己该踩的坑但只要你按这个架构思路一步步搭它基本能覆盖你未来一两年内绝大多数的发布需求。第一次跑通后你会明显感觉到再往 Web 应用里加新功能、发新版本都变成了一件很顺手的事情因为基础设施已经稳定剩下的就是业务逻辑本身的迭代了。