DockerでReact + Django + Postgresの連携・SPA構築チュートリアル

ReactとDjango REST framework、Postgresで簡単なSPAアプリを作成しました。環境の準備からブラウザに表示させるまでをまとめます。

環境の用意

次のようなコンテナ環境を用意します。また、DockerとDocker Composeのインストールが必要です。

  • Front-end:Reactが乗っかったNode.jsのコンテナ。表側はこのコンテナで用意したSPAを表示させます。
  • Back-end:Django REST frameworkがインストールされたPythonのコンテナ。テンプレート機能は使わず、JSON形式のデータを返すWebAPIとして使います。
  • DB:PostgreSQLのコンテナ。

事前準備-ディレクトリ構成

下記のような構成でDockerfileとYMLファイル、ディレクトリを用意しました。

.
├── code/
│   ├── django_rest_api/
│   │   └── requirements.txt
│   └── django_web_front/
├── DockerfileNode
├── DockerfilePython
└── docker-compose.yml

docker-compose.yml

Postgres(db)、Python(rest_api)、Node(web_front)のコンテナをDocker Composeで一元管理するためのファイルです。

version: "3"

services:
  db:
    container_name: django_db
    image: postgres:11.2
    volumes:
      - django_data_volume:/var/lib/postgresql/data
  rest_api:
    container_name: django_rest_api
    build:
      context: .
      dockerfile: DockerfilePython
    volumes:
      - ./code/django_rest_api:/code
    tty: true
    ports:
      - 8000:8000
    depends_on:
      - db
  web_front:
    container_name: django_web_front
    build:
      context: .
      dockerfile: DockerfileNode
    volumes:
      - ./code/django_web_front:/code
    tty: true
    ports:
      - 3000:3000
    depends_on:
      - rest_api
volumes:
  django_data_volume:

PostgresはDockerイメージから直でコンテナを起動し、PythonとNodeはDockerfileでビルドしてからコンテナを起動します。

Pythonのコンテナは8000番を、Nodeは3000番をポートフォワードし、両方とも localhost でブラウザからアクセスできるようにします。

depends_onで、db→rest_api→web_frontという順に起動するよう指定しています。

また、Pythonのコンテナは ./code/django_rest_api ディレクトリを、Nodeは ./code/django_web_front をコンテナ側へマウントし、ファイルを共有しています。Postgresはデータが揮発しないようにData volumeをマウントしています。

DockerfileNode

NodeのイメージをビルドするためのDockerfileですが、作業ディレクトリを指定する以外はほとんど何もしていません。

FROM node:8
RUN mkdir /code
WORKDIR /code

DockerfilePython

PythonのイメージをビルドするためのDockerfileです。こちらもほぼ何もしていませんが、最終行のpipコマンドで、必要なライブラリをインストールしています。

FROM python:3.7
ENV PYTHONUNBUFFERED 1
RUN mkdir /code
WORKDIR /code
ADD . /code/
RUN pip install -r requirements.txt

requirements.txt

Pythonのライブラリをインストールするためのリストです。

Django==2.1.7
psycopg2==2.7.6
psycopg2-binary==2.7.7
djangorestframework==3.9.2
django-cors-headers==2.5.2
  • psycopg2psycopg2-binary: Postgresと連携するためのライブラリ
  • djangorestframework:Django REST frameworkのライブラリ
  • django-cors-headers: CORS(Cross-Origin Resource Sharing)に必要なサーバーヘッダーを処理するためのDjango用ライブラリ

django-cors-headers はクロスドメイン(異なるドメイン間)でのRequestを許可し、同一ドメインでのRequestのように処理できるようになるライブラリです。React(localhost:3000)からDjango(localhost:8000)のAPIを叩く際に必要となります。

コンテナの起動

↓docker-compose.ymlの置かれているディレクトリで docker-compose up します。

docker-compose up -d

web_frontrest_apidb の3つのコンテナが起動します。

DBのセットアップ

