React + Django + Nginx + MySQL environment construction with Docker

Introduction

I wrote this article because I wanted to be able to launch Django, React, MySQL, etc. in one shot and develop it just by typing a command in the Docker environment.

This article aims to build an environment from scratch, but if you just want to see the finished version, go to GitHub here: django-react-nginx-mysql-docker (If you do what the README says, it should work)

Goal

--Create a project with docker from beginning to end without using any virtual environment --docker-compose up will bring up all the containers needed for web development --Finally deploy to K8s (I think it will be the next article).

There are many articles that start up in a virtual environment at first and then create a docker file so that you can build the environment. However, I find it troublesome to put virtualenv or yarn locally, so I would like to manage all the projects with Docker from beginning to end.

Premise

--Docker installed --docker-compose installed --Linux or Mac (preferably with a strong CPU and memory)

flow

We will proceed as follows.

  1. Backend and DB construction
  2. Try to touch the backend API and add data
  3. Building the front end
  4. Get the data with API and display it at the front desk

Technology to use

Create a folder in the root directory

Let's start by creating a folder. Create a project folder and type the following command directly under it.

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

It should look like this:

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

5 directories, 1 file

1. Backend and DB construction

Create folders for web-back and nginx. I'm planning to have nginx and web-back in the same pod when deploying to K8s in the future, so this is the configuration. The same is true for web-front and nginx at the front.

$ cd backend
$ mkdir web-back
$ mkdir nginx

Preparing web-back

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

.env is a file that contains sensitive information such as API KEY. I don't have a secret key right now, so I'll bury it in the textbook for the time being.

backend/web-back/.env


SECRET_KEY='XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
DEBUG=False

Dockerfile for Python environment.

# 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

This module is installed with 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

Preparation of nginx

Go into the nginx folder and create a Dockerfile and a conf file. I want to separate the files for future deployments, so I'll put a dev to make them distinguishable.

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

This is a Dockerfile for nginx.

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

Make sure that all communication that comes to the nginx container is sent to the django container.

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/;
    }
}

Prepare MySQL

Create Dockerfile and my.cnf directly under the mysql folder.

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

I will use 8.0.0 as the version of mysql.

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

Write settings such as character code to my.cnf.

mysql/my.cnf


#Settings for MySQL server
[mysqld]
#Character code/Collation settings
character_set_server=utf8mb4
collation_server=utf8mb4_bin

#Time zone setting
default_time_zone=SYSTEM
log_timestamps=SYSTEM

#Setting the default authentication plugin
default_authentication_plugin=mysql_native_password

#Setting mysql options
[mysql]
#Character code setting
default_character_set=utf8mb4

#mysql client tool settings
[client]
#Character code setting
default_character_set=utf8mb4

Prepare sql folder

Go to the SQL folder and create init.sql.

$ cd ../sql
$ touch init.sql

sql/init.sql


GRANT ALL PRIVILEGES ON test_todoList.* TO 'user'@'%';

FLUSH PRIVILEGES;

Launch backend with docker-compose

At this point, the file should look like this:

$ 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

I will do the front end again later, so I will start up the back end for the time being. Prepare the docker-compose.yml file as shown below.

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:

It's a little difficult because there is a lot of content. This time I want to make something that works, so I will omit the meaning. Now let's create a Django project! First, create a project called 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

If all goes well, you should have created config and todo as shown below.

$ 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
............................

Creating a setting file for the development environment

I want to separate the configuration file for the development environment and the production environment, so create a local_setting.py file in the config folder. The information in settings.py will be inherited, and only the database information will be repainted here.

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

Let's build with this.

$ 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

Now let's access localhost: 8080. You should see the following screen. When you access the 8080 port, nginx will send communication to the 8000 port. This will give you access to the pages provided by django.

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

Preparing and executing migration

--I want to use rest framework --I want to use the management screen page for operating the API

Since the above has not been done yet, we will prepare for database migration for that purpose. Edit the following three files.

Edit settings.py as follows. At this time, add the cors part so that the front end can call the back end API later.

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  #add to

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

# .Load env
load_dotenv(os.path.join(BASE_DIR, '.env'))  #add to

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

#Where to refer to static files in the development environment
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')] #add to

#Where to reference static files in production
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') #add to

#Media file path
MEDIA_URL = '/media/' #add to

#add to
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.AllowAny',
    ]
}

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

Edit models.py in todo.

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

Edit admin.py in todo.

todo/admin.py


from django.contrib import admin
from .models import Todo


admin.site.register(Todo)

Now run the migration as follows: Also create a superuser. Please set the password etc. as you like.

$ 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
username(leave blank to use 'root'):
mail address: [email protected]
Password:root
.........

URL settings

Set to jump to the admin and api pages.

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'))  #add to
]

You also have to set the URL for todo. It also creates a serializer file to convert to JSON.

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

Edit the view as well.

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. Touch the API to add data.

Run again to access admin and api

Since css files etc. are not reflected as it is, first organize static files and then start up.

$ 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 looks like this: Let's log in with the superuser created earlier.

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

After logging in, you will be taken to a screen where you can manage todos.

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

I will add it like this.

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

Now go to localhost: 8080/api/1 and you will find it.

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

You can now do the following:

