Django+MySQL+Dockerの環境をHerokuにデプロイする

一年ぶりにHerokuを触ってみたのですが、ほとんど忘れていました。

また、HerokuでDjangoの環境構築を行う上で、リソースが少なかったので同じ境遇の方の助けになれればと思います。

Herokuでは便利なことにDockerのイメージをそのままデプロイ出来るようになったそうです。

これが無料で出来るとはSalceforceさんも太っ腹ですね。

前置きはさておき早速説明を始めますね。

# 環境

```
# 実行環境
macOS: Monterey(12.3.1)
shell: zsh 5.8(x86_64-apple-darwin21.0)
Python: 3.9.7
Docker: 20.10.8
docker-compose: 1.29.2

# 主要ライブラリ
django-heroku==0.3.1
django-mysql==4.7.0
django==4.0.5
djangorestframework==3.13.1
guicorn==20.1.0
urllib3==1.26.9
whitenoise==6.2.0
```

# ライブラリのインストール

```terminal
$ pipenv install django-heroku gunicorn whitenoise
```

## Error: pg_config executable not found.

``django-heroku``をインストールする際に``Error: pg_config executable not found.``とエラーが出る場合は``postgresql``をインストール。

```
$ brew install postgresql
```

# settings.pyの変更

``settings.py``と同じディレクトリに``settings_local.py``を作成してください。

そして、``settings.py``を``settings_local.py``にコピペし、``settings.py``を変更してください。

```settings.py
import os
import django_heroku
from urllib.parse import urlparse

from .settings_local import *

DEBUG = False

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

# static setting
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'


if not DEBUG:
    # Heroku settings

    # HerokuのConfigを読み込み
    django_heroku.settings(locals())

```

また、``settings_local.py``のミドルウェア（一番上）に``whitenoise.middleware.WhiteNoiseMiddleware``を追加。

```settings_local.py
...
MIDDLEWARE = [
    'whitenoise.middleware.WhiteNoiseMiddleware',
    ...
]
...
```

厳密に言うと``django.middleware.security.SecurityMiddleware``の下に配置すれば良いのですが、あまり変化はないため一番上に追加します。

# プラットフォーム指定（M1・Mac使用者）

M1チップ搭載のMacbookを使用している方は、``Dockerfile``に``--platform=linux/amd64``を指定してください。

```Dockerfile
FROM --platform=linux/amd64 python:3.9.7-alpine
```

このプラットフォームの指定を行わないと``Error: Exec format error``とHeroku上でエラーが起き、アプリケーションを稼働させることができません。

# Dockerfile

``Docker``ファイルの最後に以下を追加。

```Dockerfile
...
RUN python3 manage.py collectstatic --noinput
CMD gunicorn --bind 0.0.0.0:$PORT app.wsgi
```

この``CMD~``がProcfileの代わりとなります。

念の為私が使っている``Dockerfile``を載せておきます。参考程度に使ってください。

```Dockerfile
FROM --platform=linux/amd64 python:3.9.7-alpine

ENV PYTHONDONTWRITEBYTECODE=1 \
  PYTHONUNBUFFERED=1 \
  PYTHONUTF8=1 \
  PIP_NO_CACHE_DIR=off \
  PIP_DISABLE_PIP_VERSION_CHECK=on \
  APP_HOME=/usr/src/app

RUN addgroup -S app && adduser -S app -G app

RUN mkdir -p $APP_HOME && \
    mkdir $APP_HOME/staticfiles
    
WORKDIR $APP_HOME

COPY . $APP_HOME

RUN apk update && \
    apk --no-cache add gcc python3-dev mysql-client mysql-dev postgresql-dev musl-dev libffi-dev tzdata && \
    pip install --upgrade pip && \
    pip install pipenv && \
    pipenv install --system --ignore-pipfile --deploy

RUN ln -fs /usr/share/zoneinfo/Etc/UTC /etc/localtime

RUN chown -R app:app $APP_HOME

USER app

RUN python3 manage.py collectstatic --noinput
CMD gunicorn --bind 0.0.0.0:$PORT app.wsgi
```

# heroku.yml

ディレクトリ直下に``heroku.yml``を作成し、以下をコピペ。

```heroku.yml
build:
  docker:
    web: Dockerfile
```

# Heroku設定

## ログイン

```
$ heroku login　&& heroku container:login
```

## アプリ作成

```
$ heroku create [heroku-app-name]
```

## DB設定

ClearDBをアプリに追加。

```terminal
$ heroku addons:add cleardb:ignite
```

### JawsDBのすゝめ（2022年6月1日追記） 