ここから、db→rest_api→web_frontという順番でセットアップしていきます。まずはdbから。

↓dbのコンテナに psql -U postgres コマンドを実行させます。

docker container exec -it django_db psql -U postgres

↓下記コマンドを順に投入し、DBを作成します。

CREATE DATABASE test_app;
CREATE USER admin WITH PASSWORD 'password';
GRANT ALL PRIVILEGES ON DATABASE test_app TO admin;

終わったら、 ctrl + d でコンテナから抜けます。

Djangoのセットアップ

次はDjangoのrest_apiの設定です。

プロジェクト・アプリケーションディレクトリの作成

↓まずDjangoのプロジェクトを作成します。 django_rest_api コンテナに対し、 django-admin startproject test_app コマンドを実行させています。

docker container exec -it django_rest_api django-admin startproject test_app

↓続いてDjangoのアプリケーションファイルを作成。

docker container exec -it django_rest_api mkdir test_app/webapi
docker container exec -it django_rest_api django-admin startapp webapi test_app/webapi

すると code/django_rest_api/ ディレクトリの中身は、次のような構成になってるかと思います。

code/django_rest_api/
├── requirements.txt
└── test_app
    ├── manage.py
    ├── test_app
    │   ├── __init__.py
    │   ├── settings.py
    │   ├── urls.py
    │   └── wsgi.py
    └── webapi
        ├── __init__.py
        ├── admin.py
        ├── apps.py
        ├── migrations
        ├── models.py
        ├── tests.py
        └── views.py

環境変数の設定

次は環境変数を設定します。 test_app/test_app/settings.py でいくつかのINSTALLED_APPSとMIDDLEWAREを有効にし、CORS_ORIGIN_WHITELISTを末尾に追加します。

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'webapi',#←追加
    'rest_framework',#←追加
    'corsheaders',#←追加
]

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',
    'corsheaders.middleware.CorsMiddleware',#←追加
]

.....

# ↓settings.pyの末尾に追加
CORS_ORIGIN_WHITELIST = (
    'localhost:3000/',
    'localhost:3000',
)

また、Postgresと接続できるように、DATABASESを次のように書き換えます。

DATABASES = {
    'default': {
    'ENGINE': 'django.db.backends.postgresql',
    'NAME': 'test_app',
    'USER': 'postgres',
    'HOST': 'db',
    'PORT': 5432,
  }
}

Vagrantなど、環境によってはホストの制限を変更する必要があります。

ALLOWED_HOSTS = ['*']
# 全てのアクセスを許可する設定。本番環境では使用しないこと。

モデルの作成

今回は名前とアドレス、メッセージ、作成日を記録できるテーブルを作成します。

webapi/models.py で下記のように定義します。

from django.db import models
class Profile(models.Model):
    name = models.CharField(max_length=64)
    email = models.EmailField()
    message = models.CharField(max_length=256)
    created_at = models.DateTimeField(auto_now_add=True)

↓次はシリアライザーを用意します。 webapi/serializers.py を作成し、下記のように定義します。これはDjango REST frameworkの機能で、モデルのデータをPythonのデータ型に変換して、JSONまたはXMLなどの形式に変換してくれます。

from rest_framework import serializers
from webapi.models import Profile
class ProfileSerializer(serializers.ModelSerializer):
    class Meta:
        model = Profile
        fields = ('id', 'name', 'email', 'message', 'created_at')

ここで一旦、DBをマイグレートしておきます。

docker container exec -it django_rest_api python test_app/manage.py makemigrations
docker container exec -it django_rest_api python test_app/manage.py migrate
docker container exec -it django_rest_api python test_app/manage.py createsuperuser

ルーティング

localhost:8000/api/profile/ へアクセスした時に、ページが表示されるようにします。

test_app/urls.py を次のようにします。

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('webapi.urls')),
]

path('', include('webapi.urls')), この行で webapi/urls.py を読み込んでいます。

↓次は読み込み元の webapi/urls.py を設定します。

