LoginSignup
67
78

More than 3 years have passed since last update.

DockerでReact+Django+Nginx+MySQLの環境構築

Last updated at Posted at 2021-01-02

はじめに

Docker環境でコマンドを打つだけで一発でDjango,React,MySQLなどを立ち上げて開発をできるようにしたくて、今回の記事を書きました。

この記事は一から環境を構築することを目指していますが、完成形だけをみたいかたはこちらのGitHubからどうぞ:django-react-nginx-mysql-docker
(READMEに書いてあることを実行すれば、うまくいくはずです)

目標

  • 仮想環境などは一切使わず、終始dockerでプロジェクトの作成などを行う
  • docker-compose upでウェブ開発に必要なすべてのコンテナが立ち上がるようにする
  • 最終的にK8sにデプロイする(次回の記事になると思います)。

最初は仮想環境で立ち上げて、そのあとにdockerfileを作成して環境構築をできるようにする、という記事は多く見かけます。ですが私はvirtualenvとかyarnをローカルに入れるのが面倒なので、終始Dockerで全部プロジェクトの管理を行いたいと思います。

前提

  • Dockerインストール済み
  • docker-composeインストール済み
  • LinuxまたはMac(強めのCPUとメモリがあるのが好ましいです)

流れ

以下のように進めていきます。

  1. バックエンドとDBの構築
  2. バックエンドのAPIを実際に触ってみて、データを追加してみる
  3. フロントエンドの構築
  4. APIでデータを取得し、フロントにて表示してみる

使う技術

  • Docker (docker-compose)
  • django (django rest framework)
  • nginx
  • mysql
  • react
  • next
  • typescript

ルートディレクトリにフォルダ作成

ではまずフォルダの作成から始めます。
プロジェクトフォルダを作成して、その直下で以下のコマンドを打ちます。

$ mkdir backend
$ mkdir frontend
$ mkdir mysql
$ mkdir mysql_volume
$ mkdir sql
$ touch docker-compose.yml

以下のようになっているはずです。

$ tree
.
├── backend
├── docker-compose.yml
├── frontend
├── mysql
├── mysql_volume
└── sql

5 directories, 1 file

1. BackendとDBの構築

web-backnginxのフォルダを作成します。nginxweb-backは今後K8sにデプロイするときには同じポッドにしようと思っているので、このような構成になります。フロントのときのweb-frontnginxも同じです。

$ cd backend
$ mkdir web-back
$ mkdir nginx

web-backの用意

$ cd web-back
$ touch .env Dockerfile requirements.txt

.envはAPIのKEYなど、センシティブな情報を含むファイルです。今はシークレットキーなどはないので、とりあえずテキトーに埋めておきます。

backend/web-back/.env
SECRET_KEY='XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
DEBUG=False

Python環境のDockerfileです。

# backend/web-back/Dockerfile
# set base image
FROM python:3.7

# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# set work directory
WORKDIR /code

# install dependencies
COPY requirements.txt ./
RUN python3 -m pip install --upgrade pip setuptools
RUN pip install -r requirements.txt

# Copy project
COPY . ./

# Expose application port
EXPOSE 8000

pipでインストールするモジュールです。

backend/web-back/requirements.txt

asgiref==3.2.7
Django==3.0.5
django-cors-headers==3.2.1
djangorestframework==3.11.0
gunicorn==20.0.4
psycopg2-binary==2.8.5
python-dotenv==0.13.0
pytz==2019.3
sqlparse==0.3.1
mysqlclient==2.0.2

nginxの用意

nginxフォルダに入ってDockerfileとconfファイルを作成します。
今後デプロイするとき用にファイルを分けたいので、devを入れておいて区別できるようにします。

$ cd ../nginx
$ touch Dockerfile.dev default.dev.conf

nginxのDockerfileです。

backend/nginx/Dockerfile.dev
FROM nginx:1.17.4-alpine

RUN rm /etc/nginx/conf.d/default.conf
COPY default.dev.conf /etc/nginx/conf.d

nginxコンテナに回ってきた通信をすべてdjangoのコンテナに流すようにします。

backend/nginx/default.dev.conf
upstream django {
    server web-back:8000;
}

server {

    listen 80;

    location = /healthz {
        return 200;
    }

    location / {
        proxy_pass http://django;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_redirect off;
    }

    location /static/ {
        alias /code/staticfiles/;
    }
}

MySQLの用意

mysqlフォルダ直下にDockerfileとmy.cnfを作成します。

