前言

前几天的文章中,我们已经把使用 pdm 的项目用 docker 搞定了,那么下一步就是把完整的 DjangoStarter v3 版本用 docker 部署。

现在不像之前那么简单直接一把梭了,因为项目用了 npm, gulp 之类的工具来管理前端依赖,又使用 pdm 管理 python 依赖,所以这波我用上了多阶段构建(multi-stage build)

而且这次还把 uwsgi 给替换掉了。不过先别说 uwsgi 老归老,性能还是不错的,只不过现在已经是 asgi 时代了,wsgi 限制还是有点多,再加上我这次部署的项目用到了 channels ,所以就顺理成章用上了它推荐的 daphne 服务器,感觉还行,更多用法还在探索中,后续搞出来就写文章来记录。

在踩过很多坑之后,终于把这套玩意搞定了。

PS:折腾这玩意真是心累…感觉自己就是一个无情的网络搬运工,根据搜索引擎和官方文档搜索到的资料(现在还可以加上GPT),不断排列组合,最终形成可以用的方案

本文记录一下折腾的过程,同时新的 docker 部署方案很快就合并入 DjangoStarter 项目的 master 分支。

一些概念

深刻理解 docker 的工作原理有助于避免很多问题

每次遇到很多坑焦头烂额之后,都会深深感觉自己还是太菜了

多阶段构建

在Dockerfile中,使用多个 FROM 指令并通过 AS 关键字为每个 FROM 指定一个名称的功能被称为 “多阶段构建”(multi-stage build)

多阶段构建允许你在一个Dockerfile中使用多个基础镜像,并且可以在构建过程中选择性地从某个阶段复制构建结果到另一个阶段。这样做的好处是,你可以在前面的阶段中使用一个较大的镜像来进行构建工作(例如编译代码),然后在最终阶段中只保留必要的文件,将它们放入一个更小的基础镜像中,以减少最终镜像的大小。

多阶段构建极大地简化了构建复杂镜像的流程,同时也有助于保持最终镜像的体积较小。

有几点要注意的:

  • 每个阶段之间不需要指明依赖关系,build 的时候会同时进行构建,只有遇到 COPY --from=<阶段名称>COPY --from=<阶段索引> 这个语句才会等待依赖的阶段构建完。
  • 每个 FROM 指令都会启动一个新的构建阶段。这些阶段是独立的,一个阶段的环境变量、文件系统状态等不会自动传递给下一个阶段,每个阶段可以选择从任何之前的阶段中复制构建成果。
  • 多阶段构建中,Dockerfile中的指令是顺序执行的,前面的阶段不会自动传递文件或状态到后面的阶段,除非明确使用 COPY --from=<阶段> 指令。

Docker Volumes 机制

Docker volumes 是由 Docker 管理的数据卷,用于在容器之间以及容器和宿主机之间持久保存和共享数据的机制。

  • 即使容器被删除,保存在 volumes 中的数据仍然存在
  • 可以将同一个 volume 挂载到多个容器上,实现数据的共享
  • volume 数据存储在 Docker 主机的特定区域,容器只是通过挂载点访问这些数据
  • 优先级(很重要),容器运行时挂载的 volume 会覆盖容器中相应路径的内容。(我就是因为这个覆盖的问题,导致 static-dist 里的文件一直无法更新)

所以在静态文件共享的场景下,如何保持数据一致性就很重要了。

dockerfile

直接来看看我最终搞完的 dockerfile 吧

ARG PYTHON_BASE=3.11
ARG NODE_BASE=18 # python 构建
FROM python:$PYTHON_BASE AS python_builder # 设置 python 环境变量
ENV PYTHONUNBUFFERED=1
# 禁用更新检查
ENV PDM_CHECK_UPDATE=false # 设置国内源
RUN pip config set global.index-url https://mirrors.cloud.tencent.com/pypi/simple/ && \
# 安装 pdm
pip install -U pdm && \
# 配置镜像
pdm config pypi.url "https://mirrors.cloud.tencent.com/pypi/simple/" # 复制文件
COPY pyproject.toml pdm.lock README.md /project/ # 安装依赖项和项目到本地包目录
WORKDIR /project
RUN pdm install --check --prod --no-editable # node 构建
FROM node:$NODE_BASE as node_builder # 配置镜像 && 安装 pnpm
RUN npm config set registry https://registry.npmmirror.com && \
npm install -g pnpm # 复制依赖文件
COPY package.json pnpm-lock.yaml /project/ # 安装依赖
WORKDIR /project
RUN pnpm i # gulp 构建
FROM node:$NODE_BASE as gulp_builder # 配置镜像 && 安装 pnpm
RUN npm --registry https://registry.npmmirror.com install -g gulp-cli # 复制依赖文件
COPY gulpfile.js /project/ # 从构建阶段获取包
COPY --from=node_builder /project/node_modules/ /project/node_modules # 复制依赖文件
WORKDIR /project
RUN gulp move # django 构建
FROM python:$PYTHON_BASE as django_builder COPY . /project/ # 从构建阶段获取包
COPY --from=python_builder /project/.venv/ /project/.venv
COPY --from=gulp_builder /project/static/ /project/static WORKDIR /project
ENV PATH="/project/.venv/bin:$PATH"
# 处理静态资源资源
RUN python ./src/manage.py collectstatic # 运行阶段
FROM python:$PYTHON_BASE-slim-bookworm as final # 从构建阶段获取包
COPY --from=django_builder /project/.venv/ /project/.venv
COPY --from=django_builder /project/static-dist/ /project/static-dist
ENV PATH="/project/.venv/bin:$PATH"
ENV DJANGO_SETTINGS_MODULE=config.settings
ENV PYTHONPATH=/project/src
ENV PYTHONUNBUFFERED=1
COPY src /project/src
WORKDIR /project

