Docker+Nginx部署Python Web应用:从开发环境到服务器的完整指南
2026/9/9 17:57:38 网站建设 项目流程

把Python Web应用从开发环境搬到服务器,是很多初学者迈不过去的一道坎。开发的时候用Flask自带服务器跑得飞起,真到了上线这天,什么Python版本不一致、依赖装不上、端口被占用、进程一关服务就挂……各种问题全冒出来了。这篇文章要解决的问题就是:如何用Docker把Python应用打包成标准镜像,配合Nginx做反向代理,稳定地跑在云服务器上。这套方案适用于Flask、FastAPI、Django等主流Python Web框架,也适用于想要规范部署流程、减少环境踩坑的开发者。我会从方案选型讲起,再到环境准备、容器化改造、Nginx配置、完整实操流程和问题排查,覆盖一套可以直接照着做的部署路径。

1. 部署方案总体拆解

1.1 为什么选Docker + Nginx,而不是直接在服务器上裸跑

很多人一开始会想:服务器上直接装Python,然后把代码clone上去,python app.py不就能跑了吗?确实能跑,但只适合临时演示。裸跑会踩这几个痛点:

  • 开发机是Windows或macOS,服务器是Ubuntu或CentOS,Python版本、底层依赖库存在差异,本地能启动的项目,服务器上一跑就报错。
  • 一台服务器上如果跑多个Python应用,依赖之间容易互相冲突。你升了一个库的版本,另一个应用挂了,排查起来非常头疼。
  • 进程管理是个大坑。用nohup python app.py &方式启动,SSH一断开、进程崩了、服务器重启了,服务就没了,还得手动去拉起。
  • 升级回滚困难。代码改了一版,改出问题了,想退回上一版,裸跑环境根本没做版本管理。

Docker把这些痛点一网打尽。镜像就是打包好的运行环境,里面固化了Python版本、依赖库、启动命令,无论推到哪台服务器,跑起来的结果都一样。容器之间相互隔离,多应用共存不冲突。镜像用tag做版本管理,回滚只是换一个镜像标签的事。配合restart: always策略,容器崩了自动拉起,服务器重启也会自动恢复。

那为什么还要加个Nginx?因为Docker解决的是“应用怎么跑”的问题,Nginx解决的是“流量怎么进”的问题。你的Python容器默认监听某个端口,比如8000,如果直接用http://服务器IP:8000访问,会遇到几个麻烦:一是端口暴露得太多,如果一个服务器上跑三个应用,总不能让用户记住三个带端口的地址;二是80/443端口是Web服务的默认入口,让用户敲端口访问非常不专业;三是Nginx能做负载均衡、静态文件加速、HTTPS证书终结这些事,性能和安全层面都比直接把Python服务暴露到公网更靠谱。

所以最终架构是:Nginx在容器里监听80/443端口,接收所有外部请求,再按规则转发到内部的Python应用容器。

1.2 整体架构与请求链路

先看最简单的一台服务器上的部署结构:

浏览器 │ ▼ Nginx 容器(监听 80/443 端口) │ proxy_pass 转发到内网地址 ▼ Python 应用容器(Gunicorn 监听 8000 端口,跑 Flask/Django/FastAPI) │ ▼ 数据库容器(MySQL/PostgreSQL/Redis)

这里有一个关键点:Nginx容器和Python容器应该在同一个Docker网络中,它们之间通过服务名互相访问,不需要把Python的8000端口暴露到宿主机上。这样外部流量只能通过Nginx进入,安全性和规范性都更好。

我用一个生活化类比来解释这套架构:Docker容器就像一个个小房子,应用在房子里跑,有自己的小环境;Nginx是小区门口的保安亭,所有访客先到保安亭登记,保安再告诉访客去几栋几单元。没有保安亭,访客就得直接跑到每个房子门口敲门,既混乱又不安全。