--Login to the management screen --Getting data with API

Now you can start building the front end.

Check with mysql db

If you enter the container and check it as shown below, the data is certainly stored.

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


(Digression) Creating and executing a test file

It's not necessary right now, but it's all about creating and running test files here. Run the following test file.

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

Run the test inside the container. It should pass well.

$ 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. Building the front end

Now, let's build the environment of nginx and react + next.js on the front end.

$ cd frontend/
$ mkdir nginx web-front

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

The file structure is as follows.

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

2 directories, 3 files

The following two files are almost the same as in the backend.

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

If you proceed as it is, the react container will start up slower than nginx every time, and nginx will mistakenly think that it is a connection error and exit. To prevent it, prepare the following shell file and delay the startup of the nginx container. This file is a copy from the vishnubob/wait-for-it repository.

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

Edit 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:

The file is now ready.

Creating a react project

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

The web-front should have the following project except 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

Preparation for next.js

Get the modules you need now.

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"

Add the dev item in package.json. Without this, you will get an error saying that dev cannot be found.

package.json


  "scripts": {
    "dev": "next dev", //add to
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },

Create a pages folder under src and put a nice typescript file in it. In next.js, it is a rule to put pages under pages.

pages/index.tsx


import { FC } from 'react'

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

Now let's do docker-compose up. When accessing the front end, just localhost is fine, no port number is needed. You should see it return hello world.

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

4. Get and display API data

--Edit 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

When you access localhost, you will see a button like the one below.

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

When you press the button, the data will be retrieved through the API.

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

I don't think I'm using CSS or Bootstrap, but I was able to confirm that the front end and back end can communicate with each other!

For the time being, I may write an article about deploying to K8s in the future.

reference

Deploy Django + React from scratch to GKE (1) Backend development --Nginx + Django wait-for-it.sh

Recommended Posts

React + Django + Nginx + MySQL environment construction with Docker
React environment construction with Docker
Rails + MySQL environment construction with Docker
[Environment construction with Docker] Rails 6 & MySQL 8
Environment construction with Docker (Ubuntu20.04) + Laravel + nginx
Laravel + MySQL + phpMyadmin environment construction with Docker
Rails application development environment construction with Docker [Docker, Rails, Puma, Nginx, MySQL]
Build a development environment for Django + MySQL + nginx with Docker Compose
[Docker] Rails 5.2 environment construction with docker
virtulbox + vagrant + Docker + nginx + puma + MySQL Rails environment construction
Node.js environment construction with Docker Compose
Environment construction with Docker for beginners
Easy environment construction of MySQL and Redis with Docker and Alfred
Docker environment construction
GPU environment construction with Docker [October 2020 version]
Rails environment construction with Docker (personal apocalypse)
Laravel development environment construction with Docker (Mac)
Create Rails 6 + MySQL environment with Docker compose
Edit Mysql with commands in Docker environment
Create a MySQL environment with Docker from 0-> 1
Rails & React & Webpacker & MySQL Environment Construction Manual
"Rails 6 x MySQL 8" Docker environment construction procedure for sharing with teams
Spring Boot environment construction with Docker (January 2021 version)
Environment construction command memo with Docker on AWS
React Native (TypeScript) + Expo project construction with Docker
Wordpress local environment construction & development procedure with Docker
Environment construction of Rails5 + MySQL8.0 + top-level volumes with docker-compose
Build Rails (API) x MySQL x Nuxt.js environment with Docker
Database environment construction with Docker in Spring boot (IntellJ)
Build docker environment with WSL
Docker × Spring Boot environment construction
[Rails / MySQL] Mac environment construction
[Docker] postgres, pgadmin4 environment construction
How to build [TypeScript + Vue + Express + MySQL] environment with Docker ~ Express ~
[Portfolio] Manage site with laravel APP implementation (Docker environment construction)
Case that gave up environment construction of react with ubuntu
Build Couchbase local environment with Docker
Build a Node.js environment with Docker
Laravel + Docker Laradock usage environment construction
Build PlantUML environment with VSCode + Docker
Build Rails environment with Docker Compose
Create SolrCloud verification environment with Docker
Rails on Docker environment construction procedure
Create Laravel environment with Docker (docker-compose)
mysql doesn't start up with docker.
Update MySQL from 5.7 to 8.0 with Docker
Build docker + laravel environment with laradock
Check MySQL logs in Docker environment
Build apache7.4 + mysql8 environment with Docker (with initial data) (your own memo)
Ruby on Rails development environment construction with Docker + VSCode (Remote Container)
Procedure for building a Rails application development environment with Docker [Rails, MySQL, Docker]
PostgreSQL environment construction with Docker (from setup to just before development)
How to build Rails + Vue + MySQL environment with Docker [2020/09 latest version]
How to build [TypeScript + Vue + Express + MySQL] environment with Docker ~ Sequelize ~
I built a rails environment with docker and mysql, but I got stuck
Alert slack with alert manager in Docker environment
CentOS8 + Anaconda + Django development environment construction procedure
[Environment construction] Rails + MySQL + Docker (Beginners can also use it in 30 minutes!)
[Docker] Use environment variables in Nginx conf
Building Rails 6 and PostgreSQL environment with Docker
Sapper × Go (echo) × Docker development environment construction