multi-stage build

这个 dockerfile 里有这几个构建阶段,看名字可以可以看出个大概了

  • python_builder: 安装 pdm 包管理器和 python 依赖
  • node_builder: 安装前端依赖
  • gulp_builder: 使用 gulp 工具处理整合前端资源
  • django_builder: 将前面几个容器的构建成果里拿出 python 依赖和前端资源,然后执行 collectstatic 之类的工作(当前仅此项,以后可能会增加其他的)
  • final: 最终完成后用于运行和生成镜像

要点

在调试这个 dockerfile 的过程中,有一些要关键点

  • 构建阶段不要使用 slim 镜像,以免环境太简陋遇到一些奇奇怪怪的问题
  • 从 django_builder 阶段开始,涉及到 python 的运行了,必须将虚拟环境中的 python 路径加入环境变量
  • 因为 DjangoStarter v3 开始使用新的项目结构,源代码都放在根目录的 src 目录下,所以在 final 阶段需要把这个目录加入 PYTHONPATH 环境变量,不然会遇到奇怪的包导入问题(我在用 uwsgi 时就遇到了)
  • 还是 final 阶段,我还设置了 DJANGO_SETTINGS_MODULE, PYTHONUNBUFFERED 等环境变量,不管有没有用,先保持跟开发环境一致避免遇到问题

docker-compose

我在 compose 配置里用了一些环境变量

避免了每个项目都要去修改项目名啥的

先上配置,后面再来介绍。

