最近有个朋友找我帮忙看一个Django项目,说在Windows上怎么都跑不起来。我一开始也没太当回事,让他直接装Python、装依赖、起服务,结果聊下去才发现事情没这么简单——项目依赖MySQL、Redis,还挂了个Celery异步任务,每个人的Python版本、系统变量、库版本全都不一样,光是把环境折腾齐就要半天,更别提部署到服务器以后能不能复现了。
后来我花了点时间把这套流程用Docker整个重写了一遍,一次性解决了“在我机器上明明是好的”这个经典问题。这篇文章就是我这次在Windows下用Docker部署Django项目的完整记录,从环境准备、Dockerfile编写、docker-compose编排一路写到踩坑排查,适合刚接触容器化部署的Django开发者参考,也适合那些已经能用python manage.py runserver跑通、但对生产部署还比较模糊的朋友。
1. 为什么要在Windows上用Docker部署Django
1.1 环境不一致带来的痛点
你大概率遇到过这种情况:本地用Python 3.10开发得好好的,测试环境用的是3.8,某个依赖库的小版本不一致,结果线上跑出来就是两种行为。更难受的是不同操作系统之间的差异——Windows下的路径分隔符、环境变量写法、数据库驱动编译方式,和Linux服务器完全不同。
Docker解决的就是“环境一致性”这个问题。它把Python版本、系统依赖、项目代码、启动命令全部打包进一个镜像,不管你在Windows、macOS还是Linux上,只要装了Docker,跑出来的容器环境就是一样的。这样带来的直接好处是:本地能跑的,服务器上基本也能跑;一个人能跑的,团队里所有人都能跑。
还有一个很容易被忽略的点:Docker让“销毁重建”变得几乎没有成本。以前改坏环境只能重装系统或者手动一个个卸依赖,现在直接docker-compose down -v再重新起一套就行,整个过程不会超过几分钟。
1.2 Docker部署的核心思路
部署Django项目,本质上要做的事情有几件:跑Web服务、连接数据库、处理静态文件、配置环境变量。Docker化之后,我们需要把这些能力分散到不同容器里协同工作。
最常用的组合是:
web容器:运行Gunicorn或uWSGI,加载Django应用db容器:运行MySQL或PostgreSQLnginx容器:做反向代理和静态文件服务(可选,后期加)
这三个服务通过docker-compose.yml关联在一起。Django容器通过环境变量拿到数据库地址,通过卷共享代码和静态文件,通过网络互相通信。
在Windows下之所以要单独写一篇文章,是因为Windows本身的容器支持和文件系统机制跟Linux有差异,很多坑是Windows平台特有的,比如WSL2的虚拟化配置、文件挂载性能问题、端口映射冲突,这些不实际踩一遍很难讲清楚。
1.3 适合谁来参考
如果你是下面这几类人,这篇文章对你会有直接的帮助:
- 在Windows上做Django开发,想用Docker统一开发环境
- 开发完项目要部署到Linux服务器,想提前验证Docker部署流程
- 带团队协同开发,想消灭“环境不一致导致的问题”
- 数据库、Redis等服务都用Docker跑,想把Django也容器化
不需要你有很深的Docker基础,只需要知道docker和docker-compose这两个基础命令就够用了。
2. 环境准备:Windows下的Docker底座搭建
2.1 Docker Desktop安装与系统要求
Windows下运行Docker的官方路径是安装Docker Desktop。它在2022年以后彻底转向了WSL2后端,性能和体验比老版本的Hyper-V方案好了不少。
安装前先确认你的系统版本。Docker Desktop要求Windows 10 64位(版本2004或更高)或Windows 11,而且必须要开启虚拟化支持。很多人在安装完成后启动报错,基本都是卡在虚拟化检测这一步。
去Docker官网下载Docker Desktop Installer.exe,双击安装即可。安装过程中会让你选择使用WSL 2还是Hyper-V,强烈建议选WSL 2,原因后面会讲到。装完以后重启一次系统,然后打开Docker Desktop,它会自动初始化WSL 2的发行版。
注意:如果你的电脑配置比较老,BIOS里没开启虚拟化(Intel VT-x或AMD-V),Docker Desktop是启动不了的。进BIOS找到
Intel Virtualization Technology或SVM Mode,改成Enabled,保存重启就行。这一步是Windows下最常见的坑。
2.2 WSL2与虚拟化支持
WSL 2本质上是一个轻量级虚拟机,它在Hyper-V虚拟化平台上运行一个完整的Linux内核,这让Docker容器可以直接运行在Linux内核之上,而不是像老Docker Toolbox那样通过VirtualBox套一层。
为什么要强调WSL 2?因为Windows本身不是Linux系统,Docker容器里的进程运行在Linux内核上,Docker Desktop需要一个Linux环境来承载容器。Hyper-V方案也能做到,但WSL 2更轻、启动更快、资源占用更合理。
安装WSL 2需要在“控制面板 -> 程序 -> 启用或关闭Windows功能”里勾选“适用于Linux的Windows子系统”和“虚拟机平台”,然后重启。也可以用管理员权限的PowerShell执行:
wsl --install这个命令会默认安装WSL 2并启用所有必要组件。装完后在PowerShell里运行wsl --status可以查看当前版本。
常见报错信息:
Docker Desktop failed to start because virtualisation support wasn't detected。这个提示看起来是虚拟化没开,但实际上除了BIOS设置,还可能是Windows的虚拟化相关功能没完全启用,或者WSL 2内核组件缺失。先跑一遍wsl --update更新内核,再重启Docker Desktop,大概率能解决。
2.3 给Docker配置国内镜像加速
在Windows下拉取Docker镜像的体验,说实话有时候挺煎熬的,尤其是拉Python、Node这种基础镜像,动不动几百MB,在国内网络环境下经常超时。
Docker Desktop提供了图形化的镜像加速配置入口。打开Docker Desktop,进入Settings -> Docker Engine,在JSON配置里加上registry-mirrors字段:
{ "registry-mirrors": [ "https://docker.m.daocloud.io", "https://dockerproxy.com" ] }保存后Docker Desktop会自动重启引擎。这里提醒一句:镜像加速只能加速从Docker Hub拉取公共镜像的过程,如果你访问的是私有镜像仓库,需要额外配置认证。
注意:网上很多教程让你直接改
C:\Users\用户名\.docker\daemon.json,其实在Docker Desktop的Settings里改会更安全,避免JSON格式写错导致Docker引擎起不来。
3. Django项目Docker化:从Dockerfile说起
3.1 调整项目结构
写Dockerfile之前,先看一下我这次部署的项目结构:
django_blog/ ├── blog/ │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── posts/ │ ├── __init__.py │ ├── models.py │ ├── views.py │ └── apps.py ├── manage.py ├── requirements.txt ├── .env ├── .dockerignore ├── Dockerfile └── docker-compose.yml这里有几个点要注意:
第一,requirements.txt要锁定版本号。不要写Django>=4.2这种宽松版本,不然不同时间构建镜像拉到的版本可能不同,镜像内容就不一致了。我这次用的是Django==4.2.7、gunicorn==21.2.0。
第二,.env文件不要提交到代码仓库。里面包含SECRET_KEY、数据库密码这些敏感信息。配置进Docker后,通过环境变量方式注入容器。
第三,Dockerfile放在项目根目录,构建上下文就是项目根目录,这样COPY指令可以访问到整个项目。
3.2 编写Dockerfile的关键细节
这是我给这个项目写的Dockerfile:
FROM python:3.11-slim ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ PIP_NO_CACHE_DIR=1 \ PIP_DISABLE_PIP_VERSION_CHECK=1 WORKDIR /app RUN apt-get update \ && apt-get install -y --no-install-recommends \ build-essential \ libpq-dev \ curl \ && rm -rf /var/lib/apt/lists/* COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . RUN useradd -m appuser USER appuser EXPOSE 8000 CMD ["gunicorn", "blog.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3"]逐行说说为什么这么写:
PYTHONDONTWRITEBYTECODE=1和PYTHONUNBUFFERED=1是老生常谈了。前者防止Python在容器里生成__pycache__文件,后者让日志输出不缓冲,这样容器里print的内容能实时显示在Docker日志里,排查问题非常有用。
PIP_NO_CACHE_DIR=1是控制pip不缓存下载的安装包。虽然这会稍微拖慢安装速度,但能显著缩小镜像体积。对于部署场景来说,镜像越小,推送和拉取越快。
python:3.11-slim基础镜像自带的是Debian的最小版本,里面很多编译工具没有。如果requirements里包含了需要编译的包(比如psycopg2-binary或mysqlclient),就需要提前装build-essential和对应的数据库开发库。我这里装了libpq-dev,因为项目用的是PostgreSQL。
踩坑经验:apt-get install那一层一定要放到COPY requirements之前。因为Docker构建有层缓存机制,requirements或代码变了,前面这些系统依赖层不需要重新构建。把系统依赖放在最前面,能大幅缩短反复构建的时间。
最后用useradd创建了一个非root用户运行应用,这纯粹是出于安全考虑。如果容器以root身份运行,被攻破后攻击者直接就有root权限,影响面太大。生产环境跑应用尽量别用root。
3.3 .dockerignore的作用
很多人写Dockerfile时会忽略.dockerignore,但它其实很重要。它和.gitignore作用类似,告诉Docker构建上下文里哪些文件不要打包进镜像。
我这次写的.dockerignore:
__pycache__/ *.pyc *.pyo .env .git .idea/ .vscode/ staticfiles/ media/ db.sqlite3 Dockerfile docker-compose.yml为什么.env一定要排除?因为构建上下文里的所有文件,在镜像构建过程中都是可见的。如果你的COPY . .把.env带进去了,那么镜像里就包含了数据库密码等敏感信息,任何人拿到镜像都能解出来。
排除staticfiles/和media/也不难理解,这些都是运行时才生成或挂载的文件,放在镜像里既占空间又容易过期。实际运行时会通过卷或者外部目录挂载进去。
还有一个很实际的好处:排除无关文件后,构建速度会明显提升,因为Docker不需要把大量无关文件传输给构建程序。
3.4 环境变量管理
Django项目里有很多配置项,SECRET_KEY、DEBUG、DATABASE_URL,这些在不同环境下值不一样,写得不能死。
我在settings.py里用os.environ.get()来读取:
import os SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "change-me") DEBUG = os.environ.get("DJANGO_DEBUG", "False").lower() == "true" ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "*").split(",") DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", "NAME": os.environ.get("DB_NAME", "django"), "USER": os.environ.get("DB_USER", "django"), "PASSWORD": os.environ.get("DB_PASSWORD", ""), "HOST": os.environ.get("DB_HOST", "db"), "PORT": os.environ.get("DB_PORT", "5432"), } }HOST默认值写成db,这个后面配合docker-compose服务名就能直接连通,不需要在Windows里配数据库主机IP。
4. docker-compose编排:让多容器协同工作
4.1 为什么要用docker-compose
如果你的项目只需要一个Django容器,那直接docker run就够了。但实际项目里基本不可能只跑Django——数据库、Redis、消息队列,这些服务要是都用docker run逐个起,命令行参数会变得非常长且容易出错。
docker-compose的价值就在于:用YAML文件把多个容器的配置、网络联通、卷挂载、环境变量统一描述,一条命令拉起来,一条命令关掉。
以我这个项目为例,数据库容器用了PostgreSQL,还加了一个Redis服务,用于Celery消息队列。这三个服务互相关联,用docker-compose管理非常清晰。
4.2 编写docker-compose.yml
version: "3.8" services: db: image: postgres:15 container_name: django_blog_db restart: unless-stopped environment: POSTGRES_DB: ${DB_NAME} POSTGRES_USER: ${DB_USER} POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"] interval: 5s timeout: 3s retries: 5 redis: image: redis:7-alpine container_name: django_blog_redis restart: unless-stopped volumes: - redis_data:/data web: build: . container_name: django_blog_web restart: unless-stopped command: > sh -c "python manage.py migrate && python manage.py collectstatic --noinput && gunicorn blog.wsgi:application --bind 0.0.0.0:8000 --workers 3" volumes: - .:/app - static_volume:/app/staticfiles - media_volume:/app/media ports: - "8000:8000" env_file: - .env depends_on: db: condition: service_healthy redis: condition: service_started几个关键点展开说一下:
db服务配置了healthcheck,Web服务通过depends_on的service_healthy条件,确保等数据库完全就绪后才启动Django,避免启动时连不上数据库报错。这个做法比老式的sleep 5可靠得多。
web服务的volumes把当前目录挂载到容器的/app。好处是开发时改代码不需要重新构建镜像,容器里代码实时更新。但后面要说的坑也在这里,Windows的文件系统映射性能问题。
env_file: .env会把宿主机当前目录下的.env文件读进来,注入到容器环境变量里。前面Dockerfile里用的os.environ.get()读取到的值,就是从这里来的。
depends_on只是在启动顺序上做了约束,并不代表人健全。真正的健康检查靠docker healthcheck,这是容器编排里很重要的一个细节。
4.3 数据卷与持久化
容器本身是无状态的,容器销毁后里面写的数据就丢了。但数据库里的用户记录、上传的媒体文件不能丢。
解决办法是使用volumes挂载。数据库容器挂了postgres_data、redis挂了redis_data和Web容器挂了static_volume、media_volume。这些卷定义在docker-compose.yml的顶层:
volumes: postgres_data: redis_data: static_volume: media_volume:卷可以在容器重建后保留数据。具体来说:
- 数据库数据存在
postgres_data卷,即使容器删了重建,数据还在 - 静态文件存在
static_volume,收集一次后不会被容器重建清掉 - 媒体文件存在
media_volume,用户上传的图片不会因为容器重建丢失
在Windows下用Docker调测时,数据卷的位置在WSL 2的虚拟磁盘里,不在C:\Users下直接可见,不要试图去文件管理器里找它。
实战建议:如果需要备份数据库,不要直接去目录复制文件。生产环境推荐
docker exec django_blog_db pg_dump -U django django > backup.sql来导出逻辑备份,这种备份文件跨环境恢复更方便。
5. 实际操作过程:从构建到运行
5.1 构建镜像
在项目根目录执行:
docker-compose build这个过程会按顺序构建web服务,然后拉取db和redis镜像。如果网络环境不理想,等待时间会比较长。看到一个镜像层一次输出,表示构建成功了。
构建完成后可以用docker-compose images查看当前项目下所有镜像。
如果遇到构建超时,通常是网络问题。改成国内镜像加速后重试,或者检查公司网络是不是封了Docker Hub的某些域名。
5.2 启动数据库并执行迁移
构建好镜像后,先单独把数据库和Redis拉起来:
docker-compose up -d db redis然后确认数据库状态:
docker-compose ps看到db健康检查显示healthy后,再启动web服务:
docker-compose up -d webweb容器启动时定义的command会自动执行python manage.py migrate,把模型映射到数据库表。执行完成后gunicorn正式开始监听8000端口。
如果迁移执行有问题,可以看日志:
docker-compose logs web5.3 创建超级管理员与配置ALLOWED_HOSTS
Django迁移完成后,数据库里还没有管理员账号。需要进容器手动创建:
docker-compose exec web python manage.py createsuperuser按提示输入用户名、邮箱、密码。这里有个小坑:Windows终端下输入密码时可能没有回显,这是正常的,不是卡住了。
然后配置ALLOWED_HOSTS。如果你直接用http://localhost:8000访问,在.env里设置DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1。如果以后要部署到服务器,加上服务器IP或域名。
5.4 静态文件收集与访问
Django开发环境下静态文件由runserver自动处理,但gunicorn不负责静态文件。所以要在容器启动时执行collectstatic。
我把collectstatic --noinput放在启动命令里,和migrate一起执行。有个细节:收集静态文件时需要能写STATIC_ROOT目录。我在settings.py里设置:
STATIC_URL = "/static/" STATIC_ROOT = BASE_DIR / "staticfiles"容器里这个目录挂在static_volume卷上,可以正常写入。收集完成后,如果你后续加了nginx容器,就把这个卷挂载给nginx,由nginx直接服务静态文件。
如果现在就想简单验证静态文件能不能访问,可以用Django自带的runserver模式临时起一个容器:
docker-compose run --rm web python manage.py runserver 0.0.0.0:8000这样静态文件就可以通过Django自己服务了,但只建议调试时这么做。
6. 常见问题与排查技巧实录
6.1 Docker Desktop启动失败:虚拟化报错
这是Windows下遇到最多的一个错误,报错信息通常是:
Docker Desktop failed to start because virtualisation support wasn't detected出现这个问题的原因有几个可能,按优先级排查:
第一步,确认BIOS虚拟化已开启。重启进BIOS,找到Intel Virtualization Technology或SVM Mode,确保是Enabled。
第二步,检查Windows功能里是否启用了虚拟机平台和适用于Linux的Windows子系统。以管理员运行PowerShell执行:
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart第三步,确认WSL 2是默认版本:
wsl --set-default-version 2更新WSL内核也顺手做一下:
wsl --update做完以上步骤重启电脑,再启动Docker Desktop,成功率很高。
这是Windows下Docker最让人头疼的坑,我处理过好几台机器,最后都是三步走解决:BIOS虚拟化、Windows功能、WSL内核更新。
6.2 端口被占用
启动容器时如果提示port is already allocated,说明8000端口被某个进程占用了。
在Windows下用这个命令查看谁占了端口:
netstat -ano | findstr :8000输出最后一列是PID,打开任务管理器根据PID找到对应进程。最常见的是我之前本地跑过的Python进程或者某个IDE占用了同一个端口。
解决办法有两个:
- 杀掉占用进程
- 改docker-compose里的端口映射,比如
"8001:8000",然后访问http://localhost:8001
开发环境推荐用第二个方法,不影响本机原有服务。
6.3 Windows下挂载卷性能慢
开发模式下把整个项目目录挂载进容器,会发现代码改动后容器里响应比较慢。原因在于Windows文件系统与Linux文件系统之间通过WSL 2做了一个转换层,跨文件系统操作性能天然有损耗。
这个问题的缓解方案:
- 尽量把代码放在WSL 2的文件系统里,也就是
\\wsl$\...路径下,而不是放在C:\盘。在WSL中开发,性能会改善很多。 - 生产部署时不要用开发模式的挂载,直接把代码COPY进镜像,不依赖宿主机文件,性能最佳。
- 懒人方案:把
volumes里的.:/app这部分注释掉,改用镜像内代码,但这样改代码后需要重新构建,调试不太方便。
6.4 时区与数据库中文乱码
Windows下部署Django有一个容易忽略的细节:时区。
Django默认的TIME_ZONE是UTC,日志和数据库存储的时间都是UTC时间。对国内开发来说,最好在settings.py里改成:
TIME_ZONE = "Asia/Shanghai" USE_TZ = True开启USE_TZ = True时,Django会在存储时把当前时间转成UTC存数据库,展示时再转回本地时区。这样处理对多时区应用更安全,但对于只在单一时区运行的内部系统来说,反而容易混淆。如果确定只用国内时间,把USE_TZ = False也可以,但跨时区部署时容易踩坑。
数据库中文字符显示乱码的问题,多数情况是数据库连接时没有指定UTF-8编码。在.env里给数据库连接URL加上编码参数:
DATABASE_URL=postgresql://django:password@db:5432/django?client_encoding=utf8MySQL则是在启动mysql容器时加参数:
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci6.5 Celery容器如何集成
如果项目用了Celery,一般还需要一个worker容器。在docker-compose里加一个service:
worker: build: . command: celery -A blog worker --loglevel=info env_file: - .env depends_on: - db - redis用同一个镜像启动不同进程,是容器化部署Celery的标准做法。Django代码一份,跑Web的进程跑gunicorn,跑异步任务的进程跑worker,互不干扰。
6.6 忘记环境变量导致报错
如果启动时出现KeyError,多半是.env里缺少某个变量。把os.environ["XXX"]改成os.environ.get("XXX", "默认值"),既能启动,又能快速定位哪个变量没配。
实战排查时建议进容器里看一下环境变量是否注入成功:
docker-compose exec web env这样能看到容器里实际生效的所有环境变量,排查问题效率高很多。
7. 结尾
一次完整的Windows下Docker部署Django项目流程走下来,最大的感受是:环境配置这些琐碎的脏活累活,就该交给Docker去处理。以前配一次Python开发环境要半小时起步,装数据库驱动还可能编译失败,现在一份Dockerfile和docker-compose.yml就能把整个技术栈固定下来,换机器、换同事、换服务器,都是同样的体验。
最后再分享一个小技巧:Dockerfile和docker-compose.yml这种配置文件,我建议放进版本控制里,和项目代码一起提交。这样团队里任何人拉下来代码,直接docker-compose up -d就能把整套环境跑起来,这才是真正的“一键部署”。后续如果加了Celery或者换成了更复杂的架构,把这套文件当模板调整一下就行,比每次搭环境都从头折腾要省太多时间。