はじめに
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とメモリがあるのが好ましいです)
流れ
以下のように進めていきます。
- バックエンドとDBの構築
- バックエンドのAPIを実際に触ってみて、データを追加してみる
- フロントエンドの構築
- 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-back
とnginx
のフォルダを作成します。nginx
とweb-back
は今後K8sにデプロイするときには同じポッドにしようと思っているので、このような構成になります。フロントのときのweb-front
とnginx
も同じです。
$ cd backend
$ mkdir web-back
$ mkdir nginx
web-backの用意
$ cd web-back
$ touch .env Dockerfile requirements.txt
.env
はAPIのKEYなど、センシティブな情報を含むファイルです。今はシークレットキーなどはないので、とりあえずテキトーに埋めておきます。
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でインストールするモジュールです。
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です。
FROM nginx:1.17.4-alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY default.dev.conf /etc/nginx/conf.d
nginxコンテナに回ってきた通信をすべてdjangoのコンテナに流すようにします。
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サーバーへの設定
[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
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
ファイルを用意します。
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
うまくいったら以下のようにconfig
とtodo
が作成されているはずです。
$ 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
の情報を引き継ぐようにして、データベースの情報だけここで塗り替えます。
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の提供してくれるページにアクセスできます。
マイグレーションの準備と実行
- rest frameworkを使いたい
- APIを操作するための管理画面ページを使いたい
上記がまだできていないので、ここではそのためのデータベースのマイグレーションの準備を行います。以下3つのファイルを編集していきます。
settings.py
todo/models.py
todo/admin.py
settings.py
を以下のように編集します。ついでにこの際にcors
の部分も追加し、あとからフロントエンドからバックエンドのAPIを呼び出せるようにしておきます。
"""
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
を編集します。
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
を編集します。
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の設定
admin
とapi
のページに飛べるように設定します。
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
from django.urls import path, include
from .views import ListTodo, DetailTodo
urlpatterns = [
path('<int:pk>/', DetailTodo.as_view()),
path('', ListTodo.as_view())
]
from rest_framework import serializers
from .models import Todo
class TodoSerializer(serializers.ModelSerializer):
class Meta:
model = Todo
fields = ('id', 'title', 'body')
viewも編集します。
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を触ってデータを追加してみる。
もう一度走らせて、admin
とapi
にアクセスする
このままでは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でログインしましょう。
ログインしたらtodoの管理などができる画面に入ります。
こんな感じで追加しておきます。
これでlocalhost:8080/api/1
に行くと、見つかります。
これで以下のことができるようになりました。
- 管理画面へのログイン
- 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)
(余談)テストファイルの作成と実行
今すぐ必要というわけではないですが、テストファイルの作成と実行も一通りここでやっておきます。以下のテストファイルを走らせます。
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つのファイルはバックエンドのときとほぼ同じです。
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;
}
}
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のレポジトリからコピーしてきたものです。
#!/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の編集
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.json
でdev
の項目を追加します。これがないとdev
が見つからないといってエラーになります。
"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の下にページを置くことがルールとなっています。
import { FC } from 'react'
export default (() => {
return (
<div>
hello world
</div>
)
}) as FC
これでdocker-compose up
してみましょう。
フロントエンドにアクセスするときは、localhost
だけで大丈夫です、ポート番号は必要ありません。hello world
と返されているのが見えるはずです。
4. APIのデータを取得して表示
- 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
にアクセスすると、以下のようにボタンが現れると思います。
ボタンを押したらAPIを通してデータが取得されます。
CSSやらBootstrapなどを使っていないのでしょうもないものですが、一応フロントエンドとバックエンドで通信ができていることを確認できました!
とりあえずここまでにしておいて、今後K8sへのデプロイについての記事を書くかもしれません。
参考
ゼロからGKEにDjango+Reactをデプロイする(1)backendの開発 - Nginx + Django
wait-for-it.sh