这套架构说起来简单,但真正落地的时候有很多细节要注意:Docker版本怎么装、镜像加速怎么配、Dockerfile怎么写才科学、Nginx反代需要设置哪些请求头、静态文件怎么处理、数据库连接怎么管理等。下面逐个拆解。

2. 部署前的服务器与环境准备

2.1 服务器选型与基础初始化

部署这套方案,服务器的配置不用太高。个人项目、小流量Web应用,2核4G的云服务器完全够用。操作系统建议选Ubuntu 22.04 LTS或Debian 12,这两者在Docker的支持上最省心,社区资料也多。

服务器到手后,先做两个基础动作。第一是更新系统软件包:

sudo apt update && sudo apt upgrade -y

第二是创建非root用户用于日常操作。我一直建议不要直接用root跑业务,万一某个操作出错,影响面太大。创建一个叫deploy的用户并加入sudo组:

sudo useradd -m -s /bin/bash deploy sudo usermod -aG sudo deploy sudo passwd deploy

之后用这个用户登录服务器操作,Docker相关的命令需要加sudo。如果你实在嫌麻烦,直接把当前用户加入docker组,可以免sudo执行docker命令:

sudo usermod -aG docker deploy

改完组要重新登录一次才生效。

2.2 安装Docker与Docker Compose

Docker的安装路径有两条:一是用官方脚本一把梭,二是用apt源安装。我推荐用官方脚本,省事,版本也新:

curl -fsSL https://get.docker.com | bash -s docker

安装完成后,验证一下:

sudo docker version sudo docker compose version

如果你拿到的是旧教程,里面写的是docker-compose(带横杠)命令,那是旧版Compose的语法。现在主流版本是Docker Compose v2,直接用docker compose(空格)调用。如果系统提示找不到compose命令,多半是Docker版本比较老,升级一下即可。

国内服务器有一个极度影响体验的问题:拉取Docker Hub镜像慢到怀疑人生。解决办法是配置镜像加速器。编辑/etc/docker/daemon.json

{ "registry-mirrors": [ "https://docker.m.daocloud.io", "https://dockerproxy.com" ] }

保存后重启Docker:

sudo systemctl restart docker

配置完加速器后,拉镜像的速度会明显提升。这里顺带提一个热搜里很多人踩的坑:在Windows上安装Docker Desktop时提示virtualization support not detected,说明电脑的CPU虚拟化没有开启,需要进BIOS打开Intel VT-x或AMD SVM。这个细节在后续“常见问题”章节会展开说。

2.3 基础网络与端口规划

部署前先想清楚端口怎么规划。通常占用这几个端口:

  • 80:HTTP入口,由Nginx容器占用。
  • 443:HTTPS入口,由Nginx容器占用。
  • 3306、6379:如果MySQL、Redis也在Docker里跑,这些端口通常只对内部网络开放,不需要映射到宿主机。如果为了本地调试方便要映射,建议只绑定到127.0.0.1。怎么理解“只对内开放”?在做端口映射的时候,"3306:3306""127.0.0.1:3306:3306"的区别在于:前者所有网络接口都能访问,相当于把数据库暴露到公网,非常危险;后者只有服务器本机localhost能访问,外部请求到不了。

云服务商控制台的安全组也要放行80、443端口。安全组是云服务器的第一道防火墙,在ECS/CVM控制台的“安全组”规则里添加入方向规则,端口填80/443,来源填写0.0.0.0/0。这一步漏了的话,服务器的防火墙无论怎么配,外面都访问不到。

3. Python项目容器化改造

3.1 代码层面需要做哪些调整

容器化不是把代码扔进Docker就完事了,有些习惯得先改过来。

第一,所有配置必须在代码外部化。什么是外部化?就是DEBUG=TrueSECRET_KEY=xxxxxDATABASE_URL=mysql://...这些值不能硬编码在代码文件里,要从环境变量读取。比如Flask项目里,配置写成:

