docker
单独部署一个 vue 的前端项目Procfile
和 runtime.txt
以及里面写什么内容,docker
来部署 django 项目,是使用 heroku container
,因此不需要设定 Procfile
和 runtime.txt
等文件,我们会将所有需要使用的指令都放在 Dockerfile
里面heroku create <应用名称>
# 使用兼容 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 .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 python manage.py runserver 0.0.0.0:$PORT
)时,命令会在shell中执行,这意味着可以进行环境变量展开。因此 $PORT
会被正确解析为 Heroku分配的端口值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 在复杂的生产环境中可能会受到限制。
此外,我们指定了 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 之前把这些依赖项先装了,按照自己的情况来
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'
STATIC_ROOT
和其他相关设置以确保静态文件能被正确收集和服务。ALLOWED_HOSTS = ['your-app-name.herokuapp.com']
这个我一般设置为:
ALLOWED_HOSTS = ['*']
INSTALLED_APPS = [
"daphne", # asgi 应用,如果不设置,默认为 wsgi 应用
"channels", # 它使服务器和客户端之间的双向通信成为可能,从而可以构建具有即时消息、通知和实时更新等功能的应用程序。
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
"" , # 只要你构建 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',
]
上面配置的 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:push web -a <heroku application name>
heroku container:release web -a <heroku application name>
前后端分离使得前端和后端可以独立开发和部署,这有助于提高团队的工作效率,同时降低了代码耦合性。
可以单独更新前端或后端,而不影响系统的其他部分。
前后端分别部署允许您为每部分选择最适合的托管和缩放策略。例如,您可能会根据流量模式对前端和后端进行不同的缩放。
通过将前端和后端分开,您可以在网络架构上实现更细粒度的安全控制。例如,可以限制对后端服务的访问,只允许来自特定前端的请求。
您可以自由选择或更改前端或后端的技术栈,而不需要对整个应用架构进行大规模的更改。
性能优化:
分别优化前端和后端的性能。例如,可以在内容交付网络 (CDN) 上托管静态前端资源,而后端则专注于API响应。
注意,这时候 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
# 第一阶段:构建 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 就可以使用现有的缓存层来重新安装依赖项,而无需从头开始。这可以节省大量时间,因为依赖项安装通常是构建过程中最耗时的部分之一。templates
文件夹来存放 index.html
,所以在 settings.py
中设置好# 模板设置
TEMPLATES = [
{
# ...
'DIRS': [os.path.join(BASE_DIR, 'templates')],
# ...
},
]
在 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
heroku container:push web -a <heroku application name>
heroku container:release web -a <heroku application name>
按说完成上述的操作,前后端的相互协作应该已经没问题了,但是当我部署完成并打开 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 项目的构建配置,使其生成的静态资源路径与 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/' : '/'
})
在这个配置中:
/static/
当应用处于生产模式(process.env.NODE_ENV === 'production')
。/
,以便于本地开发时资源可以被正确加载。npm run build
),所有静态资源(CSS、JavaScript 等)将会在它们的 URL 前加上 /static/
前缀。然后,当这些文件被部署到与 Django 集成的环境中时,这些路径将与 Django 预期的静态文件路径一致。那怎么判断是生产模式还是本地模式呢?
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 会启用各种优化,包括压缩、最小化和更有效的代码分割index.html
中使用 Django 的 {% static %}
模板标签来正确引用静态文件。不过,这可能需要手动编辑 Vue 构建出的 index.html
文件, 很显然我们使用 dockerfile 来流程化构建,因此这种手动改的方式并不适用