$ cd ../../mysql
$ touch Dockerfile my.cnf

mysqlのバージョンは8.0.0を使うことにします。

FROM mysql:8.0.0

RUN echo "USE mysql;" > /docker-entrypoint-initdb.d/timezones.sql &&  mysql_tzinfo_to_sql /usr/share/zoneinfo >> /docker-entrypoint-initdb.d/timezones.sql

COPY ./my.cnf /etc/mysql/conf.d/my.cnf

文字コードなどの設定をmy.cnfに書き込みます。

mysql/my.cnf
# MySQLサーバーへの設定
[mysqld]
# 文字コード/照合順序の設定
character_set_server=utf8mb4
collation_server=utf8mb4_bin

# タイムゾーンの設定
default_time_zone=SYSTEM
log_timestamps=SYSTEM

# デフォルト認証プラグインの設定
default_authentication_plugin=mysql_native_password

# mysqlオプションの設定
[mysql]
# 文字コードの設定
default_character_set=utf8mb4

# mysqlクライアントツールの設定
[client]
# 文字コードの設定
default_character_set=utf8mb4

sqlフォルダの用意

SQLフォルダに移動し、init.sqlを作成します。

$ cd ../sql
$ touch init.sql
sql/init.sql
GRANT ALL PRIVILEGES ON test_todoList.* TO 'user'@'%';

FLUSH PRIVILEGES;

docker-composeでバックエンドの立ち上げ

ここまででファイルは以下のようになっているはずです。

$ tree -a
.
├── backend
│   ├── nginx
│   │   ├── default.dev.conf
│   │   └── Dockerfile.dev
│   └── web-back
│       ├── Dockerfile
│       ├── .env
│       └── requirements.txt
├── docker-compose.yml
├── frontend
├── mysql
│   ├── Dockerfile
│   └── my.cnf
├── mysql_volume
└── sql
    └── init.sql

7 directories, 9 files

フロントエンドはまたあとでやるので、とりあえずバックエンドの立ち上げを行っていきます。以下のようにdocker-compose.ymlファイルを用意します。

docker-compose.yml
version: "3.7"

services:
  web-back:
    container_name: python-backend
    env_file: ./backend/web-back/.env
    build: ./backend/web-back/.
    volumes:
      - ./backend/web-back:/code/
      - static_volume:/code/staticfiles # <-- bind the static volume
    stdin_open: true
    tty: true
    command: gunicorn --bind :8000 config.wsgi:application
    networks:
      - backend_network
    environment:
      - CHOKIDAR_USEPOLLING=true
      - DJANGO_SETTINGS_MODULE=config.local_settings
    depends_on:
      - db
  backend-server:
    container_name: nginx_back
    build:
      context: ./backend/nginx/.
      dockerfile: Dockerfile.dev
    volumes:
      - static_volume:/code/staticfiles # <-- bind the static volume
    ports:
      - "8080:80"
    depends_on:
      - web-back
    networks:
      - backend_network
  db:
    build: ./mysql
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: todoList
      MYSQL_USER: user
      MYSQL_PASSWORD: password
      TZ: 'Asia/Tokyo'
    volumes:
      - ./mysql_volume:/var/lib/mysql
      - ./sql:/docker-entrypoint-initdb.d
    networks:
      - backend_network

networks:
  backend_network:
    driver: bridge
volumes:
  static_volume:

内容が多いので少し難しいですね。
今回はとりあえず動くものを作りたいので、意味については割愛させていただきます。
ではDjangoのプロジェクトを作成しましょう!まずはconfigというプロジェクトを作成します。

$ docker-compose run --rm web-back sh -c "django-admin startproject config ."
Creating backend_web-back_run ... done
etc.....

$ docker-compose run --rm web-back sh -c "python manage.py startapp todo"
Creating backend_web-back_run ... done

うまくいったら以下のようにconfigtodoが作成されているはずです。

$ tree
.
├── backend
│   ├── nginx
│   │   ├── default.dev.conf
│   │   └── Dockerfile.dev
│   └── web-back
│       ├── config
│       │   ├── asgi.py
│       │   ├── __init__.py
│       │   ├── settings.py
│       │   ├── urls.py
│       │   └── wsgi.py
│       ├── Dockerfile
│       ├── manage.py
│       ├── requirements.txt
│       ├── staticfiles
│       └── todo
│           ├── admin.py
│           ├── apps.py
│           ├── __init__.py
│           ├── migrations
│           │   └── __init__.py
│           ├── models.py
│           ├── tests.py
│           └── views.py
............................