import os DEBUG = os.getenv("DEBUG", "false").lower() == "true" SECRET_KEY = os.getenv("SECRET_KEY", "please-change-me") DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///app.db")

为什么要这么做?因为同一个镜像可能部署到测试环境和生产环境,代码完全一样,只是环境变量不同。Docker本身就支持在启动容器时注入环境变量,用环境变量管理配置是容器化部署的基本功。

第二,明确Python依赖。项目根目录放一份requirements.txt,尽量锁定大版本号或精确版本号。别偷懒写一堆不带版本号的依赖,这次装和下次装可能依赖版本就漂移了,正好砸中“开发环境能跑、线上环境跑不起来”的老问题。推荐用pip freeze > requirements.txt生成当前环境的依赖列表,或者手动整理核心依赖。

第三,确认Web框架的启动方式。开发时用Flask自带的app.run()或Django的runserver可以,生产环境必须换成异步WSGI服务器,最常用的是Gunicorn。后面会专门讲。

3.2 编写一个科学的Dockerfile

以Flask项目为例,项目的目录结构大概是:

myapp/ ├── app/ │ ├── __init__.py │ └── views.py ├── requirements.txt ├── Dockerfile ├── .dockerignore ├── nginx/ │ └── default.conf └── docker-compose.yml

Dockerfile的内容:

FROM python:3.11-slim WORKDIR /app ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple COPY . . EXPOSE 8000 CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "app:app"]

逐行解释一下关键点:

  • python:3.11-slim:slim版本体积小,包含运行Python所需的最小环境。尽量别用带-alpine的版本,虽然体积更小,但某些依赖库需要编译,容易出问题。
  • PYTHONDONTWRITEBYTECODE=1:不生成__pycache__,减少镜像里的垃圾文件。
  • PYTHONUNBUFFERED=1:日志不缓冲,实时输出到标准输出,方便用docker logs看日志。
  • pip install -i https://pypi.tuna.tsinghua.edu.cn/simple:国内服务器装PyPI依赖用清华镜像,速度差距非常大,不用镜像可能等十几分钟,用了镜像一分钟内装完。
  • CMD里用的是Gunicorn而不是python app.py。Gunicorn是多进程WSGI服务器,能利用多核CPU,并发能力远超开发服务器。--workers 3表示开3个工作进程,一般按“CPU核心数 × 2 + 1”估算。如果服务器是2核,5个worker比较合适,我这里写3是为了保守,看实际内存调整。

另外,.dockerignore文件很容易被忽略,但非常重要。它的作用是告诉Docker哪些文件不要进入镜像构建上下文。最小内容:

__pycache__/ *.pyc .git/ .env venv/

没有.dockerignoreCOPY . .会把你本地的虚拟环境、git目录、日志文件全打进去,镜像又大又慢。

3.3 静态文件与运行用户

Django项目会用到collectstatic收集静态文件,Flask项目如果有独立的前端资源,也一样默认由Python处理。但在生产环境,更好的方案是让Nginx直接处理静态文件,不要经过Python应用。这样Python容器专注于动态请求,静态资源响应速度也更快。

具体操作是:Nginx容器里挂载一份静态文件目录,配置里用aliasroot指向它。后面Nginx配置部分会给出完整写法。那静态文件怎么进到Nginx容器里的?两个思路:一是构建时在Nginx镜像里COPY进去,二是用Docker的数据卷把宿主机目录共享给两个容器。我常用的是后者,在Compose里把宿主机的static/目录同时挂载给Python容器和Nginx容器。

还有一个安全细节:容器内默认是root用户跑应用,如果镜像被打包分发,root权限会有安全风险。可以在Dockerfile里创建非root用户并切换:

RUN addgroup --system app && adduser --system --ingroup app app USER app

注意,如果用了这个配置,容器内写文件(比如Django的media上传目录)所在的数据卷,必须给app用户写权限,否则会报Permission denied

3.4 docker-compose.yml 编排三件套

