文章目录
- 使用 docker 在 heroku 上单独部署 vue 前端
- 使用 docker 在 heroku 上单独部署 django 后端
- 创建 heroku 项目
- 构建 Dockerfile
- 设置 settings.py
- database
- 静态文件管理
- 安全设置
- applicaiton & 中间件配置
- 设置 requirements.txt
- heroku container 部署应用
- 前后端分别部署的优势和问题分别是什么
- 好处
- 解耦合
- 灵活性
- 安全性
- 技术栈独立
- 坏处
- 使用 docker 在 heroku 上整体部署 vue + django
- step1: 将 vue 的文件整个挪到 django 项目根目录底下
- step2: 构建 dockerfile
- step3: settings.py
- step4: 更新 django url.py 并在 views.py 中呈现 Vue 应用
- step5: heroku container 部署整体项目
- step6: debug -- 让 django 能找到 vue 的静态资源
- 调整 Vue 构建配置
- 在 Django 中处理静态文件引用
使用 docker 在 heroku 上单独部署 vue 前端
- 在 这篇文章 中我已经介绍了如何通过
docker
单独部署一个 vue 的前端项目 - 在这个章节,我想按照下面的结构来给大家介绍:
- 如何使用 docker 在 heroku 上单独部署 django 后端
- 前后端分别部署的问题是什么
- 如何将 vue + django 合并到一起通过 docker 构建一个完整的项目
使用 docker 在 heroku 上单独部署 django 后端
- 我之前在讲如何使用 heroku 的 repository 部署 django 项目的 时较为详细地讲了如何创建
Procfile
和runtime.txt
以及里面写什么内容, - 但是我们使用
docker
来部署 django 项目,是使用heroku container
,因此不需要设定Procfile
和runtime.txt
等文件,我们会将所有需要使用的指令都放在Dockerfile
里面
创建 heroku 项目
- 如果您还没有创建应用,可以使用以下命令创建:
heroku create <应用名称>
- 也可以用 heroku 的图形化界面创建
构建 Dockerfile
# 使用兼容 Django 项目的 Python 镜像
FROM --platform=linux/amd64 python:3.9
# 设置工作目录为 /app
WORKDIR /app
# 复制项目文件到容器中
COPY . /app
# 更新安装包
RUN apt-get update
# 安装项目依赖 (这里是我自己的 Linux 依赖,如果你不需要安装这种依赖,那就只需要 RUN pip install -r requirements.txt)
RUN apt-get install -y portaudio19-dev
RUN pip install -r requirements.txt
# 设置环境变量
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# 运行 Django 服务,注意下面两个 # 修饰的部分是错误的写法
#CMD ["python", "manage.py", "runserver", "0.0.0.0:9000"]
#CMD ["python", "manage.py", "runserver", "0.0.0.0:$PORT"]
# CMD python manage.py runserver 0.0.0.0:$PORT
# CMD gunicorn <your project>.wsgi:application --bind 0.0.0.0:$PORT
CMD daphne -p $PORT -b 0.0.0.0 <your project>.asgi:application
-
需要注意的是:
-
第一种错在不应该指定 9000 这个端口
- 因为
heroku
在执行的时候会自动分配端口并生成一个http
的url
, - 外面的服务需要访问你部署的系统时直接访问这个
url
就可以访问服务,而不需要访问 9000 端口
- 因为
-
第二种错在不应该使用
CMD [..., ..., ...]
这种形式:- CMD指令可以有两种不同的格式:shell 格式和 exec格式。这两种格式在处理环境变量时有所不同
- 使用shell格式(如
CMD python manage.py runserver 0.0.0.0:$PORT
)时,命令会在shell中执行,这意味着可以进行环境变量展开。因此$PORT
会被正确解析为 Heroku分配的端口值 - 使用exec格式(如
CMD ["python", "manage.py", "runserver", "0.0.0.0:$PORT"]
)时,命令不会在shell中执行,而是直接作为进程启动。这意味着不会进行环境变量展开,因此$PORT
不会被解析为其实际值,而是被当作字符串$PORT
处理。
-
第三种没有错,但是使用
daphne
或者gunicorn
来启动服务器的方式在性能上更优秀。 -
第四种,因为我的项目里面用了
websocket
,而gunicorn
不能处理这种情况,因此不行 -
第五种是最好的,但是一定要注意,要指定
-b 0.0.0.0
如果不指定默认在127.0.0.1
,这样是不对的。同时,这里简单阐述一下为什么不直接用python manage.py runserver
而用daphne
这种专业的服务器,是因为:- Daphne 是一个 ASGI (Asynchronous Server Gateway Interface) 服务器,这意味着它能够处理异步请求,包括
WebSocket
。这对于需要实时功能(如聊天应用、实时通知等)的应用程序来说非常重要。
Django的 runserver 命令是一个 WSGI (Web Server Gateway Interface) 服务器,它 只能处理同步请求。虽然 Django 从3.0版本开始支持异步视图和中间件,但runserver 并不是为处理高并发或实时通信设计的。 - Daphne 旨在作为生产级的服务器运行,提供更高的性能和可靠性。它能够更好地处理多个并发连接。 Django的 runserver 主要是为开发过程中的本地测试设计的。它没有针对性能和并发进行优化,不适合用于生产环境。
- Daphne 和其他专用的ASGI/WSGI服务器如 Gunicorn、Uvicorn 等,通常包括用于生产环境的安全特性和最佳实践。 使用 Django的runserver 在生产环境中可能带来安全风险,因为它不是为暴露在公网上的环境设计的。
- Daphne 支持 HTTP/2(虽然需要额外的配置),这可以提供更好的性能,特别是在处理大量静态内容或使用了大量API请求的应用程序中。Django 的 runserver 不支持 HTTP/2。
- Daphne 可以更容易地与其他组件(如 Nginx、负载均衡器等)集成,提供更好的扩展性和部署灵活性。 直接使用 runserver 在复杂的生产环境中可能会受到限制。
- Daphne 是一个 ASGI (Asynchronous Server Gateway Interface) 服务器,这意味着它能够处理异步请求,包括
-
此外,我们指定了
FROM --platform=linux/amd64 python:3.9
是因为我用的是 macbook m1芯片,所以默认构建的 platform 和 docker 的linux
是有冲突的,因此我需要在构建的时候指定 platform -
docker 的默认根目录就是
/app
,我们用dockerfile
定义的一切,无非就是在 docker 的隔离环境中重建一个我们 local 的环境。因此 heroku 也可以直接依据 dockerfile 构建一个一模一样运行环境。 -
安装项目依赖那里之所以
RUN apt-get install -y portaudio19-dev
是因为我的项目在install requirements.txt
的时候报错,缺少一个头文件,而这个头文件依赖于我的这个 library,因此我需要 pip install 之前把这些依赖项先装了,按照自己的情况来
设置 settings.py
database
- 如果你要再 heroku 上使用 database(比如 postgres)等,就要提前在 settings 里面配置
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
静态文件管理
STATIC_URL = '/static/'
# 这么写表明当用户使用 python manage.py collectstatic 时,所有的静态资源都会放到 staticfiles 文件夹里面
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
# 这行其实没啥用,但相当于说,如果我有其他的 static 文件,我会放到 static 文件夹里面,而不会去占用 django 项目自己的静态文件夹 'staticfiles'
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
- 在Heroku上,静态文件的处理与本地环境不同。需要设置
STATIC_ROOT
和其他相关设置以确保静态文件能被正确收集和服务。
安全设置
- 例如ALLOWED_HOSTS,以包括您的Heroku应用程序的域名:
ALLOWED_HOSTS = ['your-app-name.herokuapp.com']
这个我一般设置为:
ALLOWED_HOSTS = ['*']
applicaiton & 中间件配置
INSTALLED_APPS = [
"daphne", # asgi 应用,如果不设置,默认为 wsgi 应用
"channels", # 它使服务器和客户端之间的双向通信成为可能,从而可以构建具有即时消息、通知和实时更新等功能的应用程序。
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
"<your project name>", # 只要你构建 django 项目,这个就要加进来
"corsheaders" # 解决跨域问题的
]
CORS_ALLOW_ALL_ORIGINS = True # 跨域问题
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', # cors problem
'django.middleware.security.SecurityMiddleware',
"whitenoise.middleware.WhiteNoiseMiddleware", # 它可以帮助处理静态文件的服务和缓存。它允许你将静态文件(如 CSS、JavaScript 和图像文件)直接提供给最终用户,而无需通过额外的服务器或 CDN。
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
设置 requirements.txt
-
上面配置的 application 的那些东西(daphne, channels 等),不要忘记在
requirements.txt
文件中进行定义,方便dockerfile
执行的时候自动安装这些依赖,这里提供一个requirements.txt
的样本:asgiref==3.4.1 Django==4.0.1 sqlparse==0.4.2 tzdata==2023.3 gunicorn==20.1.0 django-heroku==0.3.1 openai==0.28.1 django-cors-headers django-redis daphne channels_redis whitenoise dj-database-url opencv-python-headless channels pysbd pyaudio agora_token_builder google-api-core==2.11.0 google-auth==2.25.2 google-cloud-core==2.4.1 google-cloud-speech==2.19.0 google-cloud-storage==2.14.0 google-cloud-translate==3.13.0 google-crc32c==1.5.0 google-resumable-media==2.7.0 googleapis-common-protos==1.59.0 langchain==0.0.350 langchain-community==0.0.3 langchain-core==0.1.1 nltk==3.7
heroku container 部署应用
heroku container:push web -a <heroku application name>
heroku container:release web -a <heroku application name>
前后端分别部署的优势和问题分别是什么
- 现在我们已经讲完了 vue 和 django 分别通过 docker + heroku 的方式部署前端项目和后端项目的流程。那这里有个问题是,前后端分别部署这样的好处和坏处分别是什么呢?
好处
解耦合
前后端分离使得前端和后端可以独立开发和部署,这有助于提高团队的工作效率,同时降低了代码耦合性。
可以单独更新前端或后端,而不影响系统的其他部分。
灵活性
前后端分别部署允许您为每部分选择最适合的托管和缩放策略。例如,您可能会根据流量模式对前端和后端进行不同的缩放。
安全性
通过将前端和后端分开,您可以在网络架构上实现更细粒度的安全控制。例如,可以限制对后端服务的访问,只允许来自特定前端的请求。
技术栈独立
您可以自由选择或更改前端或后端的技术栈,而不需要对整个应用架构进行大规模的更改。
性能优化:
分别优化前端和后端的性能。例如,可以在内容交付网络 (CDN) 上托管静态前端资源,而后端则专注于API响应。
坏处
- heroku 单独部署两个项目分别收费不划算
- 跨域问题:前后端分别部署可能需要处理跨源资源共享 (CORS) 问题,这需要在后端正确配置以允许来自前端的请求。
使用 docker 在 heroku 上整体部署 vue + django
step1: 将 vue 的文件整个挪到 django 项目根目录底下
-
注意,这时候 vue 不再需要
server.js
这个文件了,后面会详细说为什么。 -
vue 项目中的
Dockerfile
也可以删除,因为我们也不单独基于 vue 来构建项目了 -
移动后的文件目录如下,当然你也可以不这么放,但是你要能够保证按照后面的步骤能够准确对应到你放前端代码的位置,因为后面的操作比较细节,或者你可以等你后面的步骤熟练了再自己做调整。
. ├── Dockerfile ├── db.sqlite3 ├── google_credential.json ├── manage.py ├── readme.md ├── requirements.txt ├── staticfiles │ ├── admin │ └── staticfiles.json ├── validation_backend │ ├── __init__.py │ ├── __pycache__ │ ├── admin.py │ ├── asgi.py │ ├── consumers.py │ ├── migrations │ ├── models.py │ ├── routing.py │ ├── settings.py │ ├── tools.py │ ├── urls.py │ ├── views.py │ └── wsgi.py └── validation_front ├── README.md ├── babel.config.js ├── img.png ├── jsconfig.json ├── node_modules ├── package-lock.json ├── package.json ├── public ├── src ├── todo.md ├── vue.config.js
step2: 构建 dockerfile
# 第一阶段:构建 Vue.js 应用
FROM --platform=linux/amd64 node:14 as vue-build-stage
# 设置 Vue.js 项目的工作目录,这样设置是为了 docker 里面的目录和 local 真实的目录路径保持一致
WORKDIR /app/validation_front
# 拷贝 Vue.js 项目的 package.json 和 package-lock.json
COPY validation_front/package*.json ./
# 安装 Vue.js 项目依赖
RUN npm install
# 拷贝 Vue.js 项目文件
COPY validation_front/ .
# 构建 Vue.js 应用
RUN npm run build
# 第二阶段:构建 Django 应用
# 使用兼容 Django 项目的 Python 镜像
FROM --platform=linux/amd64 python:3.9
RUN apt-get update
# 安装项目依赖
RUN apt-get install -y portaudio19-dev
# 设置工作目录为 /app
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
# 复制剩余的 Django 项目文件到容器中,这样做是为了将环境安装部分放在上一个docker 的层,避免每次 push docker 都重新安装 package
COPY . .
# 判断 app 里面是不是有 static 和 templates,没有就创建
RUN test -d /app/templates || mkdir /app/templates
RUN test -d /app/static || mkdir /app/static
# 从 Vue.js 构建阶段拷贝静态文件到 Django 的静态文件目录
COPY --from=vue-build-stage /app/validation_front/dist /app/static/
# 将 Vue 的 index.html 移动到 Django 的模板目录
COPY --from=vue-build-stage /app/validation_front/dist/index.html /app/templates/
# 在 docker 内运行的端口(暴露端口)
EXPOSE 9000
# 设置环境变量
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# 运行 Django 服务
# CMD python manage.py runserver 0.0.0.0:$PORT
CMD daphne -p $PORT -b 0.0.0.0 validation_backend.asgi:application
-
我认为上面步骤中最重要的就是要通过 build 将 vue 的项目打包,打包好的内容 vue 会自动放到
validation_front/dist
文件夹里面,大约是这个样子(我这个图是本地 build 了一下给大家展示,真正在 docker 里面的和这个是一样的,只不过不可见):
-
然后我们需要在 docker 里面把
dist
这个文件夹里面所有的内容拷贝到 django 项目的根目录下面的static
文件夹里面去(dockerfile 中已经定义了这样的操作)。将 vue 中dist
的内容 copy 到django static
路径的原因是:如果前后端放在一起部署。那么 vue 项目就不再需要一个单独的server.js
而是把 django 项目直接当成backend
,因此,当有 request 进来的时候,django 需要结合 vue 已经写好的那些页面(静态资源)来给 user 返回内容让浏览器解析。 -
因此我们 需要让 django 和 vue 的东西适配起来,也就是让 django 能够定位到 vue 的静态资源。这个我们后面会着重讲
-
此外,如果你关注上面的 dockerfile 你就会发现
上文中有两处:
- 第一阶段中:
COPY validation_front/package*.json ./
之后又进行了COPY validation_front/ .
- 第二阶段中:
COPY requirements.txt .
之后又进行了COPY . .
上述两个操作中的第二步看上去都似乎包含了第一步的操作,那么这样做的道理是什么呢?
- 这是因为:
-
在 Dockerfile 中,这种 seemingly redundant 的 COPY 操作实际上是一种优化技术,称为 Docker 层的缓存利用。每个
RUN, COPY,
和ADD
指令在 Docker 构建过程中都会创建一个新层。Docker 使用这些层来缓存构建步骤,这样在后续构建中,如果没有检测到任何变化,它就可以重用现有的层。这大大加速了构建过程。 -
分步骤 COPY 的目的:
COPY validation_front/package*.json ./
和COPY requirements.txt .
这些命令是为了首先复制package.json
和package-lock.json
(对于 Vue.js 应用),以及requirements.txt
(对于 Django 应用)。 这样做的好处是,只要这些文件未发生变化,Docker 就可以使用现有的缓存层来重新安装依赖项,而无需从头开始。这可以节省大量时间,因为依赖项安装通常是构建过程中最耗时的部分之一。
-
step3: settings.py
- 因为我们需要用 django 来找到 vue 的静态资源(css, js, index.html)之后整合到一起发到前端,我们需要一个
templates
文件夹来存放index.html
,所以在settings.py
中设置好# 模板设置 TEMPLATES = [ { # ... 'DIRS': [os.path.join(BASE_DIR, 'templates')], # ... }, ]
step4: 更新 django url.py 并在 views.py 中呈现 Vue 应用
-
在 Django 的 urls.py 中添加一个路由来指向这个视图。
from django.urls import path from . import views urlpatterns = [ path('', views.index), # 其他路由... ]
-
在 views.py 中增加如下内容:
from django.shortcuts import render def index(request): return render(request, 'index.html')
-
上述增添的项目表示,当有人访问服务器地址的时候,会得到基于 vue 来构建的静态页面
index.html
,而 django 所做的处理将会嵌入在静态页面index.html
中,这也就是为什么你访问部署后的项目能够看到前端页面。 -
在 dockerfile 中我们也指定了
dist/index.html
会被复制到 django 的static/index.html
,而render(request, 'index.html')
也正是通过访问预先定义static
文件夹来找这个index.html
step5: heroku container 部署整体项目
heroku container:push web -a <heroku application name>
heroku container:release web -a <heroku application name>
step6: debug – 让 django 能找到 vue 的静态资源
-
按说完成上述的操作,前后端的相互协作应该已经没问题了,但是当我部署完成并打开 heroku 的 app 傻眼了,经过我的测试,后端正常运行了,但是前端的页面没有显示
-
这个问题我闹了好久,最后发现是 django 的 静态路径 和 vue 默认的导出设置不匹配导致的
-
这里我们再强调一下
settings.py
中的这部分内容STATIC_URL = '/static/' STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')] STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
-
首先 django 项目的默认静态资源路径定义为
base_dir/staticfiles
,也就是,当你执行python manage.py collectstatic
命令的时候, django 项目本身的哪些css, js, index.html
(注意,这不是 vue 的静态资源)会自动加载到base_dir/staticfiles
里面,但由于我们用vue
构造的前端,所以我们自然是需要 vue 自己build
出来的静态资源而不是 django 自己的静态资源,因此我们需要区分这两部分静态资源。而我们通过STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
定义了存放其他静态资源(这里可以是 vue 的静态资源)的文件夹 -
我通过找问题,发现问题出在 vue 生成的
index.html
中<!doctype html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><title>validation_front</title><script defer="defer" src="/js/chunk-vendors.0804f4a0.js"></script><script defer="defer" src="/js/app.893a5f38.js"></script><link href="/css/chunk-vendors.10dd4e95.css" rel="stylesheet"><link href="/css/app.a285b80a.css" rel="stylesheet"></head> <body><noscript><strong>We're sorry but validation_front doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript> <div id="app"></div></body></html>
-
可以看到这里面加载资源的
src
都是直接/js/...
或者/css/...
。 -
如果我们在 docker 中部署好了之后,也就是说他会直接从 docker 的根路径
/app/...
去找是否有js
和css
文件夹,但是很显然他找不到,因为现在这些文件夹都在/app/static
里面 -
在 Django 项目中,静态文件通常通过
STATIC_URL
设置的路径来提供。默认情况下,STATIC_URL
被设置为/static/
。这意味着如果 Vue 应用构建生成的静态文件路径是/css/somefile.css
,实际上 Django 会期望它们位于/static/css/somefile.css
。 -
那么问题就来了,如何让这个矛盾解决呢?其实有几种方案
调整 Vue 构建配置
-
修改 Vue 项目的构建配置,使其生成的静态资源路径与 Django 的
STATIC_URL
一致。这通常在 Vue 项目的vue.config.js
文件中配置。例如,您可以添加一个公共路径前缀/static/
:module.exports = { publicPath: process.env.NODE_ENV === 'production' ? '/static/' : '/' };
-
这样,构建后的静态资源路径将与 Django 预期的路径一致。
-
我最终的
vue.config.js
文件是:const { defineConfig } = require('@vue/cli-service') module.exports = defineConfig({ transpileDependencies: true, publicPath: process.env.NODE_ENV === 'production' ? '/static/' : '/' })
-
在这个配置中:
- publicPath 被设置为
/static/
当应用处于生产模式(process.env.NODE_ENV === 'production')
。
对于开发模式,publicPath 保持为根路径/
,以便于本地开发时资源可以被正确加载。
这样,当构建生产版本的 Vue 应用时(通常通过运行npm run build
),所有静态资源(CSS、JavaScript 等)将会在它们的 URL 前加上/static/
前缀。然后,当这些文件被部署到与 Django 集成的环境中时,这些路径将与 Django 预期的静态文件路径一致。
- publicPath 被设置为
那怎么判断是生产模式还是本地模式呢?
- 在大多数 JavaScript 和 Node.js 环境中,包括 Vue.js 项目,您可以通过检查
process.env.NODE_ENV
环境变量的值来判断当前是生产模式(production)还是开发模式(development)。这个环境变量通常在不同的构建命令或环境中被设置。- 开发模式:通常在运行本地开发服务器时(例如,当您使用
npm run serve
或vue-cli-service serve
),NODE_ENV 被设置为"development"
。这是默认的模式,用于本地开发和调试。 - 生产模式:当您构建应用以准备部署到生产环境时(例如,使用
npm run build
或vue-cli-service build
),NODE_ENV 被设置为"production"
。在生产模式下,Vue 会启用各种优化,包括压缩、最小化和更有效的代码分割
- 开发模式:通常在运行本地开发服务器时(例如,当您使用
在 Django 中处理静态文件引用
- 使用 Django 的模板语言动态生成静态文件路径。可以在
index.html
中使用 Django 的{% static %}
模板标签来正确引用静态文件。不过,这可能需要手动编辑 Vue 构建出的index.html
文件, 很显然我们使用 dockerfile 来流程化构建,因此这种手动改的方式并不适用