DjangoをHerokuに上げる際にハマるポイント(データベース、静的ファイル)


 Djangoの続き。

とりあえずデータベース

 大体のWebサービスにおいてはデータベースは必要になると思います。Djangoにおいてはデフォルトでsqliteがデータベースとして設定されており、マイグレートするだけでdb.sqlite3というデータベースファイルが出来上がり、これに読み書きすることでサービスを実現できます。

 しかし、これをこのままHerokuにデプロイしても上手くいきません。Heroku環境にもそのままdb.sqlite3が移植され、データベースとしてはそれを参照することになりますが、これはGitに紐付かないデータなので、せっかくデータを書き込んでも定期的に初期状態に戻ってしまうのです。

 悲しい。

 これを防ぐためには、Gitファイルの外にあるデータベース、というかHerokuに備え付けられているPostgresに接続する必要があります。しかし、繰り返しになりますがDjangoの初期設定はそうなってはいません。かといって、ローカル環境ではHerokuのPostgressは使えないので、同一の設定で2つの環境を行き来することは原理的に不可能ということになります。

ローカルとリモートの切り分け

 しかし、ある工夫、というかアドホックな修正を行えば、この問題を解決することができます。ローカル環境だけにしか存在しないファイルを設定し、そのファイルがある時だけif文でコードを変えればよいのです。これは、.gitignoreを使ってリモートに上げないファイルを選択することにより実現することができます。具体的には、ローカルフォルダに例えばlocal.pyというファイルを置いておきます。これは中身が空でもいいですが、アクセストークン等の重要情報を秘匿するのにちょうどよいので、そのような情報を乗せると便利でしょう。逆に、リモート環境ではlocal.pyにある情報を取り出せないので、環境変数でアクセストークンを指定する必要がありますが、後に載せるコードではそれも実装します。

 フォルダ構成について想像しづらい人もいるかもしれないので実例を。

 これは実際に私が管理しているリポジトリですが、local.pyとそれに関わるフォルダ構成の様子です。また、db.sqlite3local.pyのファイル名が灰色になっていることに気づくと思いますが、これは.gitignoreの対象になっていることを示しています。当然ながら、リンク先のgithubにはこの2つのファイルは存在しません。

 そして、settings.pyでは以下のようなif文で、諸々のパラメータを分岐させます。

import dj_database_url

if os.path.exists('local.py'):
    DEBUG = True
    ALLOWED_HOSTS = ['*']
    from local import SK
    SECRET_KEY = SK
    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.sqlite3',
            'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
        }
    }
else:
    DEBUG = False
    ALLOWED_HOSTS = ['*']
    SECRET_KEY = os.environ['SK']
    db_from_env = dj_database_url.config()
    DATABASES = {
        'default': dj_database_url.config()
    }

 本件とは関係ないですが、ローカルと本番でDEBUGを切り替えるのも、ついでにやってもいいでしょう。

 これで、いい感じにローカルとリモートを切り分けた上でスムーズに開発していくことができます。

静的ファイル

 画像ファイルやCSS等は、静的ファイルとしてフォルダ内の特別な場所に置く必要があります。この辺りは、settings.py

STATIC_URL = '/static/'

 あたりに指定されているのですが、実は全然設定が足りていません

 まず、ローカル環境で静的ファイルを置く場所を指定しなければいけません。これは例えば、

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

 という変数を足せば大丈夫です。この例だと、プロジェクトファイル直下にstaticというフォルダを作れば、それが参照場所となります。

COLLECTSTATIC

 しかし、このままだと実はデプロイできません。Herokuの初期設定だと、デプロイ時にCOLLECTSTATICという儀式を経る設定になっていますが、Djangoの初期設定ではそれに失敗するからです。静的ファイルを一切使わないならば、そもそもCOLLECTSTATICをしないという選択も取れますが、静的ファイルを扱うつもりならばこの問題に正面から向き合う必要があります。COLLECTSTATICについてはこのページが超わかりやすいのでおすすめです。

 なお、COLLECTSTATICをオフする手段については、Heroku環境内(heroku run bashで入れます)で

heroku config:set DISABLE_COLLECTSTATIC=1

 というコマンドを打つことにより設定できますし、WebブラウザのGUIでも設定することができます。

 さて、COLLECTSTATICを成功させるには、settings.pyを以下のように設定する必要があります。なおここら辺の設定は公式ドキュメントに詳しく載っています。

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

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

 これで無事にデプロイできるはずです。

 しかしまだ静的ファイルは使えません

Whitenoise

 リモート環境では、ファイル名そのままではパスの指定に失敗します。なぜなのか。

 結論から言うと、リモート環境の静的ファイルは、ファイル名の途中に謎のハッシュ値みたいのがついています。

https://hoge.herokuapp.com/static/fuga.28487bbbe8b5.png

 こんな感じで。ではどうすればいいかというと、Whitenoiseというソフトがこれをいい感じに処理してくれるらしいです。上のドキュメントに載っているので思考停止で載せましょう。

pip install whitenoise

 した後で(requirements.txtに書き加えておくのも注意!)、settings.pyに以下を追加。

MIDDLEWARE_CLASSES = (
    'whitenoise.middleware.WhiteNoiseMiddleware',
    ...

STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'

 これでやっとうまくいきます。テンプレートhtml内において{% static 'hoge.png' %}といった形で表記された部分は、ローカル環境ではstatic/hoge.png、リモート環境ではstatic/hoge.**********.pngといい感じにレンダーされてくれます。

 これでやっとリモート環境で静的ファイルを利用できます。書き足すことが必要な設定多すぎん?