services:
redis:
image: redis
restart: unless-stopped
container_name: $APP_NAME-redis
expose:
- 6379
networks:
- default
nginx:
image: nginx:stable-alpine
container_name: $APP_NAME-nginx
restart: unless-stopped
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
- ./media:/www/media:ro
- static_volume:/www/static-dist:ro
depends_on:
- redis
- app
networks:
- default
- swag
app:
image: ${APP_IMAGE_NAME}:${APP_IMAGE_TAG}
container_name: $APP_NAME-app
build: .
restart: always
environment:
- ENVIRONMENT=docker
- URL_PREFIX=
- DEBUG=true
# command: python src/manage.py runserver 0.0.0.0:8000
command: >
sh -c "
echo 'Starting the application...' &&
cp -r /project/static-dist/* /project/static-volume/ &&
exec daphne -b 0.0.0.0 -p 8000 -v 3 --proxy-headers config.asgi:application
"
volumes:
- ./media:/project/media
- ./src:/project/src
- ./db.sqlite3:/project/db.sqlite3
- static_volume:/project/static-volume
depends_on:
- redis
networks:
- default volumes:
static_volume: networks:
default:
name: $APP_NAME
swag:
external: true

几个要点

nginx

这里面我加入了 nginx 容器,用来提供 web 服务,因为之前一直使用 uwsgi ,而是要 uwsgi 的 socket 模式是没有静态文件功能的,要让 uwsgi 提供静态文件服务,除非使用 HTTP 模式,不过那样又失去了 uwsgi 的优势了。所以我一直是用 nginx 来提供静态文件访问。

不过现在已经不直接在服务器上安装 nginx 而是改成了 swag 容器一把梭,所以必须得用在 compose 里加一个 web 服务器,既然如此,就还是继续 nginx 吧,用得比较熟了。关于这个问题,之前这篇文章(项目完成小结 - Django-React-Docker-Swag部署配置)也有讨论到。

image name

这次 build 出来的镜像终于加上名字了…

为接下来 push 到 docker hub 铺路

数据共享

因为之前一直是在本地执行 collectstatic 然后再上传到服务器,所以不存在静态文件的问题,但一点也不优雅,对于 CICD 来说也很不友好

这次 DjangoStarter v3 也一并解决这个痛点,把前端依赖和资源管理都整合到 docker 的 build 阶段里面了,所以需要使用 docker volume 来为 app 和 nginx 容器共享这部分静态资源

正如开头说的 volume 优先级更高,导致就算后面修改了一些 static 资源,build 后重启也是用已经 mounted 到 volume 里的旧版,所以这里我把 app 容器的 /project/static-volume 挂载到共享的 static_volume ,然后再把 /project/static-dist 里的文件复制过去,而不是直接挂载到 /project/static-dist 里,这样会导致 static_volume 里的旧数据把 collectstatic 出来的文件覆盖掉。

还有其他的方式,比如每次启动前先把 static_volume 里的文件清理掉,然后再挂载,不过试了下有点折腾,我就放弃了。

关于应用服务器的选择

Django(或者说是 Python 系的后端框架)不像 .netcore, springboot 这类框架一样内置 kestrel, tomcat 之类的服务器,所以部署到生产环境只能借助应用服务器。

常见的 Django 应用服务器有 uWSGI、Gunicorn、Daphne、Hypercorn、Uvicorn 等

之前一直使用 uWSGI ,这是个功能强大且高度可配置的 WSGI 服务器,支持多线程、多进程、异步工作模式,并且具有丰富的插件支持。它的高性能和灵活性使其成为许多大型项目的首选。然而,uWSGI 的配置相对复杂,对于新手来说可能不太友好。uWSGI 的高复杂性在某些场景下可能导致配置错误或难以调试。

然后在本文的例子里,使用 uwsgi 部署一直出问题,因为之前有个项目用到了 channels ,所以这次使用了 Daphne,这是 Django Channels 项目的核心部分,是一个支持 HTTP 和 WebSocket 的 ASGI 服务器。而且也支持 HTTP2 之类的新功能,其实也挺不错的。

如果需要处理 WebSocket 连接或使用 Django Channels,Daphne 是一个理想的选择。Daphne 可以与 Nginx 等反向代理服务器结合使用,以处理静态文件和 SSL 终端。

接下来我还打算试试 Gunicorn 和基于 ASGI 的 Uvicorn,这个好像在 FastAPI 项目里用得比较多,Django 自从3.0开始支持异步功能(也就是 ASGI),所以其实可以放弃传统的 WSGI 服务器了?

其他的几个我复制一些介绍

Gunicorn (Green Unicorn) 是一个轻量级的 WSGI 服务器,专为简单易用而设计。Gunicorn 的配置简单,默认情况下就能提供较好的性能,因此在开发和生产环境中都被广泛使用。与 uWSGI 相比,Gunicorn 的学习曲线较低,适合大多数 Django 项目。

Hypercorn 是一个现代化的 ASGI 服务器,支持 HTTP/2、WebSocket 和 HTTP/1.1 等多种协议。它可以运行在多种并发模式下,如 asyncio 和 trio。Hypercorn 适用于需要使用 Django Channels、WebSocket,或者希望在未来支持 HTTP/2 的项目。

Uvicorn 是一个基于 asyncio 的轻量级、高性能 ASGI 服务器,专为速度和简洁性而设计。它支持 HTTP/1.1 和 WebSocket,同时也是 HTTP/2 的早期支持者。Uvicorn 通常与 FastAPI 搭配使用,但它同样适用于 Django,特别是当你使用 Django 的异步特性时。Uvicorn 的配置相对简单,且启动速度非常快,适合开发环境和需要异步处理的生产环境。

ASGI 已经是未来的趋势了,所以接下来还是放弃 WSGI 吧…

小结

之前折腾的时候花了很多时间,实际总结下来也没啥,就那几个关键点。但因为对 docker, WSGI, ASGI, Python运行机制等理解不够深刻,所以就导致踩了很多坑,最终靠排列组合完成了这套 docker 方案……

就这样吧,接下来我会继续完善 DjangoStarter v3 ,最近还有其他一些关于 Django 的开发经验可以记录的。

参考资料

新版的Django Docker部署方案,多阶段构建、自动处理前端依赖的更多相关文章

  1. 使用 Docker 开发 - 使用多阶段构建镜像

    多阶段构建是一个新特性,需要 Docker 17.05 或更高版本的守护进程和客户端.对于那些努力优化 Dockerfiles 并使其易于阅读和维护的人来说,多阶段构建非常有用. 在多阶段构建之前 构 ...

  2. .net core in Docker 部署方案(随笔)

    前一段时间由于项目需要 .net core 在docker下的部署,途中也遇到很多坑,看了各同行的博客觉得多多少少还是有些问题,原本不想写此篇文章,由于好友最近公司也需要部署,硬是要求,于是花了些时间 ...

  3. springboot整合docker部署(两种构建Docker镜像方式)--2019-3-5转

    原文:https://www.cnblogs.com/shamo89/p/9201513.html 项目结构 package hello; import org.springframework.boo ...

  4. springboot整合docker部署(两种构建Docker镜像方式)

    项目结构 package hello; import org.springframework.boot.SpringApplication; import org.springframework.bo ...

  5. 使用docker 部署codis

    使用docker 部署codis 原文地址:https://www.jianshu.com/p/85e72ae6fec3 codis的架构图 1.zookeeeper,用于存放统一配置信息和集群状态 ...

  6. Docker 使用指南 (六)—— 使用 Docker 部署 Django 容器栈

    版权声明:本文由田飞雨原创文章,转载请注明出处: 文章原文链接:https://www.qcloud.com/community/article/98 来源:腾云阁 https://www.qclou ...

  7. Docker 部署Django项目

    使用docker部署django项目也很简单,挺不错,分享下 环境 默认你已安装好docker环境 django项目大概结构 (p3s) [root@opsweb]# tree opsweb opsw ...

  8. docker 部署django项目(nginx + uwsgi +mysql)

    最近在学习用docker部署Django项目,经过百折不挠的鼓捣,终于将项目部署成功,爬过好多坑,也发现很多技能需要提高.特此写下随笔与小伙伴们分享,希望能对大家有所启发. docker的理论我就不赘 ...

  9. 云服务器上利用Docker部署Django项目

    转载别人的,请看下面链接 云服务器上利用Docker部署Django项目

  10. Docker:docker部署PXC-5.7.21(mysql5.7.21)集群搭建负载均衡实现双机热部署方案

    单节点数据库弊端 大型互联网程序用户群体庞大,所以架构必须要特殊设计 单节点的数据库无法满足性能上的要求 单节点的数据库没有冗余设计,无法满足高可用 推荐Mysql集群部署方案 PXC (Percon ...

随机推荐

  1. 【IEEE 出版】 第三届能源与电力系统国际学术会议 (ICEEPS 2024)

    [连续2届会后4-5个月EI检索,检索稳定!特邀院士.Fellow 报告!]第三届能源与电力系统国际学术会议 (ICEEPS 2024)以"创造更加柔性.智能的能源电力系统"为主题 ...

  2. GIT 生成变更历史文件清单

    脚本搞定git文件版本变化信息,解决部署种变更的审核和统计信息工作复杂问题 git diff --name-status --ignore-cr-at-eol --ignore-space-at-eo ...

  3. Masonry在视图相对关系处理中的各种“offset”

    如果我们需要设置一个view在另一个view的右边缘距离一定距离的地方,利用Masonry这么写: [a mas_makeConstraints:^(MASConstraintMaker *make) ...

  4. 阿里云ecs自定义镜像并导出到OSS、并下载

    OSS是什么? 有个文章说得比较浅显清楚:什么是OSS?5分钟带你了解! - 知乎 (zhihu.com) 这里摘选核心内容: 白话文解释就是将系统所要用的文件上传到云硬盘上,该云硬盘提供了文件下载. ...

  5. 认真学习css3-2-css的选择器

    关于有哪些选择器,具体可以查看w3school. 本文写了一个考卷的例子,带有部分js,jquery.不会针对每个选择器做示例,只练习了一些常用的,有意思的. 先看html/js代码: <!DO ...

  6. 【创龙全国产T3核心板】赋能工业领域新发展

    在工业5.0时代浪潮持续推进并具备确定性的时代背景下,工业领域创新升级的需求日益增长,为满足各种工业环境下的应用需求,面向工业领域,创龙科技推出了基于全志T3处理器的元器件全国产化工业级核心板--SO ...

  7. 对linux的理解--个人理解

    linux系统中的命令我觉得可以和windows上的点点点,如文件的查找,文件的新建.删除,用户的添加.删除等来对比理解.一个是点点点,一个是用命令来完成. --------------------- ...

  8. Intellij IDEA 'Error:java: 无效的源发行版:13'

    第一步,依次点击,File - Settings - Bulid, Execution,Deployment - Compiler - Java Compiler,修改版本为13(你使用的java是哪 ...

  9. js 获取年、月、周、当前日期第几周、这月有那几周

    查看当前日期是第几周:https://wannianli.tianqi.com/today/zhou/ //获取完整的日期 var date=new Date; var y = date.getFul ...

  10. 探究kubernetes 探针参数periodSeconds和timeoutSeconds

    探究kubernetes 探针参数 periodSeconds和timeoutSeconds 问题起源 kubernetes probes的配置中有两个容易混淆的参数,periodSeconds和ti ...