CentOS7+Nginx+GunicornでDjangoを起動

Django CentOS7 Nginx Gunicorn

概要

CentOS7でDjangoを動かすシリーズの一つです。CentOS7でDjangoアプリケーションを実際に公開していきます。WebサーバーはNginx、WSGIサーバーはGunicornを利用します。

Djangoのインストールと初期設定

Pythonのインストールが終わっているなら、Djangoのインストールと初期設定を行います。

まず、Djangoのインストールです。venvpipenvといった仮想環境を利用しても良いのですが、今回はグローバルな環境にpipでインストールを行います。

sudo pip3.7 install django

次にDjangoプロジェクトを作成します。プロジェクトの置き場所はいろいろ考えられますが、Django公式では以下のように推奨しています。

コードはどこに置くの?

(モダンなフレームワークを使わない) 古いプレーンな PHP の経験があるなら、これまでは Web サーバのドキュメントルート下 (/var/www といった場所) にコードを配置してきたことでしょう。 Django ではそうしないでください。 Python コードを Web サーバーのドキュメントルート下に置かないでください。コードをドキュメントルート下に置くと、 誰かがコードを Web を介して読めるようになってしまうからです。これは安全上良くありません。

コードはドキュメントルートの外、例えば /home/mycode の ような場所に置きましょう。

今回はユーザーのホームディレクトリ直下にDjangoプロジェクトを置くことにします。

cd ~
django-admin startproject project
cd project

設定ファイルを少し編集します。

vim project/settings.py

開いてすぐにあります。DEBUGFalseにし、ALLOWED_HOSTSにはそのサーバーのIPアドレスをいれます。ドメインを既に取得した方は、IPアドレスの代わりにドメインでも良いです'*'とすると全てのホストを受け入れるので、面倒であればこちらでも良いですが、セキュリティ的にはあまりよろしくありません。

DEBUG = False

ALLOWED_HOSTS = ['153.126.216.172']

下側にスクロールし、言語を日本語、タイムゾーンを東京に変更しておきます。

LANGUAGE_CODE = 'ja'

TIME_ZONE = 'Asia/Tokyo'

次のようにしておきます。

STATIC_URL = '/static/'  # これは元からあります。
STATIC_ROOT = '/usr/share/nginx/html/static'

MEDIA_URL = '/media/'
MEDIA_ROOT = '/usr/share/nginx/html/media'

開発環境ではstaticファイルは自動的に配信されていました。アプリケーション内にstaticディレクトリを作ったり、settings.pyのSATICFILES_DIRS変数の設定をするとそこから配信されました。しかし、DEBUG=Falseにすると事情が変わります。

公式ドキュメントから抜粋

ファイルを配信する

これらの設定の手順に加えて、実際に静的ファイルを配信する必要があります。

開発中に django.contrib.staticfiles を使用する場合には、DEBUG を True に設定して runserver を実行すれば、自動的に設定が行われます。(詳しくは、django.contrib.staticfiles.views.serve() を参照)

ただし、この方法は 極めて非効率 であり、セキュリティ上の問題がある 可能性が高いため、本番環境で使うべきではありません。

本番環境で静的ファイルを配信するための適切な戦略については、静的ファイルのデプロイ を読んでください。

DEBUG=True はエラー内容がページ上に表示されます。攻撃者に余計な情報を与えてしまいます。また、Djangoフレームワーク側で静的ファイルの配信を行うよりも、その前の段階であるWebサーバー側で配信するほうがパフォーマンスも高くなります。

最近はWhiteNoise等の便利な物も出てきましたが、staticファイル、mediaファイルはWebサーバー側で配信する人はまだ多くいます。

static, mediaファイルはWeサーバー側で配信するとして、置き場所をどうするかというと、よくやるのはWebサーバーのドキュメントルート付近です。Nginxのデフォルトのドキュメントルートは/usr/share/nginx/htmlですので、今回はその直下にmediaとstaticディレクトリを作って配信することにしました。