開発環境用のsettingファイルの作成

開発環境と本番環境で設定ファイルを分けたいので、configフォルダにてlocal_setting.pyファイルを作成します。settings.pyの情報を引き継ぐようにして、データベースの情報だけここで塗り替えます。

config/local_settings.py

from .settings import *

DEBUG = True

ALLOWED_HOSTS = ['*']

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'todoList',
        'USER': 'user',
        'PASSWORD': 'password',
        'HOST': 'db',
        'PORT': '3306',
    }
}

これでビルドしてみましょう。

$ docker-compose up --build
Starting python-backend ... done
Starting nginx          ... done
Attaching to python-backend, nginx
python-backend | [2020-12-28 14:59:49 +0000] [1] [INFO] Starting gunicorn 20.0.4
python-backend | [2020-12-28 14:59:49 +0000] [1] [INFO] Listening at: http://0.0.0.0:8000 (1)
python-backend | [2020-12-28 14:59:49 +0000] [1] [INFO] Using worker: sync
python-backend | [2020-12-28 14:59:49 +0000] [10] [INFO] Booting worker with pid: 10

これでlocalhost:8080にアクセスしてみましょう。以下の画面が出てくるはずです。
8080ポートにアクセスすると、nginxが8000ポートに通信を流してくれます。それによってdjangoの提供してくれるページにアクセスできます。

Screenshot from 2021-01-02 22-23-38.png

マイグレーションの準備と実行

  • rest frameworkを使いたい
  • APIを操作するための管理画面ページを使いたい

上記がまだできていないので、ここではそのためのデータベースのマイグレーションの準備を行います。以下3つのファイルを編集していきます。

  • settings.py
  • todo/models.py
  • todo/admin.py

settings.pyを以下のように編集します。ついでにこの際にcorsの部分も追加し、あとからフロントエンドからバックエンドのAPIを呼び出せるようにしておきます。

