LoginSignup
8
11

More than 3 years have passed since last update.

DockerでDjango+Reactの環境を構築する

Posted at

目標

将来的にDocker 環境下で Django と React を連携させて Google Kubernetes Engine にデプロイしてポートフォリオを作りたい。
完全に独学で進めていますので間違いがあれば何卒ご指摘を。。

環境

Windows10 home で Docker Toolbox を使用しています。
DjangoRestFrameworkとReactで超簡単なTodoアプリケーションを作成します。
アプリケーションのコードはDJANGO for APIsのChapter3を参考にしています。

まずはローカルで始める

まずはローカルで Django と React の連携を ToDO アプリを作成しながら考えてみます。

ディレクトリを作成する

Docker Toolbox は Virtualbox を使って docker ホストを立てています。
コンテナのボリュームのマウントはC:Users/環境下がデフォルトで設定されているので、
Docker toolbox を使用する場合は User ディレクトリ下をおススメします。

# プロジェクトフォルダの作成
mkdir gke-django
cd gke-djagno
# ディレクトリを作成する
mkdir backend
mkdir frontend

Backend の開発を進める

backend は Django-rest-framework で API を作成します。
まずは backend から環境を作成してみます。

settings.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 .とすることで、
backend下にconfigというプロジェクトフォルダが作成されました。

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'

todo アプリを始める

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'  # 追加
]

model をつくって migration して admin に登録します。

# 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)

管理ユーザーを作成して admin にログインして 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 で登録した内容でログインしてみましょう。
Todos を 3 つほど登録しておきましょう。

djangorestframework をはじめる

最初にpipでインストールしたrestframeworkを使用できるように 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',

    # 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を作成します。

URLs

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())
]
Selializers

モデルインスタンスを json 形式へ変換するためのserializers.pyを追加します。

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 によって自動的に追加されます。

Views

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

router など設定できていませんが、とりあえずは Todo アイテムを API として使用できるようになりました。
開発サーバーでhttp://127.0.0.1:8000/api/にアクセスすると APIview を確認することができます。

ここまでは Django でよくあるローカル環境での開発です。

CORS

CORS(Cross-Origin Resource Sharing)は 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',
)
Tests

テストを書きます。

# 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'...

うまくいったようです。

Frontend の開発を進める

Nodejs は予めインストールしておきましょう。

$ 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 の Welcome ページが確認できます。

App.js

api のエンドポイントは以下のような形で api を返してくるので、これを意識しておきましょう。
まずは mock となるデータで試してみます。

[
  {
    "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にアクセスするとモックデータが表示されました。
これを backendから取得したデータで表示させたいです。

axios

frontend でリクエストを叩くには build-in の Fetch APIaxios を使う方法がありますが、
今回は 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;

これでローカル環境で frontend から backend へ api をたたいて todo リスト一覧を表示させることができました。

超超単純な形ですが一応は Django と React の連携が取れました。

次はこれを Docker 化していきたいと思います。

Docker 化を進める

frontend, backend それぞれに Dockerfile を作成して backend コンテナ、frontend コンテナを作成してみます。

まずは Docker-compose で立ち上げられるところまでを考えていきます。

Backend の Docker 化

Dockerfile を書く前に Django 側でやっておきたいことがいくつかあります。

# 静的ファイ用のディレクトリ
$ mkdir backend\static
# 静的ファイルを全部集めてstaticifilesディレクトリに集められ
$ python manage.py collectstatic

本来であればデータベースの内容や settings.py を local と production で分けたりしますが、
まずは現在の形をそのまま Docker 化できることを考えます。

Dockerfile を backend ディレクトリ内に作成します。

$ 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 で backend コンテナを起動できるようにしてます。

# 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/にアクセスすると backend コンテナの DRF ビューにアクセスすることができました。
DockerToolbox を使っている場合は docker ホストの IP アドレスでアクセスしてください。

Frontend の Docker 化

続いて frontend 側を Docker 化していきます。

参考ページ
Dockerizing a React App
Creating an app with Docker Compose, Django, and Create React App
Using Docker for Node.js in Development and Production

frontend は React で構築しています。これを Docker 化するにはどうしたら良いでしょうか。
backend と同じように frontend ディレクトリに 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

これで node コンテナ内に package.json を使って同じ環境を構築することができます。
docker-compose.yml に frontend サービスを追加します。

# 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

environment にCHOKIDAR_USEPOLLING=trueを追加することでイメージを再ビルドすることなく
ホットリローディングしてくれるようになります。

frontend に関しては 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 を確認します。

backend/settings.py

手元のブラウザから frontend コンテナ ⇒backend コンテナにアクセスするため、
CORS_ORIGIN_WHITELISTに docker ホスト IP を追加する必要があります。

# backend/settings.py

CORS_ORIGIN_WHITELIST = (
    'http://localhost:3000',
    'http://192.168.99.100:3000',  # 追加
)

frontend/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 up

docker-compose.yml のあるディレクトリ下で docker-compose up します。

$ docker-compose up --build

React のコンパイル完了には時間がかかります。

起動できたらhttp://localhost:3000にアクセスするとローカルで表示されていた内容が
再現されているはずです。

そしてGKEへ

できたら追加します。

8
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
11