Nginxのドキュメントルート以下に、media, static ディレクトリを作ります。

sudo mkdir /usr/share/nginx/html/media
sudo mkdir /usr/share/nginx/html/static

これで、mediaファイル...FileFieldやImageFieldでのファイルアップロードは/usr/share/nginx/html/media に自動的に置かれます(MEDIA_ROOTの場所)。

しかしstaticファイルについては自分で集めて/usr/share/nginx/html/staticに置く必要があります。それを自動で行うためのコマンドがDjangoにあるので、使いましょう。

sudo python3.7 manage.py collectstatic

このコマンドは各アプリケーション内のstatic、STATICFILES_DIRSにあるstaticなファイルを、STATIC_ROOT の場所にコピーするという単純な処理をしてくれます。staticファイルの追加や修正をすると、その都度collectstaticコマンドでの反映が必要になることも覚えておきましょう。

migrateと、createsuperuser でスーパーユーザーを作成しておきます。

python3.7 manage.py migrate
python3.7 manage.py createsuperuser

Nginxの設定ファイル

Nginxの設定ファイルを編集しましょう。

sudo vim /etc/nginx/conf.d/project.conf

以下のようにしておきます。

server {
    listen  80;
    server_name 153.126.216.172;

    location /static {
        alias /usr/share/nginx/html/static;
    }

    location /media {
        alias /usr/share/nginx/html/media;
    }

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

server_name はIPアドレスかドメインを入れます。 よく間違えやすいのはlocationの部分です。

/media から始まるURLは/usr/share/nginx/html/mediaを、/static から始まるURLは/usr/share/nginx/html/static を見に行くという指定です。それ以外で/から始まるURL...つまりすべてですが、このリクエストはDjangoを動かしている127.0.0.1:8000 にリクエストを転送します。

もし/blogから始まるURLだけDjangoで処理するならば、//blog のように書き換えるだけです。これを利用して、複数のDjangoプロジェクトを起動させることも簡単です。

proxy_path の部分は、今回は1サーバー構成なので127.0.01....ローカルホスト、このサーバー自体ですが、http://160.16.126.11:8000 のような別の場所になることもありますし、幾つかのアプリケーションサーバーの中から、今暇な奴に転送する、といったことも行います。

もしHTTPS対応するならば以下のような感じになるでしょう。上側のserverディレクティブは、httpのアクセスは無条件でhttpsにリダイレクトさせています。ssl関連の記述もありますね。

server {
    listen 80;
    listen [::]:80;
    server_name narito.ninja;
    return 301 https://$host$request_uri;
}


server {
    listen  443 ssl;
    server_name narito.ninja;

    ssl on;
    ssl_certificate         /etc/letsencrypt/live/narito.ninja/fullchain.pem;
    ssl_certificate_key     /etc/letsencrypt/live/narito.ninja/privkey.pem;

    location /static {
        alias /usr/share/nginx/html/static;
    }

    location /media {
        alias /usr/share/nginx/html/media;
    }

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

設定ファイルに記述ミスがないかを調べましょう。

sudo nginx -t

以下のようにokと言われればOKです。

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

問題なさそうなので、Nginxを再起動して設定ファイルを反映してもらいましょう。

sudo systemctl reload nginx

Gunicornで動かす

まず、Gunicronをインストールします。これはpipでインストールできます。

sudo pip3.7 install gunicorn

単純に動かす

cd ~/project
sudo gunicorn --bind 127.0.0.1:8000 project.wsgi:application

このあと、ブラウザでアクセスしますが...このチュートリアルどおりにやっている場合は、Djangoプロジェクトに何かDjangoアプリケーションを入れている訳でもありません。なので、標準で入っている管理サイトにアクセスして動いているかを確認しましょう。ブラウザのURL入力欄に、http://ドメインorIP/admin とします。

見慣れた管理画面が見えたら、ここまでは問題なしです。
管理画面が表示された

念のため、ユーザーの追加もおこなってエラーが出ないか確認しておきましょう。
ユーザーの追加画面

デーモンモードで動かす

↑のコマンドでは、サーバーを起動するとコンソールにログが表示され、そのままになります。サーバーを起動するためにコンソールを1つ開きっぱなしの状態になるのは嫌なので、デーモンモードで動かすことにします。

sudo gunicorn --daemon --bind 127.0.0.1:8000 config.wsgi:application

--daemonが追加されただけですね。

Djangoを停止させたい場合は以下のようにします。まず、8000番ポートで開いているプロセスを列挙します。

sudo lsof -i:8000

12902の列がプロセスの番号です。

gunicorn 12902 root    6u  IPv4 198206      0t0  TCP localhost:irdmi (LISTEN)
gunicorn 12905 root    6u  IPv4 198206      0t0  TCP localhost:irdmi (LISTEN)

後は、そのプロセスをkillします。

sudo kill -9 12902
sudo kill -9 12905

systemctlコマンドへの登録

↑の方法も次第に面倒になります。今までsystemctlコマンドで起動や再起動をしてきましたが、自分で登録したものをsystemctl ...で管理することもできます。

まず設定ファイルを作ります。/etc/systemd/system内にファイルを作る必要があります。

sudo vim /etc/systemd/system/project.service

以下のようにしておきましょう。

[Unit]
Description=gunicorn
After=network.target

[Service]
WorkingDirectory=/home/narito/project
ExecStart=/usr/local/bin/gunicorn --bind 127.0.0.1:8000 project.wsgi:application

[Install]
WantedBy=multi-user.target

WorkingDirectoryはカレントとなるパスで、ExecStartは実行コマンドです。以下のようなことをしていると思えばイメージしやすいでしょう。

cd WorkingDirectory
ExecStart

フルパスで書くようにしましょう。gunicornのフルパスがわからない場合は、which gunicorn として探せます。例えば仮想環境を利用していれば/home/.virtualenvs/project/bin/gunicorn のようになるかもしれません。

これで、以下のようなコマンドが使えます。projectの部分は、ファイル名に対応しています。

# 起動
sudo systemctl start project

# 再起動
sudo systemctl restart project

# 停止
sudo systemctl stop project

また、今度設定ファイルを修正した場合はsudo systemctl daemon-reload というコマンドも必要になります(そのようにメッセージ表示されるので直ぐにわかります)。

ロギング設定

本番環境ではDEBUG=Falseにしています。今までと違い、エラーが起きても画面に表示されず確認ができません。

そこで、本番環境ではDjangoのロギングを設定してエラー内容を明示的にまとめるのがオススメです。

settings.pyに、次のコードを追記します。これは結果的に、Djangoプロジェクト内で何かしらの大きいエラーと、自作のblogというアプリケーション内に自分で書いたログは全て表示されます。

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'standard': {
            'format': "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s",
            'datefmt': "%d/%b/%Y %H:%M:%S"

        },
    },
    'handlers': {
        'file': {
            'class': 'logging.FileHandler',
            'filename': '/var/log/narito.ninja.log',
            'formatter':'standard',
        },
    },
    'loggers': {
        'django': {
            'handlers': ['file'],
            'level': 'ERROR',
        },
        'blog': {
            'handlers': ['file'],
            'level': 'DEBUG',
        },
    },
}