docker-compose.yml是整套部署的中枢。我个人习惯把Nginx、Python应用、数据库放到同一个Compose文件里管理,用服务名互相访问,主机映射只暴露Nginx的80端口和数据库的本地端口。

一个最简但完整的编排文件:

services: web: build: . restart: always env_file: - .env volumes: - static_volume:/app/static expose: - "8000" depends_on: - db nginx: image: nginx:1.25-alpine restart: always ports: - "80:80" volumes: - ./nginx/default.conf:/etc/nginx/conf.d/default.conf - static_volume:/app/static depends_on: - web db: image: mysql:8.0 restart: always env_file: - .env volumes: - mysql_data:/var/lib/mysql expose: - "3306" volumes: static_volume: mysql_data:

逐个解释这里面的设计意图:

  • web服务用的是build: .,也就是用当前目录的Dockerfile构建镜像。
  • exposeports的区别要分清。expose只在Docker内部网络中暴露端口,宿主机和外部访问不到;ports才会把端口映射到宿主机。web服务的8000端口只需要让Nginx容器通过内部网络访问,所以用expose,不用映射到宿主机。
  • nginx服务把本机的./nginx/default.conf挂载到容器内的Nginx配置目录,改配置不用重新构建镜像,改完docker compose restart nginx就生效。
  • env_file: .env将环境变量从文件加载进容器,数据库密码、SECRET_KEY这类敏感信息都放在.env里,不进仓库。
  • depends_on保证启动顺序。但要注意,它只保证“先启动”,不保证“可用”。MySQL容器启动到真正能接受连接还有几秒到几十秒的初始化时间,Python应用如果启动时立即连数据库,可能连不上。这个问题在后面的“常见问题”章节会提供一个解决方案。

.env文件示例:

SECRET_KEY=your-secret-key DATABASE_URL=mysql://myapp:myapp123@db:3306/myapp MYSQL_ROOT_PASSWORD=root123 MYSQL_DATABASE=myapp MYSQL_USER=myapp MYSQL_PASSWORD=myapp123

有一个地方容易出错:DATABASE_URL里数据库地址写的是db而不是127.0.0.1。因为在Compose网络中,服务名db会被DNS解析到MySQL容器的IP地址。如果你写成127.0.0.1,Python容器访问的是它自己的回环地址,里面并没有MySQL在监听,必然连接失败。

4. Nginx反向代理配置详解

4.1 反向代理到底在做什么

先搞清楚正向代理和反向代理的区别。正向代理是“替客户端访问服务器”,客户端知道代理的存在,访问被限制的资源时找代理帮忙,比如常见的开发调试代理。反向代理是“替服务器接收请求”,客户端不知道代理的存在,它访问的是Nginx,Nginx再转发给后面的应用服务器。

在部署场景里,Nginx做的是反向代理。用户访问http://你的域名,请求到达Nginx,Nginx根据配置把请求转发给内部网络里的Python容器。等Python返回响应,Nginx再转回给用户。这个过程对用户完全透明。

加一层Nginx带来的实际收益有三个:

  • 统一入口。多个Python应用可以共用一个80/443端口,用不同域名或不同路径区分。
  • 安全缓冲。Python应用不需要直接暴露公网端口,减少被扫描和攻击的面。
  • 性能提升。Nginx的静态文件处理能力和并发连接能力远强于Python应用服务器,静态资源交给Nginx能显著降低应用压力。

4.2 一份完整的Nginx站点配置

在项目目录下创建nginx/default.conf

