Docker + Django + uWSGI + nginx PyCharm開発環境(&本番環境用ファイル)を作る

PyCharm 2016 の Docker サポートの使い勝手が良いです。

Docker で開発環境を作るのは初めてだったのですが、

- ローカル ( mac ) は Docker Toolbox を使って開発環境を作る。PyCharm のデバッガでステップ実行できるようにする。
- ソースコード、およびログディレクトリは Docker イメージの中に入れず、ホストのディレクトリをマウントして使う
- 公開サーバを見越して uwsgi, nginx の設定も作っておく

という所までできたので、手順を書きます。

コード置いてあります。 https://github.com/ytyng/docker-django-skeleton

こちらを参考にしました。https://github.com/dockerfiles/django-uwsgi-nginx

## 必要アプリのインストール

- [PyCharm](https://www.jetbrains.com/pycharm/) v2016.2〜
- [Docker Toolbox](https://www.docker.com/products/docker-toolbox)

インストーラに従ってインストールしておきます。

## プロジェクトの作成、PyCharm で開く

```
$ mkdir docker-django-skeleton

$ open -a PyCharm docker-django-skeleton
```

## PyCharm用の Bash プラグインのインストール

メニューバーの PyCharm → Preferences → 検索: Plugins → Plugins の検索で「BashSupport」を検索 → 該当したものをインストールしておきます。

![スクリーンショット 2016-09-26 17.40.19.png](https://qiita-image-store.s3.amazonaws.com/0/68302/b84f3c68-25bb-f4e6-88f8-49330808aea3.png)

PyCharmの再起動を求められるので、案内通り再起動します。


## Docker 用のファイルを作る。

プロジェクトルートに Dockerfile そのまま置くと煩雑になってしまうので、docker ディレクトリを作って関連ファイルを入れます。

```text:docker/Dockerfile:
FROM ubuntu:16.04

MAINTAINER ytyng

RUN apt-get update && apt-get install -y \
    python3 \
    python3-pip \
    python3-dev \
    python3-setuptools \
    libjpeg-dev \
    nginx \
    supervisor \
  && rm -rf /var/lib/apt/lists/*

# ipython, gnureadline が必要なら
# lib32ncurses5-dev

RUN ln -s /usr/lib/x86_64-linux-gnu/libjpeg.so /usr/lib/libjpeg.so

# RUN easy_install3 pip  # aptで入れた方が安定

COPY requirements/base.txt /tmp/requirements/base.txt
RUN pip3 install -r /tmp/requirements/base.txt

# setup all the configfiles
RUN echo "daemon off;" >> /etc/nginx/nginx.conf
COPY nginx-app.conf /etc/nginx/sites-available/default
COPY supervisor-app.conf /etc/supervisor/conf.d/

RUN mkdir -p /etc/nginx/certs
# COPY certs /etc/nginx/certs/

# set locale
RUN locale-gen en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8

EXPOSE 80
#EXPOSE 443
```
↑ Pillow をインストールする可能性を考え、libjpeg などインストールしてます。


```docker/requirements/base.txt
Django==1.10.1
PyMySQL
pytz
```
↑ 今回のサンプルは、DBは SQLite3 なので PyMySQL 不要ですがサンプルとして書いておきます。


```docker/nginx-app.conf
server {
    listen 80;
    # listen 443 ssl;

    server_name docker-django-skeleton.example.com;

    charset utf-8;

    # ssl_certificate /etc/nginx/certs/xxx.crt;
    # ssl_certificate_key /etc/nginx/certs/xxx.key;

    location /static/ {
        alias /var/django/docker-django-skeleton/staticfiles/;
        break;
    }

    location / {
        include uwsgi_params;
        uwsgi_pass 127.0.0.1:3031;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Real-IP $remote_addr;
    }
}
```

```docker/supervisor-app.conf
[program:app-uwsgi]
command = /usr/local/bin/uwsgi --ini /var/django/docker-django-skeleton/conf/uwsgi.ini

[program:nginx-app]
command = /usr/sbin/nginx
```

## Docker 用の設定ファイルの作成

環境変数など設定する Bash スクリプトを用意しておきます。
mac で実行した場合と公開サーバで起動した場合に読み込まれる設定を変えるようにしています。

```bash:docker/_settings.sh
#!/usr/bin/env bash

APP_NAME=docker-django-skeleton
DJANGO_APP_NAME=docker_django_skeleton

PROJECT_DIR=$(cd $(dirname ${BASH_SOURCE:-$0})/..;pwd)

if [ $(which docker-machine) ]; then
    # mac
    eval "$(docker-machine env default)"
    DOCKER_COMMAND=docker
    DOCKER_MACHINE_COMMAND=docker-machine
    LOG_DIR=/tmp/log/${APP_NAME}
    export UWSGI_PROCESSES=1
    export UWSGI_THREADS=1
    export DJANGO_SETTINGS_MODULE=${DJANGO_APP_NAME}.settings.local
else
    # ubuntu
    DOCKER_COMMAND="sudo docker"
    LOG_DIR=/var/log/${APP_NAME}
    OPTIONAL_PARAMS="--link mysql:mysql"
    RUN_COMMAND="supervisord -n"
    export UWSGI_PROCESSES=2
    export UWSGI_THREADS=2
    export DJANGO_SETTINGS_MODULE=${DJANGO_APP_NAME}.settings.production
fi
```

## Dockerイメージ作成用スクリプト

```bash:docker/docker-01-build.sh
#!/usr/bin/env bash
cd $(dirname ${BASH_SOURCE:-$0})
. _settings.sh

${DOCKER_COMMAND} build -t ${APP_NAME} .
```

```bash:docker/docker-02-run.sh
#!/usr/bin/env bash
cd $(dirname ${BASH_SOURCE:-$0})
. _settings.sh

mkdir -p -m 777 ${LOG_DIR}

${DOCKER_COMMAND} run -d -P -p 80:80 -p 443:443 \
  -v ${PROJECT_DIR}:/var/django/${APP_NAME} \
  -v ${LOG_DIR}:/var/log/django \
  -v /etc/localtime:/etc/localtime:ro \
  -e DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE} \
  -e UWSGI_PROCESSES=${UWSGI_PROCESSES} \
  -e UWSGI_THREADS=${UWSGI_THREADS} \
  ${OPTIONAL_PARAMS} \
  --name ${APP_NAME} ${APP_NAME} ${RUN_COMMAND}

if [ "${DOCKER_MACHINE_COMMAND}" ]; then
    docker-machine ip default
fi

${DOCKER_COMMAND} ps
```

```bash:docker/docker-03-bash.sh
#!/usr/bin/env bash
cd $(dirname ${BASH_SOURCE:-$0})
. _settings.sh

EXISTS=$(${DOCKER_COMMAND} ps --quiet --filter name=${APP_NAME})

if [ "${EXISTS}" ]; then
    ${DOCKER_COMMAND} exec -it ${APP_NAME} /bin/bash $@
else
    ${DOCKER_COMMAND} run --rm -it \
    -e DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE} \
    -v ${PROJECT_DIR}:/var/django/${APP_NAME} \
    --name ${APP_NAME} ${APP_NAME} /bin/bash $@
fi
```

```bash:docker/docker-09-rm.sh
#!/usr/bin/env bash

. _settings.sh

${DOCKER_COMMAND} rm --force ${APP_NAME}
```

```bash:docker/manage.sh
#!/usr/bin/env bash
cd $(dirname ${BASH_SOURCE:-$0})
. _settings.sh

EXISTS=$(${DOCKER_COMMAND} ps --quiet --filter name=${APP_NAME})

if [ "${EXISTS}" ]; then
    ${DOCKER_COMMAND} exec -it ${APP_NAME} \
    /bin/bash -c "cd /var/django/${APP_NAME}/${APP_NAME}/; ./manage.py $*"

else
    ${DOCKER_COMMAND} run --rm -it \
    -e DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE} \
    -v ${PROJECT_DIR}:/var/django/${APP_NAME} \
    --name ${APP_NAME} ${APP_NAME} /bin/bash -c \
    "cd /var/django/${APP_NAME}/${APP_NAME}/; ./manage.py $*"
fi

```

*.sh には、実行パーミッションを付与しておきます。

```bash:
$ chmod +x docker-*.sh manage.sh
```

```
docker-django-skeleton
└── docker
    ├── Dockerfile
    ├── _settings.sh
    ├── docker-01-build.sh
    ├── docker-02-run.sh
    ├── docker-03-bash.sh
    ├── docker-09-rm.sh
    ├── manage.sh
    ├── nginx-app.conf
    ├── requirements
    │   └── base.txt
    └── supervisor-app.conf
```

## Docker イメージの作成

先ほど作成した、docker-01-build.sh を PyCharm 上で右クリック → Run 'docker-01-build.sh'

![スクリーンショット 2016-09-26 17.34.58.png](https://qiita-image-store.s3.amazonaws.com/0/68302/1394b772-4762-7a96-eeb3-193a8a511ea9.png)


Dockerイメージができます。

## Django プロジェクトディレクトリの作成
既に他の件で Django が mac にインストールされている場合は、それを使って `django-admin.py startproject` すれば良いと思いますが、今回は先ほど Docker イメージにインストールした Django を使って Django プロジェクトを作成します。

プロジェクトディレクトリをまるごと Docker にマウントし、Docker内で startproject します。

```bash:
$ docker run -it -v `pwd`:/var/django/docker-django-skeleton docker-django-skeleton /bin/bash
```

※ pwd は、docker-django-skeleton ディレクトリ。docker ディレクトリの親ディレクトリ

```bash:
$ docker run -it -v `pwd`:/var/django/docker-django-skeleton docker-django-skeleton /bin/bash
root@8d2ba7a54287:/# cd /var/django/docker-django-skeleton/
root@8d2ba7a54287:/var/django/docker-django-skeleton# django-admin.py startproject docker_django_skeleton
root@8d2ba7a54287:/var/django/docker-django-skeleton# mv docker_django_skeleton docker-django-skeleton
root@8d2ba7a54287:/var/django/docker-django-skeleton#
```

好みの問題ですが、Django プロジェクトのディレクトリ名は アンダーバーではなく ハイフンに変更しています。

PyCharm を見てみると、docker-django-skeleton ディレクトリが作成されているのが確認できます。

## settings を分ける

Django でよくやるプラクティスで、settings を ローカル環境と公開環境で分けるというものがあります。

docker_django_skeleton ディレクトリ内で、

1. settings ディレクトリを作成
2. settings.py は、 settings/base.py に改名
3. settings/local.py settings/production.py を作成。

![スクリーンショット 2016-09-26 18.09.27.png](https://qiita-image-store.s3.amazonaws.com/0/68302/5805f9cb-72ef-d229-b2ec-61be0535d845.png)

settings/local.py, settings/production.py の内容は以下のようになります。

```python:settings/local.py
from .base import *  # NOQA
```

内容はありませんが、今回はこれで良しとします。

## manage.py のインタプリタ (シェバン shebang ) を python3 にしておく

作成された manage.py を見てみると、1行目が

```python:manage.py
#!/usr/bin/env python
```

となっていますが、Docker イメージ内には Python2 が残っているため不具合が起こることがあります。

```python:manage.py
#!/usr/bin/env python3
```

に変更しておきます。

## PyCharm のインタプリタで Docker の Python を指定する

PyCharm → Preferences → 検索: interpreter

Project Interpreter が出てきます。

ここで、右上の「...」ボタンをクリックし、Add Remote を選択。

Configure Remote Python Interpreter になるので、
「Docker」を選択、Image name は docker-django-skeleton:latest を選択してください。

それと、Python interpreter path: は python となっていますが、python3 に変更します。

![スクリーンショット 2016-09-26 18.13.17.png](https://qiita-image-store.s3.amazonaws.com/0/68302/c00b1900-2329-6729-4061-bd66189f48ec.png)

すると、Docker 内の Python 環境を認識します。すごい!

![スクリーンショット 2016-09-26 18.14.12.png](https://qiita-image-store.s3.amazonaws.com/0/68302/dfd3d6b6-a2f7-2086-cdd8-1714c8689105.png)


## Djangoサポートを有効にする

Preferences で、検索: django

Language & Frameworks の Django が出て来るので選択し、Enable Django Support にチェック。

Django project root: manage.py のある、docker-django-skeleton
Settings: docker_django_skeleton/settings/local.py
OKをクリック

![スクリーンショット 2016-09-26 18.52.11.png](https://qiita-image-store.s3.amazonaws.com/0/68302/47abecaf-e7d7-21a0-76b6-dc8fc0dc0d2b.png)


## Project Structure を設定する

Preferences で、検索: structure

manage.py のある、docker-django-skeleton ディレクトリを選択し、Source をクリック → OK

![スクリーンショット 2016-09-26 18.51.32.png](https://qiita-image-store.s3.amazonaws.com/0/68302/94d97b78-b277-1347-9e44-8f2f5cc1f85a.png)


## migrate

次は、./manage.py migrate をします。
docker コンテナ内なので、./manage.py を実行するのも少し面倒ですが、docker ディレクトリの中に manage.sh を作ってあるのでこれを使います。

```bash:
$ cd docker
$ ./manage.sh

Type 'manage.py help <subcommand>' for help on a specific subcommand.

Available subcommands:

[auth]
    changepassword
    createsuperuser

[django]
    check
    compilemessages
...

$ ./manage.sh migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
...
```

Docker の Django環境を使ってマイグレーションしてくれました。

## テストサーバの起動

PyCharm の右上、Run/Debug の選択をクリックし、Edit Configurations...

Run/Debug Configurations ダイアログで、左上の + をクリック

Django server を選択

Host: には、0.0.0.0 を入力(重要)
Name: は適当に (例: runserver )
→ OK

![スクリーンショット 2016-09-26 18.53.13.png](https://qiita-image-store.s3.amazonaws.com/0/68302/37aef66e-8e3a-6d6f-6e37-8e604720285e.png)


出来た runserver 実行環境が選択されている状態で、虫(デバッグ) ボタンを押下

```
System check identified no issues (0 silenced).
September 26, 2016 - 09:54:15
Django version 1.10.1, using settings 'docker_django_skeleton.settings.local'
Starting development server at http://0.0.0.0:8000/
Quit the server with CONTROL-C.
```

となりますので、0.0.0.0:8000 をクリックするとブラウザが起動し、Docker マシーンにリダイレクトされてページが表示されます。
デバッガも有効な状態ですので、ブレイクポイントが使えます。すごい!

![スクリーンショット 2016-09-26 18.55.07.png](https://qiita-image-store.s3.amazonaws.com/0/68302/2c537325-265a-53a3-2609-0d59af29b74d.png)


## 公開環境で起動する

```conf:uwsgi.ini
[uwsgi]

app = $(APP_NAME)
django_app = $(DJANGO_APP_NAME)

base = /var/django/%(app)/%(app)/
pythonpath = %(base)
chdir = %(base)

socket = 127.0.0.1:3031

module = %(django_app).wsgi:application

processes = $(UWSGI_PROCESSES)
threads = $(UWSGI_THREADS)

master = true

pidfile = /var/run/django/%(app).pid

touch-reload = %(base)/%(django_app)/wsgi.py
lazy-apps = true
thunder-lock = true

buffer-size = 32768

logto = /var/log/django/%(app)/%n.log
```
uwsgi の設定ファイルはこんな感じですかね。

ソースコードを公開環境のサーバに持っていって、

```bash:
$ ./docker-01-build.sh
```
でコンテナを作り (ローカルで作ったのを Docker hub に push -> 本番で Pull でもいいですが)、

```bash:
$ docker-02-run.sh
```

で起動すると、公開環境 (というか docker-machine がインストールされてない環境 ) では本番用設定で nginx → uwsgi → Django で起動します。

Docker コンテナを削除する時は

```bash:
$ docker-09-rm.sh
```

で消えます。