まずloggersから見ましょう。djangoとblogの2つあります。

    'loggers': {
        'django': {
            'handlers': ['file'],
            'level': 'ERROR',
        },
        'blog': {
            'handlers': ['file'],
            'level': 'DEBUG',
        },
    },

blogは、今回blogというアプリケーションを作っているとして、そこで作成したログを管理します。logger = logging.getLogger(__name__)といったコードを後で書くのですが、__name__という部分は、blogアプリケーション内のviews.pyで書いたらblog.viewsという名前に、models.pyならばblog.modelsになります。パッケージ名.モジュール名 といった形式、Python パッケージの階層と同じです。このパッケージやモジュール名に一致したロガーがあれば、それが使われるということです。

blogはDEBUGレベル以上、つまり全て受け取るようにしています。自作アプリケーションを開発している時とかは、DEBUGとかINFOレベルを使った詳細なロギングをしたいこともあるでしょう。

djangoですが、フレームワーク側でもある程度のロギングをしていて、それを受けとるためのロガーです。DEBUGとかにしちゃうと情報量が多くなるので、ERRORレベル以上のものだけを集めます。この「ERROR」というレベルは画面がそもそも表示されないとか、そういうレベルのもので、そういった大きな例外はDjangoフレームワーク側で最終的にログを取ってくれています。これを確認できるようにしておかないと、本番環境でのエラーに苦労します。

