使用Docker构建Django+React的开发环境
目标
我希望未来能够在Docker环境中将Django和React协同工作,部署到Google Kubernetes Engine上并创建个人作品集。因为我完全是自学自进,所以若有任何错误,请不吝赐教。
环境
我正在使用Windows10 home上的Docker Toolbox。
我将使用DjangoRestFramework和React来创建一个超简单的Todo应用程序。
该应用程序的代码是参考了《Django for APIs》第三章。
首先在本地开始。
首先我们可以在本地创建一个 ToDo 应用程序,同时考虑 Django 和 React 的连接。
创建一个目录
Docker Toolbox使用Virtualbox创建Docker主机。
由于容器卷的挂载默认设置为C:Users/环境下,
所以建议在使用Docker Toolbox时使用User目录下的文件夹。
# プロジェクトフォルダの作成
mkdir gke-django
cd gke-djagno
# ディレクトリを作成する
mkdir backend
mkdir frontend
推进后端开发
首先,我们将使用Django-rest-framework来创建后端API。先从后端开始创建环境进行尝试。
配置文件.py
cd backend
# 仮想環境の作成
python -m venv venv
# 仮想環境の有効化
venv\Scripts\activate
# Pythonパッケージのインストール
python -m pip install --upgrade pip setuptools
python -m pip install django djangorestframework python-dotenv
# Djangoのプロジェクトを始める。
django-admin startproject config .
通过在backend目录下运行`django-admin startproject config .`命令,创建了一个名为config的项目文件夹在backend目录下。
我们将编辑config/settings.py。
首先只编辑基本事项。
因为SECRET_KEY将在.env中追加,所以请将其复制备份。
# config/settings.py
"""
Django settings for config project.
Generated by 'django-admin startproject' using Django 3.0.4.
For more information on this file, see
https://docs.djangoproject.com/en/3.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.0/ref/settings/
"""
import os
from dotenv import load_dotenv # 追加
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PROJECT_DIR = os.path.basename(BASE_DIR)
# .envの読み込み
load_dotenv(os.path.join(BASE_DIR, '.env')) # 追加
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv('SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ['*']
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'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',
]
ROOT_URLCONF = 'config.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')], # 変更
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'config.wsgi.application'
# Database
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Password validation
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/3.0/topics/i18n/
LANGUAGE_CODE = 'ja'
TIME_ZONE = 'Asia/Tokyo'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.0/howto/static-files/
STATIC_URL = '/static/'
# 開発環境下で静的ファイルを参照する先
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
# 本番環境で静的ファイルを参照する先
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
# メディアファイルpath
MEDIA_URL = '/media/''
在settings.py文件中创建一个名为.env的文件来进行引用。
# .envファイルの作成
type nul > .env
# .envにコピペしておいたSECRET_KEYを追加する
SECRET_KEY = '+_f1u^*rb8+%cn-4o*kjn_(15*wspz0*!c)@=ll08odexo88a4'
开始使用待办事项应用
python manage.py startapp todos
将应用程序添加到settings.py文件中。
# conig/settings.py
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'todos.app.TodosConfig' # 追加
]
制作模型并进行迁移,然后将其注册到管理员界面上。
# todos/models.py
from django.db import models
class Todo(models.Model):
title = models.CharField(max_length=200)
body = models.TextField()
def __str__(self):
return self.title
$ python manage.py makemigrations todos
Migrations for 'todos':
todos\migrations\0001_initial.py
- Create model Todo
$ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions, todos
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying sessions.0001_initial... OK
Applying todos.0001_initial... OK
# todos/admin.py
from django.contrib import admin
from .models import Todo
admin.site.register(Todo)
创建管理用户并登录为管理员,在待办事项中添加大约3个任务。
$ python manage.py createsuperuser
ユーザー名 (leave blank to use 'yourname'): yourname
メールアドレス: youraddress@mail.com
Password:
Password (again):
Superuser created successfully.
$ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
March 10, 2020 - 23:41:26
Django version 3.0.4, using settings 'config.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.
当你访问 http://127.0.0.1:8000/admin 时,会打开django-admin的登录页面,因此请使用createsuperuser注册信息进行登录。我们先注册好3个待办事项。
开始使用Django REST framework。
需要更新 config/settings.py 文件,以便能够使用最初通过 pip 安装的 restframework。
# config/settings.py
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# 3rd party
'rest_framework',
# Local
'todos.apps.TodosConfig',
]
# 追加
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.AllowAny',
]
}
rest_framework.permissions.AllowAny 是为了解除 django-rest-framework 隐式设置的默认设置 ‘DEFAULT_PERMISSION_CLASSES’ 而存在的。
对于这个设置我还不是很清楚,但是我会先继续前进。
创建todos/urls.py,todos/views.py,todos/serializers.py。
网址
通过在config/urls.py中添加每个应用程序的urls.py文件来进行配置。
# config/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('todos.urls'))
]
我會添加 todos/urls.py 程式。
$ type nul > todos\urls.py
# todos/urls.py
from django.urls import path
from .views import ListTodo, DetailTodo
urlpatterns = [
path('<int:pk>/', DetailTodo.as_view()),
path('', ListTodo.as_view())
]
序列化器
添加序列化程序(serializers.py)以将模型实例转换为JSON格式。
type nul > todos\serializers.py
# todos/serializers.py
from rest_framework import serializers
from .models import Todo
class TodoSerializer(serializers.ModelSerializer):
class Meta:
model = Todo
fields = ('id', 'title', 'body')
如果在 fields = (‘id’, ‘title’, ‘text’) 中不指定 id 为 PrimaryKey,Django 会自动添加。
观点
在使用Django Rest Framework创建views.py时,需要继承rest_framework.generics中的APIView类。
# todos/views.py
from django.shortcuts import render
from rest_framework import generics
from .models import Todo
from .serializers import TodoSerializer
class ListTodo(generics.ListAPIView):
queryset = Todo.objects.all()
serializer_class = TodoSerializer
class DetailTodo(generics.RetrieveAPIView):
queryset = Todo.objects.all()
serializer_class = TodoSerializer
虽然路由器等设备尚未进行配置,但现在可以通过 API 使用待办事项项了。您可以在开发服务器上访问 http://127.0.0.1:8000/api/ 来查看 API 视图。
这里是常见的Django本地开发环境。
CORS 的中文译名是跨域资源共享。
在React和Django协同工作时,当React启动在localhost:3000上时,需要让它与Django的API服务器localhost:8000进行JSON通信。请安装django-cors-headers插件。
python -m pip install django-cors-headers
只需要一种选项:
更新config/settings.py文件。
# config/settings.py
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# 3rd party
'rest_framework',
'corsheaders',
# Local
'todos.apps.TodosConfig',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMidddleware', # 追加
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
##################
# rest_framework #
##################
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.AllowAny',
]
}
CORS_ORIGIN_WHITELIST = (
'http://localhost:3000',
)
考试 shì)
我要做考试。
# todos/test.py
from django.test import TestCase
from .models import Todo
class TodoModelTest(TestCase):
@classmethod
def setUpTestData(cls):
Todo.objects.create(title="first todo", body="a body here")
def test_title_content(self):
todo = Todo.objects.get(id=1)
excepted_object_name = f'{todo.title}'
self.assertEqual(excepted_object_name, 'first todo')
def test_body_content(self):
todo = Todo.objects.get(id=1)
excepted_object_name = f'{todo.body}'
self.assertEqual(excepted_object_name, 'a body here')
$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.007s
OK
Destroying test database for alias 'default'...
好像进展顺利。
推进前端开发
让我们预先安装Node.js吧。
$ cd frontend
$ npx create-react-app .
$ yarn start
yarn run v1.22.0
$ react-scripts start
i 「wds」: Project is running at http://192.168.56.1/
i 「wds」: webpack output is served from
i 「wds」: Content not from webpack is served from
C:\--you-path--\gke-django-tutorial\frontend\public
i 「wds」: 404s will fallback to /
Starting the development server...
Compiled successfully!
You can now view frontend in the browser.
Local: http://localhost:3000
On Your Network: http://192.168.56.1:3000
Note that the development build is not optimized.
To create a production build, use yarn build.
我能够使用React开始前端项目。
当在浏览器中访问http:// localhost:3000时,您可以看到React的欢迎页面。
App.js 应用程序.js
api的终点会以以下的形式返回api,我们需要意识到这一点。
首先,我们将在模拟数据中进行尝试。
[
{
"id": 1,
"title": "test_title",
"body": "body of test_title"
},
{
"id": 2,
"title": "test_title2",
"body": "body of test_title2"
},
{
"id": 3,
"title": "test_title3",
"body": "body of test_title3"
}
]
// src/App.js
import React, { Component } from "react";
import axios from "axiso";
import "./App.css";
const list = [
{
id: 1,
title: "test_title",
body: "body of test_title"
},
{
id: 2,
title: "test_title2",
body: "body of test_title2"
},
{
id: 3,
title: "test_title3",
body: "body of test_title3"
}
];
class App extends Component {
constructor(props) {
super(props);
this.state = { list };
}
render() {
return (
<div>
{this.state.list.map(item => (
<div key={item.id}>
<h1>{item.title}</h1>
<p>{item.body}</p>
</div>
))}
</div>
);
}
}
export default App;
当访问 http://localhost:3000 时,显示了模拟数据。
我希望用从后端获取的数据来显示它。
axios 形就是「axios」
在前端进行请求时,可以使用内置的Fetch API或axios库来发送请求,但我们选择使用axios。
npm install axios --save
yarn start
我会修改 App.js。
// src/App.js
import React, { Component } from "react";
import axios from "axios";
import "./App.css";
class App extends Component {
state = {
todos: []
};
componentDidMount() {
this.getTodos();
}
getTodos() {
axios
.get("http://127.0.0.1:8000/api/")
.then(res => {
this.setState({ todos: res.data });
})
.catch(err => {
console.log(err);
});
}
render() {
return (
<div>
{this.state.todos.map(item => (
<div key={item.id}>
<h1>{item.title}</h1>
<p>{item.body}</p>
</div>
))}
</div>
);
}
}
export default App;
我们已经成功地在本地环境中通过前端调用后端的API,实现了待办事项列表的显示。
虽然形式非常简单,但总算成功完成了Django和React的整合。
我希望接下来可以将这个项目进行Docker化。
推进Docker化
我们会为前端和后端分别创建 Dockerfile,并试着创建了一个后端容器和一个前端容器。
首先我们来考虑一下可以通过Docker-compose启动的部分。
将后端进行Docker化。
在编写 Dockerfile 之前,Django 方面有几件事可以先做。
# 静的ファイ用のディレクトリ
$ mkdir backend\static
# 静的ファイルを全部集めてstaticifilesディレクトリに集められ
$ python manage.py collectstatic
虽然通常情况下会将数据库内容和settings.py在本地和生产环境中进行分离,但首先我们要考虑将现有形式直接转换为Docker化的可能性。
在backend目录中创建一个名为Dockerfile的文件。
$ type nul > backend\Dockerfile
# backend/Dockerfile
# set base image
FROM python:3.7
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# set work directory
RUN mkdir /code
WORKDIR /code
# install dependencies
COPY requirements.txt /code/
RUN python3 -m pip install --upgrade pip setuptools
RUN pip install -r requirements.txt
# Copy project
COPY . /code/
EXPOSE 8000
然后将 docker-compose.yml 文件放置在项目目录中,以便使用 docker-compose up 命令启动后端容器。
# docker-compose.yml
version: "3.7"
services:
backend:
build: ./backend/.
command: python /code/manage.py runserver 0.0.0.0:8000
volumes:
- ./backend:/code
ports:
- "8000:8000"
$ docker-compose up
使用本链接http://localhost:8000/api/,即可访问后端容器的 DRF 视图。
如果使用 DockerToolbox,请使用 Docker 主机的 IP 地址进行访问。
将前端代码容器化
接下来将前端部分进行Docker化。
参考页面:
Docker化React应用程序
使用Docker Compose、Django和Create React App创建应用程序
在开发和生产中使用Docker来运行Node.js
前端是用React构建的。如何将其Docker化比较好呢?
我们可以像后端一样,在前端目录中创建一个Dockerfile。
type nul > frontend\Dockerfile
# frontend/Dockerfile
FROM node:12.2.0-alpine
RUN mkdir /code
WORKDIR /code
# Install dependencies
COPY package.json /code/
COPY package-lock.json /code/
RUN npm install
# Add rest of the client code
COPY . /code/
EXPOSE 3000
您可以使用 package.json 在 node 容器内构建相同的环境。
将 frontend 服务添加到 docker-compose.yml 文件中。
# docker-compose.yml
version: "3.7"
services:
backend:
build: ./backend/.
volumes:
- ./backend:/code
ports:
- "8000:8000"
stdin_open: true
tty: true
command: python /code/manage.py runserver 0.0.0.0:8000
environment:
- CHOKIDAR_USEPOLLING=true
frontend:
build: ./frontend/.
volumes:
- ./frontend:/code
- /code/node_modules
ports:
- "3000:3000"
command: npm start
stdin_open: true
tty: true
environment:
- CHOKIDAR_USEPOLLING=true
- NODE_ENV=development
depends_on:
- backend
在将CHOKIDAR_USEPOLLING=true添加到环境变量之后,不需要重新构建图像,就可以实现热加载。
由于前端部分的 node_modules 目录非常庞大,因此在挂载或复制时会花费大量时间。
因此,我们将添加 .dockerignore 文件,以确保不在镜像构建中使用 node_modules。(对吗?)
$ type nul > frontend\.dockerignore
/node_modules
在执行 docker-compose up 之前
你现在可以使用docker-compose up来启动了,但是如果你正在使用docker-toolbox,那么你的端口转发主机名不是localhost。你需要将其更改为主机IP。使用docker-machine ls命令来查找你正在使用的主机IP。
后端/设置.py
在本地的浏览器上,为了从前端容器访问后端容器,需要将 Docker 主机的 IP 地址添加到 CORS_ORIGIN_WHITELIST。
# backend/settings.py
CORS_ORIGIN_WHITELIST = (
'http://localhost:3000',
'http://192.168.99.100:3000', # 追加
)
前端/src/App.js
API的终端点将是Docker主机的IP地址。在这里我们使用192.168.99.100:8000作为示例。
// src/App.js
import React, { Component } from "react";
import axios from "axios";
import "./App.css";
class App extends Component {
state = {
todos: []
};
componentDidMount() {
this.getTodos();
}
getTodos() {
axios
.get("http://192.168.99.100:8000/api/") //変更
.then(res => {
this.setState({ todos: res.data });
})
.catch(err => {
console.log(err);
});
}
render() {
return (
<div>
<h1>mother fucker!!?? </h1>
{this.state.todos.map(item => (
<div key={item.id}>
<h1>{item.title}</h1>
<p>{item.body}</p>
</div>
))}
</div>
);
}
}
export default App;
运行docker-compose
在包含docker-compose.yml文件的目录下运行docker-compose up。
$ docker-compose up --build
React编译需要花费一定的时间。
如果能够启动的话,访问http://localhost:3000,应该可以在本地看到与之前显示的内容相同。
然后将GKE
如果可以的话,我会添加的。