from django.urls import path
from . import views
urlpatterns = [
    path('api/profile/', views.ProfileListCreate.as_view() ),
]

↓最後に webapi/views.py を設定します。

from webapi.models import Profile
from webapi.serializers import ProfileSerializer
from rest_framework import generics
class ProfileListCreate(generics.ListCreateAPIView):
    queryset = Profile.objects.all()
    serializer_class = ProfileSerializer

データの挿入

fixtures機能でDBにデータを流し込みます。

webapi/fixtures/profiles.json を作成して次のように設定。

[
    {
        "model": "webapi.profile",
        "pk": 1,
        "fields": {
            "name": "A子",
            "email": "a-ko@gmail.com",
            "message": "A子でーす",
            "created_at": "2019-01-01 00:00:00"
        }
    },
    {
        "model": "webapi.profile",
        "pk": 2,
        "fields": {
            "name": "B子",
            "email": "b-ko@gmail.com",
            "message": "B子でーす",
            "created_at": "2019-02-01 00:00:00"
        }
    },
    {
        "model": "webapi.profile",
        "pk": 3,
        "fields": {
            "name": "C子",
            "email": "c-ko@gmail.com",
            "message": "C子でーす",
            "created_at": "2019-03-01 00:00:00"
        }
    }
]

↓下記コマンドでデータを挿入

docker container exec -it django_rest_api python test_app/manage.py loaddata profiles

動作確認

サーバーを起動してブラウザでアクセスし、レスポンスが帰ってくるか確認します。

docker container exec -it django_rest_api python manage.py runserver 0:8000

localhost:8000/api/profile へアクセスすると、↓こんなページが返ってきます。

Reactのセットアップ

いよいよReactを使ってフロントサイドを作り込んでいきます。

↓それでは早速、Node.jsのコンテナでcreate-react-appを投入します。

docker container exec -it django_web_front npx create-react-app .

投入後、下記のようなファイル群が作成されます。

code/django_web_front/
├── README.md
├── node_modules/
├── package-lock.json
├── package.json
├── public/
├── src/
└── yarn.lock

↓次は必要なパッケージをインストールします。今回はAxiosとBulmaを使ってみました。

docker container exec -it django_web_front npm install axios bulma
  • Axios…ブラウザとnode.js で動作する Promise ベースのHTTPクライアント。簡単に扱えるのでAJAXライブラリとして人気。
  • Bulma…軽量なCSSフレームワーク。

public/index.html

public/index.html ファイルを下記のようにしました。最終的に <div class="container" id="root"></div> へReactで構築したDOMが出力される予定です。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="shortcut icon" href="%!P(MISSING)UBLIC_URL%!/(MISSING)favicon.ico" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, shrink-to-fit=no"
    />
    <link rel="manifest" href="%!P(MISSING)UBLIC_URL%!/(MISSING)manifest.json" />
    <title>React App</title>
  </head>
  <body>
    <nav class="navbar" role="navigation" aria-label="main navigation">
      <div class="container">
        <div class="navbar-brand">
          <a class="navbar-item" href="/">
            MY REACT-DJANGO TEST APP
          </a>

          <a role="button" class="navbar-burger burger" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample">
            <span aria-hidden="true"></span>
            <span aria-hidden="true"></span>
            <span aria-hidden="true"></span>
          </a>
        </div>
      </div>
    </nav>
    <div class="container" id="root"></div>
  </body>
</html>

src/index.js

src/index.js でBulmaをインポートします。

import 'bulma/css/bulma.min.css';

src/App.js

src/App.js を次のようにしました。Reactの習熟度が低いので大変でしたが、だんだんわかってきました。

import React, { Component } from 'react';
import './App.css';
import axios from 'axios';