config/settings.py
"""
Django settings for config project.
Generated by 'django-admin startproject' using Django 3.0.5.
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 = '1_vj5u9p3nm4fwufe_96e9^6li1htp9avbg8+7*i#h%klp#&0='

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

    # 3rd party
    'rest_framework',
    'corsheaders',

    # Local
    'todo.apps.TodoConfig',
]

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',
]

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'




# 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 = 'UTC'

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

# 追加
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.AllowAny',
    ]
}

CORS_ORIGIN_WHITELIST = (
    'http://localhost',
)

todoの中のmodels.pyを編集します。

todo/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

todoの中のadmin.pyを編集します。

todo/admin.py
from django.contrib import admin
from .models import Todo


admin.site.register(Todo)

これでマイグレーションを以下のように実行します。ついでにsuperuserも作成しておきます。パスワードなどは好きなように設定してください。

$ docker-compose run --rm web-back sh -c "python manage.py makemigrations"
Creating backend_web-back_run ... done
Migrations for 'todo':
  todo/migrations/0001_initial.py
    - Create model Todo

$ docker-compose run --rm web-back sh -c "python manage.py migrate"
Creating backend_web-back_run ... done
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions, todo
Running migrations:
  Applying contenttypes.0001_initial... OK
........

$ docker-compose run --rm web-back sh -c "python manage.py createsuperuser"
Creating backend_web-back_run ... done
ユーザー名 (leave blank to use 'root'):
メールアドレス: example@gmail.com
Password:root
.........

URLの設定

adminapiのページに飛べるように設定します。

backend/web-back/config/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('todo.urls'))  # 追加
]

todoでもURLの設定などを行わなければいけません。また、JSONに変換するserializerファイルも作成します。

backend/web-back/todo $ touch urls.py serializers.py
urls.py
from django.urls import path, include
from .views import ListTodo, DetailTodo

urlpatterns = [
    path('<int:pk>/', DetailTodo.as_view()),
    path('', ListTodo.as_view())
]
serializers.py
from rest_framework import serializers
from .models import Todo


class TodoSerializer(serializers.ModelSerializer):
    class Meta:
        model = Todo
        fields = ('id', 'title', 'body')

viewも編集します。

views.py
from django.shortcuts import render

# Create your views here.
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

2.APIを触ってデータを追加してみる。

もう一度走らせて、adminapiにアクセスする

このままではcssファイルなどが反映されないので、staticなファイルをまず整理してから立ち上げます。

$ cd backend/web-back
$ mkdir static
$ docker-compose run --rm web-back sh -c "python manage.py collectstatic"
Starting ... done

163 static files copied to '/code/staticfiles'.
$ docker-compose up

localhost:8080/adminは以下のようになります。先ほど作成したsuperuserでログインしましょう。

Screenshot from 2021-01-02 22-47-01.png

ログインしたらtodoの管理などができる画面に入ります。

Screenshot from 2021-01-02 22-47-51.png

こんな感じで追加しておきます。

Screenshot from 2021-01-02 22-48-40.png

これでlocalhost:8080/api/1に行くと、見つかります。

Screenshot from 2021-01-02 22-49-35.png

これで以下のことができるようになりました。

  • 管理画面へのログイン
  • APIでデータの取得

これでフロントエンドの構築を始めることができます。

mysql dbでも確認してみる

以下のようにコンテナに入って確認すると、たしかにデータが格納されています。

$ docker exec -it container_db bash
root@e34e5d2a20e1:/# mysql -u root -p

mysql> use todoList;

mysql> select * from todo_todo;
+----+-------------+--------------+
| id | title       | body         |
+----+-------------+--------------+
|  1 | do homework | finish maths |
+----+-------------+--------------+
1 row in set (0.00 sec)


(余談)テストファイルの作成と実行

今すぐ必要というわけではないですが、テストファイルの作成と実行も一通りここでやっておきます。以下のテストファイルを走らせます。

backend/web-back/todo/tests.py
from django.test import TestCase

# Create your tests here.
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')

テストをコンテナの中で走らせます。うまく通るはずです。

$ docker-compose run --rm web-back sh -c "python manage.py test"
Creating backend_web-back_run ... done
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.005s

OK
Destroying test database for alias 'default'...

3. フロントエンドの構築

それでは、フロントエンドのほうのnginxとreact+next.jsの環境を構築していきます。

$ cd frontend/
$ mkdir nginx web-front

$ cd nginx
$ touch Dockerfile.dev default.dev.conf wait.sh

以下のようなファイル構成にします。

$ cd ../
$ tree
.
├── nginx
│   ├── default.dev.conf
│   ├── Dockerfile.dev
│   └── wait.sh
└── web-front

2 directories, 3 files

以下の2つのファイルはバックエンドのときとほぼ同じです。

frontend/nginx/default.dev.conf

upstream react {
    server web-front:3000;
}

server {

    listen 80;

    location = /healthz {
        return 200;
    }

    location / {
        proxy_pass http://react;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_redirect off;
    }
    location /sockjs-node {
        proxy_pass http://react;
      proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    error_page 500 502 503 504    /50x.html;

    location = /50x.html {
        root    /usr/share/nginx/html;
    }
}
frontend/nginx/Dockerfile.dev
FROM nginx:1.17.4-alpine

RUN apk add --no-cache bash

COPY wait.sh /wait.sh

RUN chmod +x /wait.sh

CMD ["/wait.sh", "web-front:3000", "--", "nginx", "-g", "daemon off;"]

RUN rm /etc/nginx/conf.d/default.conf
COPY default.dev.conf /etc/nginx/conf.d

このまま進めると、reactのコンテナは毎回nginxより遅く立ち上がってしまい、nginxは接続エラーだと勘違いしてexitしてしまいます。それを阻止するために以下のシェルファイルを用意してnginxコンテナの立ち上げを遅らせます。こちらのファイルはvishnubob/wait-for-itのレポジトリからコピーしてきたものです。

wait.sh
#!/usr/bin/env bash
# Use this script to test if a given TCP host/port are available

WAITFORIT_cmdname=${0##*/}

echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }

usage()
{
    cat << USAGE >&2
Usage:
    $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args]
    -h HOST | --host=HOST       Host or IP under test
    -p PORT | --port=PORT       TCP port under test
                                Alternatively, you specify the host and port as host:port
    -s | --strict               Only execute subcommand if the test succeeds
    -q | --quiet                Don't output any status messages
    -t TIMEOUT | --timeout=TIMEOUT
                                Timeout in seconds, zero for no timeout
    -- COMMAND ARGS             Execute command with args after the test finishes
USAGE
    exit 1
}

wait_for()
{
    if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
        echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
    else
        echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout"
    fi
    WAITFORIT_start_ts=$(date +%s)
    while :
    do
        if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then
            nc -z $WAITFORIT_HOST $WAITFORIT_PORT
            WAITFORIT_result=$?
        else
            (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1
            WAITFORIT_result=$?
        fi
        if [[ $WAITFORIT_result -eq 0 ]]; then
            WAITFORIT_end_ts=$(date +%s)
            echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds"
            break
        fi
        sleep 1
    done
    return $WAITFORIT_result
}

wait_for_wrapper()
{
    # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692
    if [[ $WAITFORIT_QUIET -eq 1 ]]; then
        timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
    else
        timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
    fi
    WAITFORIT_PID=$!
    trap "kill -INT -$WAITFORIT_PID" INT
    wait $WAITFORIT_PID
    WAITFORIT_RESULT=$?
    if [[ $WAITFORIT_RESULT -ne 0 ]]; then
        echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
    fi
    return $WAITFORIT_RESULT
}

# process arguments
while [[ $# -gt 0 ]]
do
    case "$1" in
        *:* )
        WAITFORIT_hostport=(${1//:/ })
        WAITFORIT_HOST=${WAITFORIT_hostport[0]}
        WAITFORIT_PORT=${WAITFORIT_hostport[1]}
        shift 1
        ;;
        --child)
        WAITFORIT_CHILD=1
        shift 1
        ;;
        -q | --quiet)
        WAITFORIT_QUIET=1
        shift 1
        ;;
        -s | --strict)
        WAITFORIT_STRICT=1
        shift 1
        ;;
        -h)
        WAITFORIT_HOST="$2"
        if [[ $WAITFORIT_HOST == "" ]]; then break; fi
        shift 2
        ;;
        --host=*)
        WAITFORIT_HOST="${1#*=}"
        shift 1
        ;;
        -p)
        WAITFORIT_PORT="$2"
        if [[ $WAITFORIT_PORT == "" ]]; then break; fi
        shift 2
        ;;
        --port=*)
        WAITFORIT_PORT="${1#*=}"
        shift 1
        ;;
        -t)
        WAITFORIT_TIMEOUT="$2"
        if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi
        shift 2
        ;;
        --timeout=*)
        WAITFORIT_TIMEOUT="${1#*=}"
        shift 1
        ;;
        --)
        shift
        WAITFORIT_CLI=("$@")
        break
        ;;
        --help)
        usage
        ;;
        *)
        echoerr "Unknown argument: $1"
        usage
        ;;
    esac
done

if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then
    echoerr "Error: you need to provide a host and port to test."
    usage
fi

WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15}
WAITFORIT_STRICT=${WAITFORIT_STRICT:-0}
WAITFORIT_CHILD=${WAITFORIT_CHILD:-0}
WAITFORIT_QUIET=${WAITFORIT_QUIET:-0}

# Check to see if timeout is from busybox?
WAITFORIT_TIMEOUT_PATH=$(type -p timeout)
WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH)

WAITFORIT_BUSYTIMEFLAG=""
if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then
    WAITFORIT_ISBUSY=1
    # Check if busybox timeout uses -t flag
    # (recent Alpine versions don't support -t anymore)
    if timeout &>/dev/stdout | grep -q -e '-t '; then
        WAITFORIT_BUSYTIMEFLAG="-t"
    fi
else
    WAITFORIT_ISBUSY=0
fi

if [[ $WAITFORIT_CHILD -gt 0 ]]; then
    wait_for
    WAITFORIT_RESULT=$?
    exit $WAITFORIT_RESULT
else
    if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
        wait_for_wrapper
        WAITFORIT_RESULT=$?
    else
        wait_for
        WAITFORIT_RESULT=$?
    fi
fi

if [[ $WAITFORIT_CLI != "" ]]; then
    if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then
        echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess"
        exit $WAITFORIT_RESULT
    fi
    exec "${WAITFORIT_CLI[@]}"
else
    exit $WAITFORIT_RESULT
fi

docker-compose.ymlの編集

docker-compose.yml
version: "3.7"

services:
  web-back:
    container_name: python-backend
    env_file: ./backend/web-back/.env
    build: ./backend/web-back/.
    volumes:
      - ./backend/web-back:/code/
      - static_volume:/code/staticfiles # <-- bind the static volume
    stdin_open: true
    tty: true
    command: gunicorn --bind :8000 config.wsgi:application
    networks:
      - backend_network
    environment:
      - CHOKIDAR_USEPOLLING=true
      - DJANGO_SETTINGS_MODULE=config.local_settings
    depends_on:
      - db
  backend-server:
    container_name: nginx_back
    build:
      context: ./backend/nginx/.
      dockerfile: Dockerfile.dev
    volumes:
      - static_volume:/code/staticfiles # <-- bind the static volume
    ports:
      - "8080:80"
    depends_on:
      - web-back
    networks:
      - backend_network
  db:
    build: ./mysql
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: todoList
      MYSQL_USER: user
      MYSQL_PASSWORD: password
      TZ: 'Asia/Tokyo'
    volumes:
      - ./mysql_volume:/var/lib/mysql
      - ./sql:/docker-entrypoint-initdb.d
    networks:
      - backend_network

  web-front:
    image: node:14.13.1
    volumes:
      - ./frontend/web-front:/home/app/frontend
    ports:
      - 3000:3000
    working_dir: /home/app/frontend
    command: [bash, -c, yarn upgrade --no-progress --network-timeout 1000000 && yarn run dev]
    networks:
      - frontend_network
  frontend-server:
    container_name: nginx_frontend
    build:
      context: ./frontend/nginx/.
      dockerfile: Dockerfile.dev
    ports:
      - "80:80"
    depends_on:
      - web-front
    networks:
      - frontend_network
networks:
  backend_network:
    driver: bridge
  frontend_network:
    driver: bridge
volumes:
  static_volume:

これでファイルの用意はできました。

reactのプロジェクトの作成

docker-compose run --rm web-front sh -c "npx create-react-app ."

web-frontはnode_modulesを除くと以下のようにプロジェクトができているはずです。

$ tree web-front -I node_modules
web-front
├── package.json
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── logo192.png
│   ├── logo512.png
│   ├── manifest.json
│   └── robots.txt
├── README.md
├── src
│   ├── App.css
│   ├── App.js
│   ├── App.test.js
│   ├── index.css
│   ├── index.js
│   ├── logo.svg
│   ├── reportWebVitals.js
│   └── setupTests.js
└── yarn.lock

2 directories, 17 files

next.jsのための準備

必要なモジュールを今のうちに入れておきましょう。

docker-compose run --rm web-front sh -c "yarn add next axios"
docker-compose run --rm web-front sh -c "yarn add --dev typescript @types/react"

package.jsondevの項目を追加します。これがないとdevが見つからないといってエラーになります。

package.json
  "scripts": {
    "dev": "next dev", //追加
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },

pagesフォルダをsrcの下に作って、テキトーなtypescriptファイルをおいてみます。next.jsではpagesの下にページを置くことがルールとなっています。

pages/index.tsx
import { FC } from 'react'

export default (() => {
    return (
        <div>
            hello world
        </div>
    )
}) as FC

これでdocker-compose upしてみましょう。
フロントエンドにアクセスするときは、localhostだけで大丈夫です、ポート番号は必要ありません。hello worldと返されているのが見えるはずです。

Screenshot from 2021-01-02 23-27-19.png

4. APIのデータを取得して表示

  • index.tsxの編集
pages/index.tsx
import React, { FC, useEffect, useState } from 'react'
import axios, { AxiosInstance } from 'axios'

type Todo = {
    id: string
    title: String
    body: String
}

export default (() => {
    const [todos, setTodo] = useState<Todo[]>([])

    const getAPIData = async () => {
        let instance: AxiosInstance

        instance = axios.create({
            baseURL: 'http://localhost:8080',
        })

        try {
            const response = await instance.get('/api/')
            console.log(response?.data)
            const tododata = response?.data as Todo[]
            setTodo(tododata)
        } catch (error) {
            console.log(error)
        }
    }
    return (
        <div>
            hello world
            <button onClick={getAPIData}>click</button>
            {todos.map((item) => (
                <div key={item.id}>
                    <h1>{item.title}</h1>
                    <p>{item.body}</p>
                </div>
            ))}
        </div>
    )
}) as FC

localhostにアクセスすると、以下のようにボタンが現れると思います。

Screenshot from 2021-01-03 00-23-01.png

ボタンを押したらAPIを通してデータが取得されます。

Screenshot from 2021-01-03 00-23-07.png

CSSやらBootstrapなどを使っていないのでしょうもないものですが、一応フロントエンドとバックエンドで通信ができていることを確認できました!

とりあえずここまでにしておいて、今後K8sへのデプロイについての記事を書くかもしれません。

参考

ゼロからGKEにDjango+Reactをデプロイする(1)backendの開発 - Nginx + Django
wait-for-it.sh

67
78
4

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
67
78