upstream myapp { server web:8000; } server { listen 80; server_name example.com www.example.com; client_max_body_size 20M; location /static/ { alias /app/static/; expires 7d; access_log off; } location / { proxy_pass http://myapp; 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 myapp { server web:8000; }:定义了上游应用服务器池,web是Compose里Python服务的名称。如果以后扩展成多实例,可以在这里加多行server实现负载均衡。
  • location /static/:静态文件请求由Nginx直接处理,alias /app/static/指向Nginx容器内挂载的静态文件目录,注意alias后面必须有/expires 7d给静态资源加7天浏览器缓存,降低服务器压力。加了access_log off避免静态资源请求刷屏日志。
  • location /:其余请求全转发到Python应用。proxy_set_header这几行必不可少,特别是X-Forwarded-For,如果缺了它,Django/Flask拿到的客户端IP全是Nginx容器的IP,日志里的真实访客IP就丢了。
  • client_max_body_size 20M:允许上传的最大请求体大小。默认值只有1M,如果应用有文件上传功能,不调大会直接413错误。

关于proxy_pass http://myappproxy_pass http://web:8000的区别,两种写法都能用。用upstream的好处是后期可以在Nginx层做更灵活的负载均衡策略,比如加权重、加健康检查。简单场景下,直接在proxy_pass里写http://web:8000也行。

4.3 启用HTTPS证书

没有HTTPS,现代浏览器地址栏会提示不安全,而且HTTP明文传输时密码、Cookie都能被截获。部署完成后强烈建议上HTTPS。

最省事的方案是用Certbot自动申请和续期Let‘s Encrypt免费证书。安装Certbot:

sudo apt install certbot python3-certbot-nginx

然后执行:

sudo certbot --nginx -d example.com -d www.example.com

Certbot会自动识别Nginx配置、自动申请证书、自动改写配置文件加入SSL相关设置,还会自动配置HTTP跳转HTTPS。证书90天有效期,Certbot会通过systemd定时任务自动续期,基本不用手动管。

如果你用的是云厂商的免费证书,流程是:去云控制台申请证书 → 下载Nginx版证书文件 → 上传到服务器 → 手动改Nginx配置。手动配置的关键片段:

server { listen 443 ssl; server_name example.com; ssl_certificate /etc/nginx/ssl/example.com.pem; ssl_certificate_key /etc/nginx/ssl/example.com.key; # 其余配置同HTTP版 } server { listen 80; server_name example.com; return 301 https://$host$request_uri; }

注意容器里的Nginx需要把证书文件也挂载进去,在Compose的nginx服务里加一行:

volumes: - /etc/nginx/ssl:/etc/nginx/ssl:ro

5. 完整实操部署流程

5.1 代码上传与目录规划

先把项目代码传到服务器。最推荐的方式是用Git,在服务器上直接clone仓库。如果项目是私有的,需要配置SSH key,或者在服务器上使用带token的clone地址。没有Git仓库的话,用scp从本地传到服务器也行:

scp -r ./myapp deploy@服务器IP:/home/deploy/www/

传到服务器后,目录结构应该是:

/home/deploy/www/myapp/ ├── app/ ├── requirements.txt ├── Dockerfile ├── .dockerignore ├── .env ├── nginx/ │ └── default.conf └── docker-compose.yml

注意:.env文件里有数据库密码和SECRET_KEY,千万不要把这个文件提交到Git仓库。我见过太多把密钥提交进仓库导致被爬虫扫出数据库密码的案例。建议在Git仓库中忽略.env,传到服务器后手动创建。

5.2 构建并启动容器

进入项目目录,先检查配置文件语法有没有问题:

cd /home/deploy/www/myapp docker compose config

这个命令会解析Compose文件并输出最终的解析结果。如果报错,说明YAML格式有问题或者服务定义有误,先修好再往下走。

然后构建并启动:

docker compose up -d --build

-d表示后台运行,--build表示构建镜像后启动。第一次构建会比较慢,因为要拉基础镜像、装依赖。耐心等待构建完成后,查看容器状态:

docker compose ps

如果所有服务的状态都是Up,说明容器已经跑起来了。此时在服务器本地用curl测试一下:

curl -I http://127.0.0.1

如果返回HTTP/1.1 200 OK,说明Nginx已经正常响应。再用完整域名从本地浏览器访问,看页面是否正常。

数据库迁移也需要执行一次。Django项目:

docker compose exec web python manage.py migrate

Flask项目如果有初始化表结构的命令,同理用docker compose exec web进入容器执行。

5.3 查看日志与排错

容器跑起来了不代表一切正常。查看所有服务的日志:

docker compose logs -f

只看某个服务的日志:

docker compose logs -f web

日志是最直接的排错入口。Python应用启动报错了、数据库连接失败了、Nginx转发超时了,都会反映在日志里。日志默认是彩色的,-f参数是持续跟踪新日志输出。生产环境我一般会把日志接入到集中式日志平台,但个人项目直接用docker compose logs就够了。

5.4 更新与发布新版本

代码改了要上线,最常规的操作是:

git pull docker compose up -d --build

流程是:拉取最新代码 → 重新构建镜像 → 优雅替换容器。Gunicorn默认支持优雅重启,正在处理的请求会处理完才结束进程,不会出现请求中断。如果想零停机更新,可以配置deploy.rollback_config或其他蓝绿发布方案,但个人项目用上面的简单方案已经足够稳。

5.5 数据持久化与备份验证

Compose文件里已经为MySQL配置了数据卷mysql_data,数据库数据存放在卷里,容器删了重建数据不会丢。但这不等于万事大吉,卷里的数据如果服务器磁盘坏了同样会丢。建议定期备份MySQL数据:

docker compose exec db mysqldump -u root -p myapp > backup_$(date +%F).sql

可以把这条命令加到crontab里,每天凌晨备份一次,备份文件保留最近7天。恢复的时候:

cat backup_2025-01-01.sql | docker compose exec -T db mysql -u root -p myapp

一定要亲手验证一次备份文件能正常恢复。我见过太多人配置了定时备份,结果因为密码写错、命令路径不对,备份文件全是0字节,真正出事的时候才发现根本没备份成功。

6. 常见问题与排查技巧实录

6.1 502 Bad Gateway

这是Nginx部署中最常见的错误,意思是Nginx无法连接到上游的Python应用。排查思路按顺序走:

  1. 看web容器是否还在运行:docker compose ps,如果显示Exit,说明Python应用启动失败,进web容器日志找原因:docker compose logs web
  2. 确认web容器内部端口是否监听了:docker compose exec web curl -I http://127.0.0.1:8000。如果容器里没有curl,可以用Python代替:docker compose exec web python -c "import urllib.request; print(urllib.request.urlopen('http://127.0.0.1:8000').status)"
  3. 确认Nginx的upstream配置里的服务名与Compose服务名一致。配置里写的是server web:8000,Compose服务名必须是web,如果写错了或者忘了在同一个网络里,就会502。
  4. 确认Nginx配置没有语法问题:docker compose exec nginx nginx -t

多数情况下,问题出在Python应用启动失败导致容器退出,日志里会明确提示是缺依赖、端口被占用还是代码报错。

6.2 同端口冲突:服务器上已经有服务占用80

新部署一个项目,启动Nginx容器时报bind: address already in use,说明宿主机80端口已经被占用了。这种现象很常见,之前用裸机部署过Nginx的、服务器面板自带Web服务的,都会占80端口。

处理办法是先看看谁占了端口:

sudo lsof -i :80

如果是系统自带的Apache或老版本Nginx,停掉并禁用开机启动:

sudo systemctl stop apache2 sudo systemctl disable apache2

如果是另一个Docker容器占用了80端口,需要检查那个容器的端口映射配置,改掉其中一个。

6.3 静态文件全部404

页面能打开但样式全丢,F12看到静态资源返回404。核心原因是静态文件挂载路径和Nginx alias路径对不上。

比如Django项目collectstatic后文件在/app/static/,Nginx配置里location /static/ { alias /app/static/; },如果 /app/static/ 下没有文件,自然404。排查方法:

docker compose exec nginx ls -la /app/static/

如果目录为空,回Python容器执行静态文件收集:

docker compose exec web python manage.py collectstatic --noinput

还有一个常见错误:alias路径结尾的/没写,导致/static/css/style.css请求被映射到/app/staticcss/style.css,路径拼接错误。

6.4 容器总是自动重启又立刻退出

配置了restart: always后,容器启动失败会进入无限重启循环。用docker compose ps能看到Restarting状态。此时要做的不是急着改代码,而是先关掉自动重启,让容器停在该停的地方:

docker compose stop web

然后手动前台启动,直接看报错:

docker compose run --rm web

这种方式会把Python应用的stdout直接打印到终端,所有启动报错、语法错误、ImportError一目了然。

6.5 Python容器启动时连不上MySQL

在Compose的depends_on只能保证容器的启动顺序,不能保证MySQL就绪。如果Python应用启动时立刻执行连库操作,而MySQL还在初始化,就会报Can't connect to MySQL server

我常用的解决方式是在启动命令前加一个等待脚本,用sh -c组合命令实现。Compose里web服务的命令改成:

command: > sh -c " echo 'Waiting for db...' && while ! nc -z db 3306; do sleep 1; done && echo 'db is ready' && gunicorn --bind 0.0.0.0:8000 app:app "

如果Python基础镜像里没有nc命令,可以用Python实现同样的等待逻辑:

command: > sh -c " python -c \"import socket, time; s = socket.socket(); while True: try: s.connect(('db', 3306)); break except Exception: time.sleep(1)\" && gunicorn --bind 0.0.0.0:8000 app:app "

这种方式比直接依赖depends_on可靠得多。不过从设计上考虑,更优雅的姿势是应用在启动时做重试逻辑,比如Django的连接池和Retry机制,但小项目先跑起来更重要。

6.6 Windows上Docker Desktop启动失败

搜索热词里好几个都和这个有关:virtualization support not detected docker desktop failed to start。这个问题本质是Windows的虚拟化功能没开启。确认路径:

  1. 任务管理器 → 性能 → CPU,看“虚拟化”是否显示“已启用”。
  2. 如果显示“已禁用”,重启电脑进BIOS/UEFI设置,找到Intel VT-xAMD SVM,设为Enabled。
  3. 重启后确认Windows功能里Hyper-VWindows 虚拟机监控程序平台是勾选状态。

另外,Docker Desktop在旧版Windows 10上需要WSL2支持。建议直接去Docker官网下载最新版Docker Desktop,安装包会自动处理WSL2的启用流程。装完如果是新装的系统,先重启一次再启动Docker Desktop,成功率会高很多。

6.7 常见问题速查表

现象可能原因快速处理
502 Bad GatewayPython容器挂了或网络不通查看web容器状态和日志
静态资源404alias路径不对或未执行collectstatic检查挂载路径并收集静态文件
80端口被占用其他服务占用host端口停掉占用服务或换端口映射
容器无限重启Python启动即崩溃docker compose run --rm web前台查看报错
数据库连接失败MySQL未就绪或地址写错把URL地址改为服务名db并加等待脚本
单文件上传超过1M报413Nginx默认body限制太小调大client_max_body_size
真实客户端IP丢失缺少X-Forwarded-For头检查proxy_set_header配置

6.8 部署完成后还需要做的小事

容器全部跑通之后,有三件小事容易被忽略但很重要。

第一,在云控制台配置安全组时,只开放必要的端口。SSH端口22可以改成非默认端口,或者限制来源IP;数据库端口3306绝不要对外开放。安全组宁可少开不要多开,不开端口并不影响Docker内部网络的通信。

第二,为Nginx配置基础安全响应头。在Nginx配置的server块里加几行:

add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always;

这些响应头能防点击劫持、MIME嗅探等基础Web攻击。Django项目的话强烈建议把SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")写进配置,配合Nginx的X-Forwarded-Proto头,让Django知道当前请求是通过HTTPS来的,否则Django会一直认为自己处于不安全连接中,可能引发重定向死循环。

第三,确认服务器的时区和系统时间准确。容器日志时间如果差8小时,排查问题时会非常难受。设置时区:

sudo timedatectl set-timezone Asia/Shanghai

容器内的时区如果是UTC,可以在Compose的web服务里加一个环境变量:

environment: - TZ=Asia/Shanghai

7. 几个提高运维效率的小技巧

7.1 用别名精简Docker命令

docker compose命令天天敲,太长了。在~/.bashrc里加几个别名:

alias dc="docker compose" alias dcps="docker compose ps" alias dclog="docker compose logs -f" alias dcbuild="docker compose up -d --build" alias dce="docker compose exec"

保存后source ~/.bashrc生效。之后dclog web就能查看web服务日志,效率高很多。

7.2 容器里改代码即时生效

开发环境联调时,每次改代码都要重建镜像挺痛苦的。如果在Compose的web服务里挂载了源码目录:

volumes: - .:/app

那么修改宿主机代码后,容器内部同步变化。配合Gunicorn的--reload参数,代码改动后自动重启服务。这个方案只建议开发环境用,生产环境千万要关掉,否则代码文件被意外改动会影响线上服务。

7.3 定期清理无用镜像

迭代几轮之后,服务器上会堆满旧的镜像和悬空镜像。一条命令清理:

docker system prune -af

-a删除所有未使用的镜像,-f跳过确认。注意它会删除所有没有被容器使用的镜像,执行前先看一下docker image ls的输出,别误删了要用的镜像。

7.4 不要让Docker容器跑在坏习惯上

有几个坏习惯一定要改:

  • 容器内不要用apt install装一堆东西。容器是临时的,任何手动安装的包在容器重建后都会丢失。正确做法是把安装步骤写进Dockerfile。
  • 不要往容器里传密码。密钥、密码通过环境变量或密钥管理服务传入,不要写进代码或镜像。
  • 不要把数据库数据放在容器可写层。MySQL的/var/lib/mysql必须挂载数据卷,否则容器一删数据全没了。

8. 一次完整的部署实战记录

从零走一遍完整流程,这是我实际部署一个Flask博客应用的记录,按这个流程操作基本不会卡壳。

第一步,本地代码整理。确认项目结构干净,删掉虚拟环境、缓存文件,写出requirements.txt,加上.dockerignore

第二步,准备服务器。Ubuntu 22.04,2核4G。执行系统更新,安装Docker和Compose,配置镜像加速器,创建普通用户并加入docker组。

第三步,上传代码。用Git的方式,服务器上git clone项目仓库。创建一个.env文件,写入数据库密码、SECRET_KEY等环境变量。

第四步,构建启动。执行docker compose up -d --build,观察构建日志确认依赖安装成功。构建完成后docker compose ps确认三个服务都在运行。

第五步,初始化数据库。执行docker compose exec web python manage.py migrate,确认迁移成功。

第六步,配置HTTPS。先确保域名解析到服务器IP,然后执行certbot --nginx -d example.com,按提示完成申请。程序会自动修改Nginx配置并重载。

第七步,全链路验证。浏览器访问域名,确认首页能打开;登录后台,确认数据库读写正常;上传一张图片,确认静态文件处理和Nginx body大小配置正常;查看docker compose logs web,确认没有报错。

整个过程大概20分钟,前两次做可能踩坑花一两个小时,熟悉之后速度会快很多。

在实际操作中,我自己的体会是:部署这件事,80%的问题都出在“环境差异”上,Docker解决的就是这部分问题,但依然有20%的问题出在“配置细节”上,比如网络、路径、权限,这些只能靠经验和日志来积累。所以遇到问题不要慌,先看日志,再按网络通路一层一层排查,绝大多数问题都能定位。如果这篇文章能帮你把第一次部署顺利跑通,那就算没白写。

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

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

立即咨询