class Form extends React.Component {
  render() {
    return (
      <form id="post-data" onSubmit={this.props.handleSubmit}>
        <div className="field">
          <div className="control">
            <input
              className="input"
              type="text" name="name"
              id="name"
              placeholder="Name"
              value={this.props.name}
              onChange={this.props.handleChange}
            />
          </div>
        </div>
        <div className="field">
          <div className="control">
            <input
              className="input"
              type="text"
              name="email"
              id="email"
              placeholder="Email"
              value={this.props.email}
              onChange={this.props.handleChange}
            />
          </div>
        </div>
        <div className="field">
          <div className="control">
            <textarea
              className="textarea"
              name="message"
              id="body"
              cols="10"
              rows="3"
              placeholder="Message"
              value={this.props.message}
              onChange={this.props.handleChange}>
            </textarea>
          </div>
        </div>
        <input
          className="button is-fullwidth is-primary is-outlined"
          type="submit"
          value="SEND POST"
        />
      </form>
    );
  }
}

class UserList extends React.Component {
  renderRow() {
    const listItems = this.props.users.map((u) =>
      <tr key={u.id}>
        <td>{u.id}</td>
        <td>{u.name}</td>
        <td>{u.email}</td>
        <td>{u.message}</td>
        <td>{u.created_at}</td>
      </tr>
    );
    return(
      listItems
    );
  }
  render() {
    return (
      <table className="table is-fullwidth">
        <thead>
          <tr>
            <th>Id</th>
            <th>Name</th>
            <th>Email</th>
            <th>Message</th>
            <th>Created at</th>
          </tr>
        </thead>
        <tbody>
          {this.renderRow()}
        </tbody>
      </table>
    );
  }
}

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      users: [],
      usersLength: 0,
      name: "",
      email: "",
      message: "",
    };
    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(event) {
    if (event.target.name === 'name') {
      this.setState({name: event.target.value});
    }
    else if (event.target.name === 'email') {
      this.setState({email: event.target.value});
    }
    else if (event.target.name === 'message') {
      this.setState({message: event.target.value});
    }
  }

  handleSubmit(event) {
    event.preventDefault();
    const { name, email, message } = this.state;
    const conf = {
      'name': name,
      'email': email,
      'message': message
    };
    axios.post("http://localhost:8000/api/profile/?format=json", conf)
    .then(response => {
      this.state.users.unshift(response.data);
      this.setState({
        users: this.state.users,
        usersLength: this.state.users.length,
      });
    })
    .catch((error) => {
      console.error(error);
    });
  }

  componentDidMount() {
    axios.get('http://localhost:8000/api/profile/?format=json')
    .then(response => {
      this.setState({
        users: response.data.reverse(),
        usersLength: response.data.length,
      });
    })
    .catch((error) => {
      console.log(error);
    });
  }

  render() {
    return (
      <div className="columns is-multiline">
        <div className="column is-6">
          <div className="notification">
            This is my react-django app.
          </div>
        </div>
        <div className="column is-6">
          <Form
            name={this.state.name}
            email={this.state.email}
            message={this.state.message}
            handleChange={this.handleChange}
            handleSubmit={this.handleSubmit}
          />
        </div>
        <div className="column is-12" id="user-table">
          <p>There are {this.state.usersLength} users.</p>
          <UserList
            users={this.state.users}
          />
        </div>
      </div>
    );
  }
}

export default App;

src/App.css

src/App.css を次のようにしました。と言ってもテーブルのスタイルを少しいじっただけですが。

#user-table {
  overflow: scroll;
}

最終動作確認

djangoとnode.jsのサーバーを起動し、アクセスできるか確認します。

↓djangoのサーバーを起動

docker container exec -it django_rest_api python test_app/manage.py runserver 0:8000

↓別のタブでターミナルを開いて、node.jsのサーバーを起動

docker container exec -it django_web_front npm start

localhost:3000 へアクセスすると。。!

まとめ

いい感じになりました。やったね!

ソースコードは下記GitHubリポジトリをご覧ください。dockerのエントリーポイントなども追加しています。

https://github.com/hodanov/react-django-postgres-sample-app