Heroku上でMySQLのバージョンが5.7以上を使用する場合は``JawsDB``が推奨されているそうなので、特に理由はない限りJawsDBを使用しましょう。

念の為、ClearDB・JawsDB両方の設定方法を記述します。

https://qiita.com/haruyan_hopemucci/items/14e0dbeb0aedd85a74ee

JawsDBの追加方法

```terminal
$ heroku addons:create jawsdb:kitefin --version=8.0 // 任意のバージョンを指定
```

次に、ClearDBのデータベースURLを確認しコピーします。

```
$ heroku config
CLEARDB_DATABASE_URL: mysql://***:***@***/***?reconnect=true // ClearDB
JAWSDB_URL: mysql://***:***@***/*** // JawsDB
```

### 環境変数

環境変数に``DATABSE_URL``を追加。

```.env
DATABASE_URL = mysql://***:***@***/***
```

ClearDBを使用した場合``?reconnect=true``は取り除いちゃってください。

### settings.py

冒頭に修正した``settings.py``にClearDBの設定を追加。

```settings.py
import os
import django_heroku
from urllib.parse import urlparse

from .settings_local import *

DEBUG = False

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

# static setting
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'


if not DEBUG:
    # Heroku settings

    # ClearDB setting
    if 'DATABASES' not in locals():
        DATABASES = {}

    if 'DATABASE_URL' in os.environ:
        url = urlparse(os.environ['DATABASE_URL'])

        # Ensure default database exists.
        DATABASES['default'] = DATABASES.get('default', {})

        # Update with environment configuration.
        DATABASES['default'].update({
            'ENGINE': 'django.db.backends.mysql',
            'NAME': url.path[1:],
            'USER': url.username,
            'PASSWORD': url.password,
            'HOST': url.hostname,
            'PORT': url.port,
        })

    django_heroku.settings(locals())

```

## スタック設定

```terminal
$ heroku stack:set container
``` 

## Dockerイメージをデプロイ

```
$ heroku container:push web && heroku container:release web
```

## スケール設定

```
$ heroku ps:scale web=1
```

## マイグレーション

```
$ heroku run python3 manage.py migrate
```

## 管理者ユーザー作成

```
$ heroku run python3 manage.py createsuperuser
```

## アプリ状態確認

```
$ heroku open
```

いつもどおりの画面が開けばOK

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/954754/6a370d77-5b0f-543b-9804-6c68140aa704.png)

# 補足

## Procfileは不要

途中にも書いたのですが、``Dockerfile``の``CMD``がProcfileの代わりとなっています。

## STATICFILES_DIRSは不要

Heroku上での環境構築を行うにあたって、静的ファイルを読み込めないエラーに苦戦しました。

結果、settings.pyに以下の２文を付け加えるだけ。

```settings.py
...
    STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
    STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
...
```

色々情報は出回ってますが、これが現時点での最適解かと思われます。

また、``whitenoise``に関連するミドルウェアの追加も忘れずに。

## ALLOWED_HOSTS

Heroku上で作業を行う上で、``settings_local.py``にある``ALLOWDED_HOSTS``にHerokuのホストを指定してください。

```settings.py
...
ALLOWED_HOSTS = ['[アプリ名].herokuapp.com']
...
```

## 追記：スリープ

Herokuの無料枠では30分アクセスがないとサーバーがスリープ状態に入ります。

それを防ぐためには様々な方法があります。

https://qiita.com/Oyuki123/items/855aa97e5ce27e44079f

こちらの記事を見ていただければ大概のことは解決できます。

ただし、少し調べてみると、メジャーなやり方でもある``Scheduler``だと時々実行されない、意図せぬ課金が発生などの問題もあるそうです。

なので、私は``django-crontab``を使って25分毎にHerokuにアクセスするように設定しました。

https://blog.kai-lab.com/django_crontab_with_docker/

# 最後に

Djangoのリソースが出回っておらず、かなり苦戦しましたが動いてくれてよかったです。

何か不明な点や上手く動作しないということがあれば気軽にコメントにて質問してください。

以上、「Django+MySQL+Dockerの環境をHerokuにデプロイする」でした！

Thank you for reading

# 参考記事

以下、この記事を書くにあたって参考にさせていただいた記事となります。

・https://qiita.com/rebi/items/efd1c36f0a9e46222d80
・https://zenn.dev/daku10/articles/m1-heroku-container-trouble-exec-format-error
・https://qiita.com/akirakudo/items/16a01271b0a39316e439
・https://devcenter.heroku.com/articles/cleardb#using-cleardb-with-python-django