DockerでDjangoの開発環境を再構築！！！！

# はじめに

以前、こんな記事を書きました。
[Docker で Django の開発環境を構築！（Docker-compose/Django/postgreSQL/nginx）](https://qiita.com/nokonoko_1203/items/242367a83c313a5e46bf)

元記事はだいぶ昔（1年くらい？）に書いたものですが、今では知見が溜まりもうちょっと良い感じの構成に出来るんじゃないかなーと思ってチャレンジしてみることにしました！

Dockerのインストールなどの細かい話はしないので、気になる方は元記事を見てみてくださいね！
([こちら](https://qiita.com/zembutsu/items/dd2209a663cae37dfa81)なんかが参考になると思います。)

また、以前の記事と明確に異なる点として、DjangoはAPI配信サーバーとして考え、フロントエンドの実装にはVue.jsやReactを想定しているので、静的ファイルの配信やtemplateの設定などは行わない点と、`docker-compose.yml`と`docker-compose.yml.prod`を作って本番環境と開発環境を分ける…みたいなことはやっていないのでご了承ください。

## 目次

<!-- TOC -->

- [目次](#目次)
- [ディレクトリ構成](#ディレクトリ構成)
- [Django環境の構築](#django環境の構築)
- [PostgreSQL環境の構築](#postgresql環境の構築)
- [docker-composeを利用してみる](#docker-composeを利用してみる)
- [nginx環境の構築](#nginx環境の構築)
- [docker-compose.ymlにnginxを追加する](#docker-composeymlにnginxを追加する)

<!-- /TOC -->

## ディレクトリ構成

最終的に、こんな感じのディレクトリ構成になりました！

```shell-session
backend
└── containers
    ├── django
    │   ├── Dockerfile
    │   ├── Pipfile
    │   ├── Pipfile.lock
    │   ├── config
    │   │   ├── __init__.py
    │   │   ├── settings.py
    │   │   ├── urls.py
    │   │   └── wsgi.py
    │   ├── entrypoint.sh
    │   ├── manage.py
    ├── docker-compose.yml
    ├── nginx
    │   ├── Dockerfile
    │   └── nginx.conf
    └── postgres
        ├── Dockerfile
        └── sql
            └── init.sql
```

backendディレクトリ以下のcontainersディレクトリに各コンテナを配置しています。

前回の記事と比較してpostgresディレクトリが追加されたりしています。

では早速、一番上のdjangoディレクトリから構成していきましょう！

## Django環境の構築

まずbackendとcontainersとdjangoの3つのディレクトリを作成していきましょう！そしてdjangoディレクトリに移動します。

```shell-session
$mkdir -p backend/containers/django
$cd backend/containers/django
```

まずはdjangoディレクトリ内にPipfileを作成しましょう。各パッケージのバージョンは本記事執筆時点最新版であり、特にこだわりはありません。ご自由に変更ください。（動作は保証していません）

また、インストールするパッケージは任意であり環境構築のみが目的であれば、`django`と`gunicorn`と`django-environ`のみが入っていれば問題ありません。

```ini:backend/containers/django/Pipfile
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true

[dev-packages]

[packages]
django = "==3.0.6"
djangorestframework = "==3.11.0"
djangorestframework-simplejwt = "==4.4.0"
djangorestframework-gis = "==0.15"
django-cors-headers = "==3.2.1"
django-environ = "==0.4.5"
djoser = "==2.0.3"
gunicorn = "==20.0.4"
psycopg2-binary = "==2.8.5"

[requires]
python_version = "3.8.2"
```

pipenvで仮想環境をpython3.8系で作成していき、仮想環境に入ってバージョンとインストール済みのパッケージを確認していきましょう。

```shell-session
$pipenv install
$pipenv shell
(django) $python -V
Python 3.8.2
(django) $pip list
Package                       Version
----------------------------- -------
asgiref                       3.2.7
Django                        3.0.6
django-cors-headers           3.2.1
django-environ                0.4.5
django-templated-mail         1.1.1
djangorestframework           3.11.0
djangorestframework-gis       0.15
djangorestframework-simplejwt 4.4.0
djoser                        2.0.3
gunicorn                      20.0.4
pip                           20.0.2
psycopg2-binary               2.8.5
PyJWT                         1.7.1
pytz                          2020.1
setuptools                    46.1.3
sqlparse                      0.3.1
wheel                         0.34.2
```

シェルの左端に`(django)`の文字が表示されていれば仮想環境に入れています。(簡略化のため次回以降は`(django)`の文字列を記載しません)

ちゃんとDjangoがインストールされていますね！

djangoディレクトリにいるのを確認したら、Djangoのコマンドを利用してプロジェクトを作成します。

```console
$django-admin startproject config .
```

これでDjangoプロジェクトの作成完了です。configディレクトリと`manage.py`という名前のファイルがdjangoディレクトリに作成されているのが確認できるはずです。

この時点で`python manage.py runserver localhost:8000`コマンドでデバッグサーバーを起動し、ブラウザから`loaclhost:8000`に接続すると親の顔よりも見たあの画面が表示されているはずです。

<img width="837" alt="スクリーンショット 2020-05-07 17.16.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/45948982-0da9-afea-049b-9d6c31cb11b8.png">

お次にDjango周りのファイルを以下のように修正していきましょう。

SECRET_KEYについてはあとで利用するのでコメントアウトするなどしておいてください。

```python:backend/containers/django/config/settings.py
import os
from datetime import timedelta
import environ

env = environ.Env()
env.read_env('.env')

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

SECRET_KEY = env.get_value('SECRET_KEY')

DEBUG = env.get_value('DEBUG')

ALLOWED_HOSTS = ['localhost', '127.0.0.1']

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'rest_framework',
    'rest_framework_gis',
    'corsheaders',
    'django.contrib.gis',
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
    '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',
]

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'

DATABASES = {
    'default': {
        'ENGINE': env.get_value('DATABASE_ENGINE', default='django.db.backends.sqlite3'),
        'NAME': env.get_value('DATABASE_DB', default=os.path.join(BASE_DIR, 'db.sqlite3')),
        'USER': env.get_value('DATABASE_USER', default='django_user'),
        'PASSWORD': env.get_value('DATABASE_PASSWORD', default='password'),
        'HOST': env.get_value('DATABASE_HOST', default='localhost'),
        'PORT': env.get_value('DATABASE_PORT', default='5432'),
    }
}

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

LANGUAGE_CODE = 'ja'

TIME_ZONE = 'Asia/Tokyo'

USE_I18N = True

USE_L10N = True

USE_TZ = True

STATIC_URL = '/static/'
STATICFILES_DIRS = (os.path.join(BASE_DIR, 'static'),)

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
}

SIMPLE_JWT = {
    'AUTH_HEADER_TYPES': ('JWT',),
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30),
}

CORS_ORIGIN_ALLOW_ALL = False
CORS_ORIGIN_WHITELIST = (
    'http://localhost:8080',
    'http://127.0.0.1:8080',
)
```

最初にPipfileを作成したときに`django-environ`というパッケージを追加しましたが、これはSECRET_KEYやDBへの接続情報など、隠しておきたい情報を`.env`ファイルなどに記載し、ソースコードとは別に管理するためのパッケージで、`setting.py`などから

```python
env = environ.Env()
env.read_env('.env')
```

のように利用します。

早速`.env`ファイルを作成していきましょう。

```env:backend/containers/django/.env
DEBUG=True
SECRET_KEY=<YOUR_SECRET_KEY>
DATABASE_ENGINE=django.contrib.gis.db.backends.postgis
DATABASE_DB=<YOUR_DB_NAME>
DATABASE_USER=<YOUR_DB_USER>
DATABASE_PASSWORD=<YOUR_DB_PASSWORD>
#entrypoint.shで利用
#compose.ymlに記載のサービス名で名前解決してくれる
DATABASE_HOST=postgres
DATABASE_PORT=5432
DATABASE=postgres
```

- <YOUR_SECRET_KEY>:setting.pyに元々記載されていたKEY
- <YOUR_DB_NAME>(USER/PASSWORD):任意のもの入れてください。詳しくはPostgreSQLコンテナを立ち上げる際に説明します

あとはゴリゴリと関連するファイルを直していきます。backendディレクトリからのパスも記載するので、適宜必要なファイルの作成と追記・修正を行っていきます。

```Dockerfile:backend/containers/django/Dockerfile
# ubuntuのイメージをプルし、Pythonをインストールしていく
FROM ubuntu:20.04

SHELL ["/bin/bash", "-c"]

# pythonをインストール
RUN apt-get update -y \
    && apt-get upgrade -y \
    && apt-get install -y python3.8 python3.8-dev \
    && source ~/.bashrc \
    && apt-get -y install vim

# 作業ディレクトリを設定
WORKDIR /usr/src/app

# 環境変数を設定
# Pythonがpyc filesとdiscへ書き込むことを防ぐ
ENV PYTHONDONTWRITEBYTECODE 1
# Pythonが標準入出力をバッファリングすることを防ぐ
ENV PYTHONUNBUFFERED 1
ENV DEBIAN_FRONTEND=noninteractive

# 依存関係のインストールとpipenvをインストール
RUN apt-get install -y curl \
    && curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py \
    && apt-get install -y python3.8-distutils \
    && python3.8 get-pip.py \
    && pip install -U pip \
    && apt-get install -y build-essential libssl-dev libffi-dev python-dev python3-dev libpq-dev

# pipenvのインストール
RUN pip install pipenv

# ローカルマシンののPipfileをコンテナにコピー
COPY Pipfile ./

# Pipfile.lockを無視してPipfileに記載のパッケージをシステムにインストール
# その後、pipenvをアンインストール
RUN pipenv install --system --skip-lock \
    && pip uninstall -y pipenv virtualenv-clone virtualenv

# 地理空間ライブラリをインストールする際の依存関係
RUN apt-get update -y \
    && apt-get upgrade -y \
    && apt-get install -y libgeos-dev binutils libproj-dev gdal-bin libgdal-dev \
    && apt-get install -y python3-gdal

RUN apt-get install -y netcat \
    && apt-get install -y expect

# シェルスクリプトをコピー
# COPY ./entrypoint.sh /usr/src/app/entrypoint.sh

COPY . /usr/src/app/

# シェルスクリプトを実行
# ENTRYPOINT ["/usr/src/app/entrypoint.sh"]
```

この段階でコンテナが動作するかどうか確認してみましょう。

```shell-session
$docker build . -t pipenv_sample
...
$docker run -it pipenv_sample
root@e6bdfb335bee:/usr/src/app#
```

このようにコンテナにrootユーザーでログインできれば、おそらく成功です！

```shell-session
# python3 -V
Python 3.8.2
# pip list
Package                       Version
----------------------------- ----------
appdirs                       1.4.3
asgiref                       3.2.7
certifi                       2020.4.5.1
distlib                       0.3.0
Django                        3.0.6
django-cors-headers           3.2.1
django-environ                0.4.5
django-templated-mail         1.1.1
djangorestframework           3.11.0
djangorestframework-gis       0.15
djangorestframework-simplejwt 4.4.0
djoser                        2.0.3
filelock                      3.0.12
GDAL                          3.0.4
gunicorn                      20.0.4
numpy                         1.17.4
pip                           20.1
pipenv                        2018.11.26
psycopg2-binary               2.8.5
PyJWT                         1.7.1
pytz                          2020.1
setuptools                    46.1.3
six                           1.14.0
sqlparse                      0.3.1
virtualenv                    20.0.20
virtualenv-clone              0.5.4
wheel                         0.34.2
```

Pythonのバージョンは3.8.2になっていますし、パッケージがちゃんとインストールされていますね！

`Control + d`でログアウトしましょう。

最後に、起動時に実行するpostgresコンテナとの接続用shellスクリプトを書いて保存しましょう。

```sh:backend/containers/django/entrypoint.sh
#!/bin/sh

if [ "$DATABASE" = "postgres" ]
then
    echo "Waiting for postgres..."

    while ! nc -z $DATABASE_HOST $DATABASE_PORT; do
      sleep 0.1
    done

    echo "PostgreSQL started"
fi

exec "$@"
```

ついでに、上で編集した Dockerfile の最終行付近にある 2 つのコメントアウトを外してください。

```Dockerfile:backend/containers/django/Dockerfile
# これ
COPY ./entrypoint.sh /usr/src/app/entrypoint.sh

COPY . /usr/src/app/

# これ
ENTRYPOINT ["/usr/src/app/entrypoint.sh"]
```

ひとまず、Djangoディレクトリについては以上です。

- ディレクトリ構成

```shell-session:tree
backend
└── containers
    ├── django
    │   ├── Dockerfile
    │   ├── Pipfile
    │   ├── Pipfile.lock
    │   ├── config
    │   │   ├── __init__.py
    │   │   ├── settings.py
    │   │   ├── urls.py
    │   │   └── wsgi.py
    │   ├── entrypoint.sh
    │   ├── manage.py
    └── docker-compose.yml
```

## PostgreSQL環境の構築

次はPostgreSQLコンテナを立ち上げていきます。

まずは、containersディレクトリに移動してpostgresディレクトリなどを作成しましょう。

```shell-session
$cd ../../
$mkdir -p postgres/sql/
```

これで、postgresディレクトリとその中にsqlディレクトリができたと思います。

postgresディレクトリの中にDockerfileを追加していきましょう。

```Dockerfile:backend/containers/postgres/Dockerfile
FROM mdillon/postgis:11

RUN localedef -i ja_JP -c -f UTF-8 -A /usr/share/locale/locale.alias ja_JP.UTF-8
ENV LANG ja_JP.UTF-8
```

僕の場合、作成したいのは地図アプリケーションだったので、PostgreSQLの拡張機能であるPostGISのイメージを利用していきますが、任意のPostgreSQLイメージで構わないと思います。（動作は検証はしていません）

次に、sqlディレクトリ内にコンテナ起動時に実行したいsqlファイルを格納しましょう。

```sql:backend/containers/postgres/sql/init.sql
CREATE EXTENSION postgis;
```

今回は拡張機能を有効にするためのsqlファイルだけ格納しましたが、初期登録したいデータなどがあればそれをご自由に格納してください。

最後に、.env_dbファイルを追加していきます。

この中には、Djangoコンテナを作成したときに記載した、<YOUR_DB_NAME>(USER/PASSWORD)と同じものを書いてください。

ここに記載した内容で、DBが自動作成されます。

```env:backend/containers/postgres/.env_db
#envに書いておけば自動でDBが作成される
POSTGRES_DB=<YOUR_DB_NAME>
POSTGRES_USER=<YOUR_DB_USER>
POSTGRES_PASSWORD=<YOUR_DB_PASSWORD>
```

以上で、postgres環境の構築終了です。

- ディレクトリ構成

```shell-session
backend
└── containers
    ├── django
    │   ├── Dockerfile
    │   ├── Pipfile
    │   ├── Pipfile.lock
    │   ├── config
    │   │   ├── __init__.py
    │   │   ├── settings.py
    │   │   ├── urls.py
    │   │   └── wsgi.py
    │   ├── entrypoint.sh
    │   ├── manage.py
    ├── docker-compose.yml
    └── postgres
        ├── Dockerfile
        └── sql
            └── init.sql
```

## docker-composeを利用してみる

docker-composeは複数のコンテナを同時起動させたり、それらを接続する際に利用する便利ツールです。

早速、containersディレクトリに移動し、設定ファイルを作っていきましょう。

```shell-session
$cd ../
$touch docker-compose.yml
```

```yml:backend/containers/docker-compose.yml
version: "3.7"
services:
  django:
    # コンテナ名
    container_name: django
    # ビルドするdockerファイルが格納されたディレクトリ
    build: ./django
    # 正常起動後に実行するコマンド
    command: python3 manage.py runserver 0.0.0.0:8000
    volumes:
      # マウントするディレクトリ
      - ./django:/usr/src/app/
    ports:
      # ホスト側のポート：コンテナ側のポート
      - 8000:8000
    env_file:
      # 環境変数に設定するファイル
      - ./django/.env
    depends_on:
      # 接続するサービス
      - postgres

  postgres:
    container_name: postgres
    build: ./postgres
    volumes:
      # DBのデータはボリュームを作成して保存
      # ディレクトリとマウントとって実データをホストOSに直接残しても良い
      # /var/lib/postgresql/dataにDBのデータが格納されている
      - sample_postgis_data:/var/lib/postgresql/data
      # down -vなどでボリュームがない時などを含めた初回起動時に実行されるファイルを指定
      - ./postgres/sql:/docker-entrypoint-initdb.d
    env_file: ./postgres/.env_db
    ports:
      # ホスト側のポートはローカルのpsqlとバッティングするので5432以外にする
      - 5433:5432

volumes:
  sample_postgis_data:
```

書きおわっったらdocker-composeを立ち上げてみましょう。

```shlel-session
$docker-compose up -d --build
```

`localhost:8000`にブラウザから接続してみましょう。

兄の顔より見たDjangoの初期画面が表示されていれば成功です。

コンテナには以下のコマンドでログインできます。

```console
$docker exec -it <サービス名> bash
```

つまり今回であれば以下で接続できます。

```shell-session
$docker exec -it django bash
または
$docker exec -it postgres bash
```

djangoコンテナの起動は確認しましたので、postgresコンテナも確認しましょう。

```shell-seesion
$docker exec -it postgres bash
#psql -U <YOUR_DB_USER> -d <YOUR_DB_NAME>
psql (11.2 (Debian 11.2-1.pgdg90+1))
"help" でヘルプを表示します。

<YOUR_DB_NAME>=#
<YOUR_DB_NAME>=# SELECT postgis_version();
            postgis_version
---------------------------------------
 2.5 USE_GEOS=1 USE_PROJ=1 USE_STATS=1
(1 行)
```

DBが指定したもので作成されており、postgisも有効になっているのが確認できました。

以下のコマンドで一旦コンテナを停止・イメージを削除しましょう。

```shell-session
$docker-compose down -v
```

## nginx環境の構築

最後にnginxの環境を構築していきましょう。

まずはnginxディレクトリを作成してDockerfileも作成していきます。

```shll-session
$mkdir nginx
$cd nginx/
$touch Dockerfile
```

```Dockerfile:backend/containers/nginx/Dockerfile
FROM nginx:1.17.10

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

次に

```conf:backend/containers/nginx/nginx.conf
upstream config {
    # コンテナのサービス名を指定すると名前解決してくれる
    server django:8000;
}

server {
    # 80ポートで待ち受け
    listen 80;

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

    # 静的ファイルの要求をstaticにルーティング
    location /static/ {
        alias /usr/src/app/static/;
    }
}
```

一通り、必要なファイルは揃った思います。

## docker-compose.ymlにnginxを追加する

以下のようにnginxサービスを追加し、djangoサービスの`command`を`runserver`から`gunicorn`に変更してください。

```yml:backend/containers/docker-compose.yml
version: "3.7"
services:
  django:
    # コンテナ名
    container_name: django
    # ビルドするdockerファイルが格納されたディレクトリ
    build: ./django
    # 正常起動後に実行するコマンド
    command: gunicorn config.wsgi:application --bind 0.0.0.0:8000
    volumes:
      # マウントするディレクトリ
      - ./django:/usr/src/app/
    ports:
      # ホスト側のポート：コンテナ側のポート
      - 8000:8000
    env_file:
      # 環境変数に設定するファイル
      - ./django/.env
    depends_on:
      # 接続するサービス
      - postgres

  postgres:
    container_name: postgres
    build: ./postgres
    volumes:
      # DBのデータはボリュームを作成して保存
      # ディレクトリとマウントとって実データをホストOSに直接残しても良い
      # /var/lib/postgresql/dataにDBのデータが格納されている
      - sample_postgis_data:/var/lib/postgresql/data
      # down -vなどでボリュームがない時などを含めた初回起動時に実行されるファイルを指定
      - ./postgres/sql:/docker-entrypoint-initdb.d
    env_file: ./postgres/.env_db
    ports:
      # ホスト側のポートはローカルのpsqlとバッティングするので5432以外にする
      - 5433:5432

  nginx:
    container_name: nginx
    build: ./nginx
    volumes:
      - ./django/static:/usr/src/app/static
    ports:
      - 80:80
    depends_on:
      - django

volumes:
  sample_postgis_data:
```

コンテナを起動させてみます。

```shell-session
$docker-compose up -d --build
```

nginxコンテナで80ポートから配信できているのが確認できれば環境構築完了です。

`localhost`に接続してみましょう。

姉の顔より見たあの画面が表示されていますね。

お疲れ様でした。あとは煮るなり焼くなりお好きにどうぞ！

- 最終的なディレクトリ構成（冒頭に記載のものと同じ）

```shell-session
backend
└── containers
    ├── django
    │   ├── Dockerfile
    │   ├── Pipfile
    │   ├── Pipfile.lock
    │   ├── config
    │   │   ├── __init__.py
    │   │   ├── settings.py
    │   │   ├── urls.py
    │   │   └── wsgi.py
    │   ├── entrypoint.sh
    │   ├── manage.py
    ├── docker-compose.yml
    ├── nginx
    │   ├── Dockerfile
    │   └── nginx.conf
    └── postgres
        ├── Dockerfile
        └── sql
            └── init.sql
```