モダンなWebアプリ(Django, Next.js, Docker, AWS)を開発する #環境構築編

## イントロダクション
環境構築編では、docker-composeを使ってDjango, Next.js, MySQL, Nginxの環境を同時に立ち上げられるようにしていきます。
バックエンドはAWS ECSに、フロントエンドはVercelにデプロイする予定なので、そのことを前提に環境構築を進めていきます。

！この記事の内容は今後推敲し修正、追加していくつもりなので、内容の変更にご注意ください。

参考元の記事はこちらになります↓
[DockerでReact＋Django+Nginx＋MySQLの環境構築](https://qiita.com/greenteabiscuit/items/c40ba038703c9f33499b)

## 構築の流れ
* バックエンドの構築
djangoの構築
nginxの構築
mysqlの構築
docker-composeの設定
* フロントエンドの構築
Next.jsの構築
docker-composeの設定

## 環境構築
最終的なディレクトリ構成は以下のようになります。

```
Todo
├── backend
│   ├── api
│   └── nginx
├── frontend
│   └── app
├── mysql
├── mysql_volume
├── sql
└── docker-compose.yml
```

## Backendの構築
まずはフォルダーを作成していきます。以下のコマンドを実行してください。

```
mkdir Todo && cd Todo
mkdir backend
mkdir backend/api backend/nginx
mkdir frontend
mkdir frontend/app
mkdir mysql
mkdir mysql_volume
mkdir sql
touch docker-compose.yml
```
treeコマンドで確認できます。
```
tree .
```
treeをインストールしていない方(mac)は下記で実行できるようになります。
```
brew install tree
```

#### Djangoの構築
backend/apiにdjangoプロジェクトを作成していきます。
```
cd backend/api
touch .env Dockerfile.dev requirements.txt entrypoint.dev.sh
```
.envファイルにはDjangoのシークレットキーを記述します。今のところは以下で大丈夫です。
```:.env
SECRET_KEY='XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
DEBUG=False
```
続いて、開発環境用のpythonのDockerfileです。
```:Dockerfile.dev
# backend/api/Dockerfile.dev
FROM python:3.7  #1

ENV PYTHONDONTWRITEBYTECODE 1  #2
ENV PYTHONUNBUFFERED 1

WORKDIR /usr/src/app  #3

COPY ./entrypoint.dev.sh ./usr/src/app/  #4
COPY ./requirements.txt ./usr/src/app/
RUN python3 -m pip install --upgrade pip setuptools
RUN pip install -r ./usr/src/app/requirements.txt

RUN chmod +x ./usr/src/app/entrypoint.dev.sh  #5

COPY . ./usr/src/app/  #6

ENTRYPOINT ["sh", "entrypoint.dev.sh"]  #7

EXPOSE 8000  #8
```
内容の解説です。
#1　リモートのpythonイメージをインストール
#2　環境変数を設定
#3　コンテナ内の作業ディレクトリを決定
#4　ホスト内の各ファイルをコンテナ内の作業ディレクトリにコピー
#5　entrypoint.dev.shに権限を付与
（これが無いとECSデプロイ時の権限にエラーになります。）
#6　ホスト内のdjangoプロジェクトをコンテナ内の作業ディレクトリにコピー
#7　ENTRYPOINT（コンテナ起動時に実行したいコマンド）の指定
#8　Djangoで使用したいポートを指定


続いて、Djangoに必要なモジュールです。
```:requirements.txt
asgiref==3.2.10
certifi==2020.12.5
dj-database-url==0.5.0
dj-static==0.0.6
Django==3.1
django-cors-headers==3.4.0
django-templated-mail==1.1.1
djangorestframework==3.11.1
djangorestframework-simplejwt==4.6.0
djoser==2.0.3
PyJWT==2.0.0
python-decouple==3.3
pytz==2020.5
sqlparse==0.4.1
static3==0.7.0
gunicorn==20.0.4
python-dotenv==0.13.0
mysqlclient==2.0.2
```
entrypoint.dev.shの中身になります。
```:entrypoint.dev.sh
#!/bin/sh

gunicorn config.wsgi:application --bind 0.0.0.0:8000
exec "$@"
```
最初と最後はおまじないです。
真ん中はgunicornの設定で、のちにconfigというdjangoプロジェクトを作成するので、そのconfig内のwsgi.pyを指定している形になります。
--bindというのは、GunicornのListenポートとして待ち受けるポートであり、ネットワーク:ポートで記述します。0.0.0.0にbindすることで、どの環境からでもアクセスを受け付けられるようにしています。
今はgunicornの設定だけですが、ここに python manage.py makemigrationsなどを記述していきます。

#### Nginxの構築
backend/nginxにnginxを作成していきます。
```
cd ../nginx
touch Dockerfile.dev default.dev.conf
```
開発環境用のnginxのDockerfileです。
```:nginx/Dockerfile.dev
FROM nginx:1.17.4-alpine  #1

RUN rm /etc/nginx/conf.d/default.conf  #2
COPY default.dev.conf /etc/nginx/conf.d/default.dev.conf  #3

CMD /usr/sbin/nginx -g 'daemon off;' -c /etc/nginx/nginx.conf  #4
```
内容の解説です。
#1　リモートのnginxイメージをインストール
#2　nginxのデフォルト設定ファイルを削除
#3　代わりに自作したnginx設定ファイルをコンテナ内にコピー
#4　CMDでnginxを起動

default.dev.confの中身になります。
```:nginx/default.dev.conf
upstream django {
    server api:8000;
}

server {
    listen 80;
    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 /usr/src/app/static/;
    }
}
```

#### MySQLの構築
本番環境ではAWS RDSを使うので、ローカルのmysqlは使い捨てになります。
backend/mysqlにmysqlを作成していきます。
```
cd ../../mysql
touch Dockerfile my.cnf
```
mysqlのDockerfileです。
```:mysql/Dockerfile
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
```
ここの設定をいじることはないので、内容は省略します。

mysqlの設定ファイルです。
```: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フォルダに移動しinit.sqlを作成します。
```
cd ../sql
touch init.sql
```
init.sqlの中身です。
```:sql/init.sql
GRANT ALL PRIVILEGES ON test_todoList.* TO 'user'@'%';

FLUSH PRIVILEGES;
```
GRANTでuserにtest_todoListテーブルへの権限を与えています。
FLUSH PRIVILEGESでデータベース内のGRANTテーブルを再読込します。 mysqlはキャッシュに権限等を保持しており、この情報を最新化するためにFLUSH PRIVILEGESを実行します。

ここまででファイルはこのようになっているはずです。
```
cd ..
tree -a
.
├── backend
│   ├── api
│   │   ├── .env
│   │   ├── Dockerfile.dev
│   │   └── requirements.txt
│   └── nginx
│       ├── Dockerfile.dev
│       └── default.dev.conf
├── docker-compose.yml
├── frontend
│   └── app
├── mysql
│   ├── Dockerfile
│   └── mysql.cnf
├── mysql_volume
└── sql
    └── init.sql

9 directories, 9 files
```

#### docker-composeの設定
docker-composeの設定をしていきます。
```:docker-compose.yml
version: "3.7"

services:
  api:
    container_name: python-backend
    env_file: ./backend/api/.env
    build:
      context: ./backend/api/.
      dockerfile: Dockerfile.dev
    volumes:
      - ./backend/api:/usr/src/app/
      - static_volume:/usr/src/app/static # <-- bind the static volume
    stdin_open: true
    tty: true
    networks:
      - backend_network
    environment:
      - CHOKIDAR_USEPOLLING=true
      - DJANGO_SETTINGS_MODULE=config.local_settings
    depends_on:
      - db
  backend-server:
    container_name: nginx_backend
    build:
      context: ./backend/nginx/.
      dockerfile: Dockerfile.dev
    volumes:
      - static_volume:/usr/src/app/static # <-- bind the static volume
    ports:
      - "8080:80"
    depends_on:
      - api
    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:
```
内容が多いですが、慣れてしまえば簡単です。
大まかにapi, backend-server, dbの３つに分けられます。
それぞれpython, nginx, mysqlのdockerfileに該当しています。

contianer_name: コンテナの名前
env_file: envを指定
build: Dockerfile.devへのパスとファイルを指定
volumes: 後述
stdin_open: docker run -it {container_name}の-iにあたる設定
tty: docker run -it {container_name}の-tにあたる設定
networks: コンテナのネットワーク設定
environment: 環境変数等の設定
depends_on: 依存関係の設定(今回だとdbが起動した後にapiを起動させる)
ports: {ホストのポート}：{コンテナのポート}をつなぐ
command: コンテナ起動時に実行したいコマンドの指定(dockerfileのentrypointと同じ)

＊ docker run -it {container_name} bashはコンテナ内に入るためのコマンドです。
stdin_openやttyはそのために必要な設定です。
[docker-compose.yml個人的Tips集](https://qiita.com/yuki_0920/items/dc3f32667d004979cc5a)
stdin_openやtty、volumeについて、詳しくはこちらを参考にしてください。

ここまでの設定ができたら、いよいよdjangoプロジェクトとアプリを作成していきます。
```
docker-compose run --rm api sh -c "django-admin startproject config ."

docker-compose run --rm api sh -c "python manage.py startapp todo"
```
backend/api配下がこのような構成になると思います。
```
tree .
.
├── Dockerfile.dev
├── config
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── entrypoint.dev.sh
├── manage.py
├── requirements.txt
└── todo
    ├── __init__.py
    ├── admin.py
    ├── apps.py
    ├── migrations
    │   └── __init__.py
    ├── models.py
    ├── serializers.py
    ├── tests.py
    ├── urls.py
    └── views.py
 ```

アプリが動くかを確認します。
```
docker-compose up --build
```
localhost:8080にアクセスしてブラウザにdjangoのデフォルトページが表示されれば成功です。