ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python Web项目部署指南:Docker + Nginx 实现快速上线

Python Web项目部署指南:Docker + Nginx 实现快速上线 把本地跑得好好的 Python Web 项目放到服务器上看起来是一件“就差那一步”的事真做起来却能让不少人卡上一整天。我第一次部署 Flask 应用时在本地python app.py一切正常上传到服务器后不到一小时就遇到了 502、404、静态文件加载不出来、数据库连不上最后只能半夜蹲在服务器前面看日志。后来把所有流程沉淀下来改用 Docker 打包应用、用 Nginx 做反向代理才真正实现了“换一台服务器十分钟就能重新上线”。这篇就围绕 Python Web 项目部署到服务器这件事把我实际用 Docker Nginx 的方案、配置细节和踩过的坑完整写出来适合刚接触部署、想把项目从 localhost 搬到公网的开发者参考。1. 部署方案选型为什么是 Docker Nginx1.1 先想清楚裸进程、虚拟环境还是容器很多人上手部署时第一反应是把项目文件传到服务器然后建一个虚拟环境装依赖再用nohup python app.py 方式跑。这套流程在个人练习项目上确实能通但问题会在你换机器、升级依赖、回滚版本时集中爆发。我见过把同一个项目部署到三台服务器每台环境都不一样最后只有一台能正常运行的情况排查起来非常痛苦。用虚拟环境加 systemd 管理进程也不是不行这种方式更适合那种“服务器上只跑一个项目、不频繁变更”的场景。它的缺点在于系统和 Python 环境仍然存在耦合比如服务器升级 Python 版本、安装系统级依赖包都可能让应用失去原有依赖而 Docker 把 Python 版本、第三方库、系统库、代码一起封进镜像相当于把整个运行环境“冻结”下来。Docker 的第二个优势是版本回滚。本地构建一个带标签的镜像比如webapp:v1.0.3线上出问题时只要把容器切回上一个镜像就能恢复整个过程只需一条命令。裸进程部署的时候想回滚就得重新上传代码、重新装依赖、重新重启服务耗时长得让人不愿意回滚。对大多数中小型 Python Web 项目来说Docker 的投入产出比非常清晰。不过也要提醒一点Docker 并不是银弹。它不解决代码本身的质量问题也不解决数据库结构混乱的问题。它解决的是环境一致性问题。选择 Docker 之前确保项目的依赖都已经写在requirements.txt或pyproject.toml里而不是靠服务器上“正好装了某个包”来运行。1.2 Nginx 不是可选项它是整个链路的“门卫”如果 Docker 已经把应用跑起来了比如容器内部 Gunicorn 监听了 8000 端口为什么还要在外面加一层 Nginx直接让用户访问 8000 端口不就行了吗这个问题我被问过很多次答案并不复杂Gunicorn 擅长跑 Python 代码不擅长处理静态文件和应对不可控的网络环境。在实际项目中一个页面的请求往往包含 HTML 文档、CSS、JS、图片、字体。Nginx 处理这些小文件的效率远超 WSGI 服务器它读取磁盘上的静态文件并返回几乎不消耗 Python 进程的计算资源。如果不加 Nginx所有静态文件请求都要先进入 Flask/FastAPI再一层层返回既浪费 CPU响应速度也会明显变慢。Nginx 还有一个容易被忽略的功能请求缓冲与超时控制。公网链路不稳定客户端可能出现慢速上传、断线、数据分批到达等情况。没有 Nginx 在前面缓冲WSGI 服务器很容易被慢连接拖住线程导致正常请求排队等不到资源。Nginx 先把客户端请求接收完再转发给应用等于把网络层的不确定性和应用层隔离了。另外Nginx 可以很自然地做多应用共存。一台服务器不只有一个 Web 项目可能是主站、后台、API 服务同时存在。Nginx 通过不同域名、不同路径前缀把请求分流到不同端口这样 80 和 443 端口可以被多个服务共用而不用为每个服务开一个端口。1.3 一台服务器上完整的工作链路先给整个部署画个结构用户浏览器发出请求先到达服务器上的 NginxNginx 根据域名和路径规则把动态请求通过反向代理转发给 Docker 容器里的 GunicornGunicorn 调用你的 Python 应用逻辑应用需要读写数据库时再连接服务器上的 MySQL/PostgreSQL或者在容器网络里连接数据库容器。这条链路的关键点是反向代理这个词听起来专业理解起来很简单Nginx 是用户的接待员用户不需要知道背后是哪台机器、哪个端口在真正处理业务。用户在浏览器里只输入域名和路径Nginx 自己决定把请求给谁。用容器部署时我习惯把项目代码和 Python 环境放进应用容器把 Nginx 也跑成一个容器用 Docker Compose 把两者编排在一起。这样配置、日志、升级都可以统一管理。下面会详细讲这个方案的准备、配置和实际操作。2. 服务器准备与本地改造上线前需要跨过的三道坎2.1 服务器系统与 Docker 安装部署前先确认服务器环境。我现在的主力服务器是 Ubuntu 22.04腾讯云、阿里云、AWS 的默认镜像基本都能直接装 Docker。如果你买到的服务器是 CentOS操作步骤略有不同但核心逻辑一致不建议为了部署项目再去折腾系统重装先用现有的系统装 Docker 就行。Ubuntu 上安装 Docker 官方推荐的方式是先卸载旧版本再添加官方软件源。完整命令如下sudo apt-get update sudo apt-get install -y ca-certificates curl gnupg sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod ar /etc/apt/keyrings/docker.gpg echo \ deb [arch$(dpkg --print-architecture) signed-by/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(. /etc/os-release echo $VERSION_CODENAME) stable | \ sudo tee /etc/apt/sources.list.d/docker.list /dev/null sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin安装完成后验证版本并设置开机自启sudo systemctl enable docker sudo systemctl start docker sudo docker --version sudo docker compose version服务器会不会因为网络问题下载失败在国内云服务器上确实存在这种情况。如果 Docker Hub 拉取镜像特别慢可以配置镜像加速器。编辑/etc/docker/daemon.json加入 registry 镜像地址然后重启 Docker。需要注意加速器只是帮你更快地拉取公共镜像不影响容器运行本身。这里有一个我踩过的坑云服务器自带的安全组和操作系统防火墙是两个独立层面。Docker 容器端口对外不可见有时不是因为软件配置错了而是安全组没放行。做部署排障时优先检查安全组是否放行了 80、443 端口再检查服务器内部防火墙顺序不要反。2.2 让 Python 项目先跑在 Gunicorn 上本地开发时大家习惯用框架自带的开发服务器Flask 是app.run()FastAPI 是uvicorn app:app --reload。这些服务器在调试环境下很友好但不应该直接用于生产原因很简单性能弱、安全配置少、不适合被 Nginx 长时间转发。真实项目中生产环境通常会选择 Gunicorn它是一个纯 Python 实现的 WSGI 服务器成熟稳定支持多 worker 并发。如果要给项目添加 Gunicorn在requirements.txt中加一行即可flask3.0.0 gunicorn21.2.0如果你的应用是 Flask项目入口文件叫app.py里面定义了app Flask(__name__)那 Gunicorn 的启动命令是gunicorn -w 4 -b 0.0.0.0:8000 app:app-w 4表示开 4 个 worker 进程-b 0.0.0.0:8000表示监听容器内所有网卡的 8000 端口。这里要特别注意不要监听 127.0.0.1因为 Nginx 如果需要访问这个容器走的是 Docker 网络不是容器内部回环地址。FastAPI 项目则建议使用uvicorn配合gunicorn或者直接使用uvicorn运行因为 FastAPI 基于 ASGIGunicorn 原生只支持 WSGI。FastAPI 官方推荐的方式是gunicorn -k uvicorn.workers.UvicornWorker -w 4 app:app需要安装uvicorn[standard]。本地改完依赖后记得先在本地用pip install -r requirements.txt验证一遍把缺失的包补全。这一步能省很多后续构建镜像的时间。2.3 编写一个适合生产的 DockerfileDockerfile 的内容直接关系到镜像体积和构建速度。一个普通的 Flask 项目Dockerfile 可以这样写FROM python:3.11-slim WORKDIR /app ENV PYTHONDONTWRITEBYTECODE1 \ PYTHONUNBUFFERED1 RUN apt-get update \ apt-get install -y --no-install-recommends build-essential \ rm -rf /var/lib/apt/lists/* COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . RUN useradd -m -u 1000 appuser USER appuser EXPOSE 8000 CMD [gunicorn, --bind, 0.0.0.0:8000, --workers, 4, app:app]基础镜像选python:3.11-slim而不是全量python:3.11原因很直接全量镜像包含大量编译工具和文档体积动辄 1GB 以上而 slim 版只有它的三分之一左右对部署在云服务器上的应用来说镜像越小拉取和启动越快暴露的攻击面也越小。build-essential是给某些需要编译的 Python 包准备的比如Pillow、lxml、psycopg2这些包在 slim 镜像里如果没有编译环境会安装失败。如果项目里完全没有这种包可以去掉这一行镜像会更小。用非 root 用户运行容器是一个容易被忽略的安全点。默认情况容器内以 root 身份运行一旦应用被攻破攻击者获得的就是容器内最高权限。创建一个appuser并切换到普通用户可以在一定程度上降低风险。注意我设置了-u 1000这样容器内用户和宿主机普通用户的 UID 一致挂载目录时权限问题会少很多。2.4 环境变量与依赖版本管理的准备上线时最容易出问题的不是逻辑代码而是配置。数据库地址、密码、密钥、第三方 API Key这些信息如果硬编码在代码里会让你上线后非常被动。我现在的习惯是项目根目录放一个.env.example把需要配置的变量名和示例值写清楚真正运行时通过环境变量读取。Flask 项目读取环境变量可以用os.getenv或者使用python-dotenv让本地开发也能自动读取.env文件。容器里运行时通过 Docker Compose 的environment字段传入。这样代码仓库里不会保存真实密码换环境时只需修改配置根本不需要改代码。依赖版本也需要锁定。requirements.txt里的包最好精确到版本号不要只写flask。像flask3.0.0这样固定版本可以避免几个月后因为依赖升级导致的不兼容。如果项目已经很大建议使用pip-tools生成requirements.lock把间接依赖也固定下来。这个习惯能减少大量“上次还能跑这次怎么不行了”的问题。3. 编排与启动用 Docker Compose 把应用和 Web 服务器串起来3.1 Compose 文件怎么设计两个服务跑一条链路用 Docker Compose 管理多个容器是部署 Python Web 的常见做法。一个典型的compose.yaml会把应用和 Nginx 写成两个 service应用容器负责跑 GunicornNginx 容器负责接收外部请求并反向代理。数据库一般根据项目情况选择放在 compose 里一起跑还是用服务器上现成的实例。下面是一份能直接改用的 compose 配置services: web: build: . container_name: myweb_app restart: always environment: - DB_HOSTdb - DB_PORT5432 - SECRET_KEY${SECRET_KEY} - DEBUG0 volumes: - static_data:/app/staticfiles - media_data:/app/media expose: - 8000 depends_on: - db nginx: image: nginx:1.25-alpine container_name: myweb_nginx restart: always ports: - 80:80 - 443:443 volumes: - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro - static_data:/static:ro - media_data:/media:ro depends_on: - web db: image: postgres:15-alpine restart: always environment: - POSTGRES_USER${POSTGRES_USER} - POSTGRES_PASSWORD${POSTGRES_PASSWORD} - POSTGRES_DB${POSTGRES_DB} volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: [CMD-SHELL, pg_isready -U ${POSTGRES_USER}] interval: 5s timeout: 5s retries: 5 volumes: static_data: media_data: pgdata:这份文件里应用容器没有直接对外发布端口只通过expose暴露给 compose 网络内部的 Nginx 容器访问。这样外部流量只能进 Nginx不能直接访问 Gunicorn安全性和可管理性都更好。Nginx 容器把 80、443 端口映射到宿主机再依赖 compose 内部的 DNS 解析访问web:8000。强调一下depends_on的作用它只保证启动顺序不保证服务已经可用。数据库容器启动后往往需要几秒才能接受连接如果应用启动时数据库还没就绪应用会报连接失败。所以数据库容器最好配置healthcheck应用容器里再做一个启动等待逻辑或者让应用多次重试连接数据库。3.2 启动、停止与升级的完整命令习惯所有容器都编排好以后部署操作基本就是几条命令docker compose up -d --build docker compose ps docker compose logs -f web docker compose down docker compose restart nginxup -d --build会先重新构建镜像再在后台启动容器。日常上线新代码时只要把代码同步到服务器然后执行这条命令即可旧容器会被重建。如果你只改了 Nginx 配置不需要重建应用镜像直接docker compose restart nginx就行。注意 restart 和 reload 的区别restart是重启进程会短暂断开连接Nginx 的reload方式可以在不中断服务的情况下加载新配置。容器里可以执行docker compose exec nginx nginx -s reload这比重启更平滑。升级回滚是生产环境非常重要的场景。构建镜像时养成打标签的习惯例如docker build -t myweb_app:20250407 .然后先保持旧镜像不变构建新镜像并启动。如果新版本有问题立即切回旧标签的镜像docker compose down docker compose up -d web提前保留好标签镜像回滚成本就很低。3.3 数据持久化别让你的数据跟着容器一起消失容器本身是临时性的容器删除后内部文件系统也会消失。如果应用里生成了用户上传的图片、静态资源、SQLite 数据库文件这些数据必须放在 volume 或 bind mount 里。Compose 里的static_data、media_data、pgdata就是持久化卷。数据库服务使用 volume 存储数据文件这个很重要。如果直接把数据库文件写在容器可写层一旦容器被删除重建数据就没了。使用命名 volume 后即使容器销毁数据依然保留在宿主机指定位置。对于上传文件的处理我的建议是把上传目录作为独立的media卷Nginx 直接从这个卷读取文件减轻应用压力。代码更新时可以只重新构建应用容器卷里的数据文件不会受影响。注意备份策略卷里的数据同样需要定期备份不要因为 Docker 帮你管理了文件就以为万事大吉。4. Nginx 配置反向代理、静态文件与性能参数4.1 一份开箱即用的 Nginx 配置纯用nginx:1.25-alpine镜像时我会把配置写到宿主机./nginx/nginx.conf然后通过 compose 挂载到容器内/etc/nginx/conf.d/default.conf。这份配置是完整站点配置不是全局nginx.conf。基本内容如下upstream web_backend { server web:8000; } server { listen 80; server_name yourdomain.com; client_max_body_size 50m; gzip on; location /static/ { alias /static/; expires 30d; add_header Cache-Control public, max-age2592000, immutable; } location /media/ { alias /media/; expires 30d; } location / { proxy_pass http://web_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 300s; proxy_send_timeout 300s; } }upstream web_backend { server web:8000; }定义了一个后端组这里web是 compose 里应用容器的服务名Docker 内置 DNS 会自动解析到对应容器 IP。如果不用upstream也可以直接在proxy_pass写http://web:8000。我通常保留upstream好处是以后扩容时可以在这里加多个后端地址Nginx 会自动做负载均衡。listen 80监听宿主机 80 端口外部请求到达后Nginx 根据server_name匹配域名。如果服务器上要跑多个网站就用多个 server 块每个域名一对。location /是默认匹配路径所有不以/static或/media开头的请求都会走这里转发给 Gunicorn。4.2 location 匹配规则与 proxy_set_header 的坑location是 Nginx 里最容易让人困惑的配置但规则其实有限。location /匹配所有路径前缀匹配按最长匹配优先location /static/更具体所以静态文件请求会命中/static/而不会进入反代规则。如果想对某个路径做特殊处理比如/api转发到另一套服务就写location /api { proxy_pass ...; }。proxy_set_header这几行不要省略。Host $host让后端应用认为访问的域名是原始域名如果缺了它Flask 生成的绝对链接、重定向 URL 可能变成容器的内部 IP。X-Real-IP和X-Forwarded-For用于传递客户端真实 IP在很多 Web 应用里用户 IP 用于日志、限流、审批等业务逻辑没有这些请求头应用拿到的全是 Nginx 容器的 IP。应用端还需要配合设置。Flask 里如果使用request.remote_addr在 Nginx 后面拿到的会是 127.0.0.1 或容器网关地址。要拿到真实 IP需要让应用信任 Nginx 设置的X-Forwarded-For头。Flask 通常配合ProxyFix中间件使用FastAPI 也有对应的ProxyHeadersMiddleware部署前记得加上否则做地域统计、IP 黑白名单时会得到完全错误的数据。还有一个很容易忽略的地方当proxy_pass后面不带 URI 路径时和带 URI 路径时 Nginx 的转发规则不一样。上面配置里proxy_pass http://web_backend;不带 URINginx 会把原始请求 URI 原样转发给后端。如果写成proxy_pass http://web_backend/;则会把location匹配的部分替换经常导致路径丢失。项目根路径部署时尽量不带 URI。4.3 静态文件与上传文件的直通处理静态文件交给 Nginx 直出是部署里收益最明显的优化。CSS、JS、图片这类文件通过 Nginx 读磁盘返回速度非常快而如果先进入 Gunicorn 再读取每个静态请求都会占一个 worker。配置里/static/和/media/分别指向容器内挂载的静态目录和上传目录。关于路径alias和root的区别值得记一下。root /static/会把访问/static/x.css映射到/static/static/x.css通常会出问题alias /static/则会把/static/x.css映射到/static/x.css。多数人用alias处理静态目录更符合直觉。expires 30d和Cache-Control的作用是让浏览器缓存静态文件。发布新版本时如果你没有给静态文件加上版本号或 hash 文件名浏览器可能会继续用旧的缓存导致页面样式错乱。推荐做法是引入flask-statics之类的打包工具或者在引用静态资源时带上版本参数如/static/app.css?v20250407。上传文件如果也由 Nginx 处理注意挂载同一个卷。compose 里 Nginx 挂载了media_data:/media:ro应用写的上传文件在media_dataNginx 通过只读方式读取同一个卷用户请求图片时由 Nginx 直接返回不需要经过 Python 代码。这是典型的读写分离应用负责写Nginx 负责读。4.4 超时、体积限制与 HTTPS 升级生产环境还会遇到两类常见请求问题上传大文件和请求耗时长。默认 Nginx 不接受超过 1MB 的请求体如果你的业务允许用户上传文件、头像、Excel 导入必须主动设置client_max_body_size。这个配置既能放在 http、server 层也能放在 location 层。按照业务需求设置合理值不要给得太大。如果设置了 200m 但实际只需要 2m等于给恶意上传打开了方便之门。慢接口也需要单独关注。有些报表、导出接口执行超过 60 秒而 Nginx 默认proxy_read_timeout是 60 秒超时后直接返回 504 Gateway Timeout。我把超时改成 300 秒并配合 Gunicorn 的 worker 超时配置。注意 Gunicorn 默认--timeout是 30 秒同步 worker 下超过 30 秒没响应Gunicorn 会直接杀掉 worker 然后重启导致请求失败。所以要同时调整 Gunicorn 的--timeout参数。添加--timeout 120到 CMD 中即可。HTTPS 是另一个话题。搜索引擎已经明确把 HTTPS 作为排名信号浏览器也会对没有证书的站点给出警告。如果服务器有域名最简单的方式是安装certbot然后执行apt install certbot python3-certbot-nginx certbot --nginx -d yourdomain.comCertbot 会自动修改 Nginx 配置添加证书路径和 443 监听。证书快到期时用certbot renew续期。如果用了 Docker 版 Nginx不太推荐直接在容器里跑 certbot可以在宿主机生成证书再挂载到容器内。不过这个主题展开太多核心还是要记住没有 HTTPS 的站点等于裸奔上线前务必解决。5. Hello World 到生产环境一次完整部署流程实录5.1 从代码上传到容器启动的完整命令序列假设现在项目已经在本地 Git 仓库里服务器已经安装了 Docker 和 Docker Compose。整个部署流程可以拆成以下步骤。第一步把代码同步到服务器。可以用git clone或scp也可以直接在服务器上建一个目录用 rsync 同步。对于没有配置 CI/CD 的项目我个人更推荐gitgit clone https://your-git-server/webapp.git cd webapp确保服务器上的目录里有Dockerfile、compose.yaml、nginx/nginx.conf、.env。.env文件一般不会进入 Git 仓库需要手动上传。第二步准备环境变量文件保证SECRET_KEY、数据库密码等敏感变量已经配置好。然后构建并启动docker compose up -d --build如果构建过程中报错最常见的原因是依赖包安装失败或者 Dockerfile 里的复制路径不对。可以先减去-d用前台模式运行日志会直接打出来。启动后检查容器状态docker compose ps输出里应该有两个容器处于 running 状态web和nginx。如果web显示 Restarting说明容器启动后崩溃可以通过日志排查docker compose logs --tail200 web第三步在服务器本地验证 Nginx 是否正确转发。先直接访问容器内部接口curl http://127.0.0.1:8000/health再通过 Nginx 访问curl http://127.0.0.1/如果第一步能通、第二步不通问题基本在 Nginx 配置或容器网络。如果两步都通说明本地链路正常接下来修改安全组放行 80、443 端口再通过浏览器访问服务器公网 IP 验证。最后确认数据库迁移是否执行。如果项目没有使用数据库迁移工具至少要在应用容器里手动创建表docker compose exec web python manage.py migrate启动顺序我通常是这样先启动数据库等数据库正常再构建启动应用最后启动 Nginx。Compose 里的depends_on虽然不能保证完全就绪但配合 healthcheck 已经能满足绝大多数场景。5.2 常见错误速查表502、504、404、403部署里最常见的四个错误码出现原因和处理方法各不相同。错误码可能原因排查命令解决方法502 Bad GatewayNginx 连不上后端应用容器崩溃或端口不对docker compose ps检查容器状态docker compose logs web查看崩溃原因修复依赖、启动命令确保容器能正常启动504 Gateway Timeout后端处理超时或 Gunicorn worker 无响应docker compose exec web ps aux看 worker 状态看 Gunicorn 日志调大proxy_read_timeout和 Gunicorn--timeout404 Not Found路径不匹配或proxy_pass路径设置错误用curl -v http://127.0.0.1/查看真实转发地址检查 location 和 proxy_pass 的 URI 写法403 Forbidden静态目录权限不足或 IP 被服务器配置阻止docker compose exec nginx ls -l /static查看权限确保目录可读修改 volume 权限502 是最常出现的。它说明 Nginx 已经把请求转发出去但后端没有正常返回响应。常见原因包括应用容器没启动成功、Gunicorn 监听端口与 Nginx 里upstream指向的端口不一致、应用容器在创建后几秒内崩溃。排查时先别急着改 Nginx先看应用容器日志一般报错信息会直接告诉你缺包、配置错误、数据库连不上。404 则要分两层看。如果访问静态文件返回 404优先检查location /static/的 alias 路径和挂载卷是否正常如果访问首页返回 404优先检查 Flask 路由是否真的是/再检查 Nginx 的proxy_pass没有加/导致路径被改写。用curl -I请求一下 Nginx看响应头中的Server是否来自 Nginx能帮我们快速定位问题在哪一层。5.3 数据库连不上的真凶容器网络的地址误区部署 Python Web 项目时“数据库连接失败”几乎是每个新人都遇到过的问题。最常见的原因不是密码错而是数据库地址错误。本地开发时数据库通常就是localhost代码里写的也是localhost或127.0.0.1。到了 Docker 里应用容器是独立网络环境localhost指向容器自己而不是宿主机所以数据库连接会失败。解决方案要分场景。数据库也跑在 compose 里时应用连接地址应该写 compose 里的服务名比如db而不是127.0.0.1。数据库跑在宿主机时应用容器要连接宿主机可以使用host.docker.internal这个特殊域名但注意这个域名在 Linux 上默认不一定可用需要加--add-hosthost.docker.internal:host-gateway参数。在 compose 文件里更稳妥的做法是把数据库连接地址通过环境变量传入不硬编码在代码里environment: - DB_HOSTdb这样应用内部只读DB_HOST环境变量。该值在不同环境下是不同的本地开发是localhost生产容器是db用环境变量天然区分。数据库服务和应用服务之间的网络隔离也需要理解。compose 会为项目自动创建一个 bridge 网络同一个 compose 文件下定义的 service 默认能通过服务名互相访问但如果数据库在用docker run单独启动、没有加入同一个网络即使服务名对也无法 ping 通。这时可以让应用容器加入数据库所在网络或者反过来让数据库容器加入 compose 网络。多了解一点 Docker 网络模型排这种错会快很多。5.4 静态资源缓存不更新的老问题上线后最隐蔽的问题之一是“代码已经更新页面看起来还是旧的”。这种情况常常不是代码部署失败而是静态资源缓存问题。浏览器第一次访问页面时CSS 和 JS 文件会被本地缓存。再次部署新代码后如果静态文件名不变浏览器会继续使用缓存的旧版本于是你看到的是旧样式、旧逻辑。处理方式分几层。第一层是在静态资源 URL 上带版本参数改成/static/main.css?v20250407每次发布时手动更新版本号。第二层是使用带 hash 的文件名这需要构建工具配合但更彻底。第三层是 Nginx 缓存策略对带 hash 的资源可以设置immutable对不确定的资源设置no-cache或较短的max-age。排查这个问题时用浏览器的开发者工具看 Network 面板如果请求返回200 OK (from disk cache)说明用的是缓存如果返回200 OK并有真实服务器响应说明资源已经更新。强制刷新一次判断即可不用着急重启 Nginx。还有一个细节修改了 CSS 或 JS 后浏览器缓存和服务器端 Nginx 缓存都可能生效。有些配置里可能在location /static/加了proxy_cache如果加了还需要主动清理 Nginx 缓存目录。不过大多数中小项目不需要开 Nginx 缓存直接让浏览器缓存就够用了。5.5 日志、重启策略与上线后的运维习惯容器不是启动一次就万事大吉。生产环境里我需要确保容器在中途崩溃后能自动拉起于是 compose 里会设置restart: always。这个策略的意思是只要不是手动docker stop容器异常退出后都会尝试重启。如果同时设置了多个依赖注意不要因为restart导致循环重启的雪崩配合 healthcheck 使用会好很多。日志也需要管。Docker 默认会把容器 stdout 写入 json-file 日志文件如果不加限制一个日志量大的应用可能几天就把磁盘耗尽。compose 文件里建议加上logging: driver: json-file options: max-size: 10m max-file: 3这样单个日志文件达到 10MB 会自动轮转最多保留 3 个文件。上线后观察日志我会用docker compose logs --tail200 -f web这个命令只显示最近 200 行并持续跟踪适合排查当前问题。查找某个关键字时直接 grep 日志文件也可以。最后说一个长久受益的习惯把部署步骤写成文档。你可能觉得自己记得住但两周后再部署一个相似项目一定会忘记depends_on参数、环境变量、Nginx 配置里某个alias路径。把这些步骤沉淀成一份简单的标准操作流程文档包括构建命令、启动命令、回滚命令和常见问题会让运维变得非常轻松。我个人实际操作中的体会是Docker Nginx 这套组合真正解放了我对“环境不一致”的焦虑。以前最怕换服务器现在整个项目打包成一个镜像换服务器只需要在目标机器上装好 Docker把 compose 文件和.env放过去执行一遍构建命令就行。最后再分享一个小技巧新手在一开始学习部署时不要直接拿生产服务器练手先在本地用 Docker Desktop 模拟一套相同的架构跑通 Dockerfile、compose、Nginx 的完整流程再去云服务器上操作。我在第一次部署时就是因为本地和线上行为不一致漏掉了 Nginx 配置文件的挂载权限问题多花了大半天。先在本地复现完整的 Docker Nginx 链路把每个细节都验证清楚到了服务器上就能一次通过。这是把部署能力真正变成自己手艺最重要的一步。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进