これらのhandlersにfileと指定していますが、これは次の部分です。

    'handlers': {
        'file': {
            'class': 'logging.FileHandler',
            'filename': '/var/log/narito.ninja.log',
            'formatter':'standard',
        },

classはFileHandler、見るからにファイルに書き込みそうなものを使います。filenameはログを置きたい場所、ファイルパスです。/var/log/narito.ninja.logとしておくことにします。そして、'formatter':'standard'として、standardというフォーマットを使います。次の部分です。

    'formatters': {
        'standard': {
            'format': "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s",
            'datefmt': "%d/%b/%Y %H:%M:%S"

        },
    },

日付、ログレベル、ロガー名...blog.viewsとかdjango.request、ログのメッセージという書式にしています。

blogアプリケーション内にて、自分でログを書くには、次のようにします。

import logging
logger = logging.getLogger(__name__)

def my_view(request):
    if hogehoge:
        logger.error('ここに来るのはエラー')

他にもログを取りたい、既に取ってあるアプリケーションがあれば、上のloggersに追加していくと良いでしょう。

ロギングはもっと細かく設定できるので、Django公式なども参考にしてみてください。

ログローテーション

このままだと1つのログファイルにずっと書き込まれていきます。よくやるのは、一定時間・日にち毎や、ファイルサイズ毎にログファイルを新しく作り、古いログファイルは適当にリネームして保存してもらう、というものです。ログローテーションと呼ばれるものです。

    'handlers': {
        'file': {
            'class': 'logging.handlers.TimedRotatingFileHandler',
            'filename': '/var/log/narito.ninja.log',
            'formatter': 'standard',
            'when': 'W0',
        },
    },

今回は月曜日ごとに、新しいログファイルに切り替える方法にします。こういった時間間隔でのログローテーションには、logging.handlers.TimedRotatingFileHandlerを使います。whenにW0としていますが、これはWeek0ということで、月曜日を意味します。

1日おきにログファイルを作るならば、次のようになります。

    'handlers': {
        'file': {
            'class': 'logging.handlers.TimedRotatingFileHandler',
            'filename': '/var/log/narito.ninja.log',
            'formatter': 'standard',
            'when': 'D',
            'interval': 1,
        },
    },

whenのDはDayですね。日や時間などをwhenに指定した場合は、intervalで、頻度を指定します。1なので1日ごとですね。

他の指定方法は、Python公式 ロギングのドキュメントを見てみてください。

FAQ

エラー 413 (Request Entity Too Large)

このエラーが出た場合、Nginxのデフォルトのアップロード許容サイズを超えています。confファイルに以下のように追加しましょう。

client_max_body_size 100M;

settings.pyを分けている場合

開発環境と本番環境でsettings.pyを分けるケースはよくあります。その際、本番環境のGunicoronに別のsettings.pyを読み込んでもらいたい場合は以下のようにします。

gunicorn --env DJANGO_SETTINGS_MODULE=project.other_settings --bind 127.0.0.1:8000 project.wsgi:application

--env DJANGO_SETTINGS_MODULE=project.other_settingsの部分ですね。

他に私がよくやるのは、本番環境用にproduction_wsgi.py, production_settings.pyを別途用意しておき、gunicorn --bind 127.0.0.1:8000 project.production_wsgi:application として本番用のwsgi.py...production_wsgi.pyをまず読み込ませます。本番環境でシンプルに進めた場合、どの設定ファイルを使うかはwsgi.pyを読み込んだ際に決定するためです。wsgi.pyの以下の部分です。

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.production_settings')

本番用のwsgi.pyを読み込ませることで、本番用のsettings.pyを読み込ませる形になります。urls.pyも本番環境ではちょっと変更したい場合がある(admin管理サイトのパス変更)ので、production_urls.py も作成することがあります。それをするために、production_settings.pyで以下のように書き換えます。

from .settings import *
#本番環境用に色々上書きしていく...
...
...
ROOT_URLCONF = 'project.produnction_urls'

ROOT_URLCONF = 'project.produnction_urls'の部分ですね。

サイトマップやフィードのURLがhttpsにならない

↑にあるconfファイルのproxy_set_header...の部分があることを確認し、settings.pyに以下を追加します。

SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

大きいファイルをアップロードすると、ファイルが見つからない

Djangoの標準の動作では、2.5Mb以上のファイルを送信すると/tmp といった場所に一時ファイルを作成し、それをMEDIA_ROOTへ移動するということをします。この作成されるtmpファイルの権限は、多くの場合600です。そして、600の権限のまま移動されたファイルを読み込むことはできず、結果的にPermission deniedというログがWebサーバーのログに残ります。

このような大きいファイルをアップロードするということ自体、色々と対策を考えるべきですが、とりあえず動かしたいという場合は次のような設定をsettings.pyに書くこともできます。

FILE_UPLOAD_MAX_MEMORY_SIZE = 2621440 * 10  # 2.5M×10=25M

もし正攻法でやりたい場合は、settings.pyFILE_UPLOAD_HANDLERSリストの先頭にでも、専用のファイルハンドラーを入れることになるでしょう。

Relation Posts

CentOS7でDjangoを動かすシリーズ

CentOS7、Nginx、Gunicornの環境でDjangoを動作させる上での、個人的なまとめ記事です。

シリーズ・まとめ CentOS7

Comment

記事にコメントする

名無し

Naritoさん いつもブログを拝見させていただいております。 質問なのですが、collect staticを行う際、SQLiteのversionが3.8.3以上じゃないと怒られてしまい、先に進めることができません。

エラーメッセエラーメッセージ: django.core.exceptions.ImproperlyConfigured: SQLite 3.8.3 or later is required (found 3.7.17).

ただ、python3.7でimport sqlite3 を行い、バージョンを確認すると $ python3 Python 3.7.3 (default, Mar 30 2019, 15:09:56) [GCC 5.4.0 20160609] on linux Type "help", "copyright", "credits" or "license" for more information.

import sqlite3 sqlite3.sqlite_version '3.28.0'

と出ています。

恐れ入りますが、sqlite3の設定について教えていただけないでしょうか。 よろしくお願いいたします。

返信する

名無し

すみません、解決いたしました。 パスが悪かったようなのでpython.jpを参考に $ cd Python-3.7.3 $ ./configure --enable-shared $ make $ sudo make install $ sudo sh -c "echo '/usr/local/lib' > /etc/ld.so.conf.d/custom_python3.conf" $ sudo ldconfig

としたらなおりました。

名無し

いつも大変勉強になっております。

gunicornで起動した際に静的ファイルがNotFoundとなってしまうのですが、何か設定のポイントがあるのでしょうか?

返信する

なりと

python manage.py collectstaticで、Djangoプロジェクト内の静的ファイルを移動していないのではないでしょうか。settings.pySTATIC_ROOT/usr/share/nginx/html/mediaといったパスにし、collectstaticしてみてください

名無し

ご返信遅くなり申し訳ありません。 以前apacheで動かしていたこともあってnginxのstaticファイル配信ディレクトリが異なっていました。 修正しcollectstaticしたところ表示できました。 ありがとうございました!