SIGMA-SE Math & Tech Library

SIGMA-SE Math & Tech Library


数学と情報技術をテーマに、書籍や教材だけではつかみにくい考え方を具体例とともにわかりやすく伝える解説サイトです。

VPSで作るDjangoサイト構築手順 - Nginx編:4/4 PostgreSQL・Django本番設定と起動確認

概要

VPS上にPostgreSQLを構築し、Djangoからローカル接続する設定、マイグレーション、静的ファイルの収集、本番環境の起動確認までを整理する。

Djangoサイトを公開する最後の段階では、データベースへ接続できるだけでなく、秘密情報をソースコードから分離し、Nginx、uWSGI、Django、PostgreSQLの境界をそろえる必要がある。
この記事ではPostgreSQLをDjangoと同じVPSで動かし、5432番ポートをインターネットへ公開しない構成を前提とする。

前提環境

  • OS
    CentOS Stream 9
  • 言語
    Python 3.11
  • Webサーバー
    Nginx
  • アプリケーションサーバー
    uWSGI
  • フレームワーク
    Django 5.2 LTS
  • データベース
    PostgreSQL 16
  • ドメイン
    example.com
  • 作業ユーザー
    vpsuser
  • uWSGI実行ユーザー
    sweb
  • Djangoプロジェクト
    /var/www/projs/sweb
  • Python仮想環境
    /var/www/venvs/sweb
  • Django設定モジュール
    config
  • Djangoアプリケーション
    tblog

この記事で扱うこと

  • PostgreSQL 16の初期化とローカル接続だけを許可する設定。
  • SCRAM認証を使ったデータベースユーザーの作成。
  • Psycopg 3とDjangoのデータベース接続設定。
  • 秘密情報、HTTPS、静的ファイルに関する本番設定。
  • マイグレーションからNginx・uWSGI経由の起動確認までの流れ。

作業時の注意点

  • PostgreSQLの公開範囲
    Djangoと同じVPSで動かす場合、5432番ポートを外部へ開放せず、待受け先と認証対象をローカルホストに限定する。
  • 秘密情報
    データベースのパスワードとSECRET_KEYsettings.pyやGit管理下のファイルへ書かない。
  • マイグレーション
    本番サーバーで新しいマイグレーションを作らず、開発環境で作成・確認したファイルを適用する。
  • HTTPS判定
    NginxがHTTPSを終端する構成では、転送したスキームをDjangoが正しく判定できないとリダイレクトループになる。
  • 静的ファイル
    STATIC_ROOTとNginxのaliasを同じ実ディレクトリへ対応させ、収集時はumask 0027と権限確認によってNginxからの書込みを禁止する。

実施内容

PostgreSQLのインストールと初期化

  • PostgreSQL 16をインストール
    Django 5.2はPostgreSQL 14以降に対応している。CentOS Stream 9で利用可能なストリームを確認し、PostgreSQL 16のサーバープロファイルをインストールする。

    $ sudo dnf module list postgresql
    $ sudo dnf module install -y postgresql:16/server
    $ psql --version
    
  • データベースクラスタを初期化
    postgresql-setup --initdbは新規構築時だけ実行する。既存データがある環境で初期化をやり直さない。

    $ sudo postgresql-setup --initdb
    
  • ローカル接続とSCRAM認証を設定
    /var/lib/pgsql/data/postgresql.confで待受け先と、新しく設定するパスワードの保存方式を指定する。

    $ sudoedit /var/lib/pgsql/data/postgresql.conf
    
    listen_addresses = 'localhost'
    password_encryption = 'scram-sha-256'
    

    /var/lib/pgsql/data/pg_hba.confには、対象データベースとユーザーを限定したルールを追加する。
    pg_hba.confは上から順に最初に一致したルールが使われるため、ローカルホスト全体を対象にした広いhostルールより前へ配置する。

    $ sudoedit /var/lib/pgsql/data/pg_hba.conf
    
    # TYPE  DATABASE   USER       ADDRESS          METHOD
    host    exampledb  appuser    127.0.0.1/32     scram-sha-256
    host    exampledb  appuser    ::1/128          scram-sha-256
    
  • PostgreSQLを起動

    $ sudo systemctl enable --now postgresql
    $ sudo systemctl status postgresql --no-pager
    

データベースとユーザーの作成

  • アプリケーション専用ユーザーを作成
    PostgreSQLの管理ユーザーでpsqlを開き、appuserexampledbを作成する。パスワードはURLセーフなランダム値を生成してパスワード管理ツールへ保存し、後述の環境ファイルにも同じ値を設定する。

    $ python3.11 -c 'import secrets; print(secrets.token_urlsafe(48))'
    

    \passwordを使うと、生成したパスワードをSQLやシェルの履歴へ残さず対話的に設定できる。

    $ sudo -u postgres psql
    postgres=# SHOW password_encryption;
    postgres=# CREATE ROLE appuser LOGIN;
    postgres=# \password appuser
    postgres=# CREATE DATABASE exampledb OWNER appuser ENCODING 'UTF8';
    postgres=# \q
    
  • ローカル接続を確認
    パスワードをコマンドラインへ直接書かず、プロンプトから入力する。

    $ psql --host=127.0.0.1 --dbname=exampledb --username=appuser --password
    exampledb=> \conninfo
    exampledb=> \q
    
  • 外部へ公開されていないことを確認
    PostgreSQLが127.0.0.1または::1だけで待ち受け、firewalldで5432番ポートを追加していないことを確認する。0.0.0.0:5432[::]:5432が表示される場合は設定を見直す。

    $ sudo ss -lntp | grep 5432
    $ sudo firewall-cmd --list-ports
    

Psycopg 3のインストール

  • 仮想環境へPostgreSQLドライバーを追加
    Django 5.2ではPsycopg 3が推奨されている。本番環境ではシステムのlibpqlibsslの更新を反映できるローカルビルドが推奨されているため、開発パッケージを追加してC実装をインストールする。psycopg2とは混在させない。

    $ sudo dnf install -y postgresql-devel
    $ source /var/www/venvs/sweb/bin/activate
    $ python -m pip install "psycopg[c]>=3.1.8"
    $ python -m pip show psycopg
    

    再構築できるよう、実際に採用したバージョンは依存関係ファイルへ固定してGitで管理する。

本番用の秘密情報を分離

  • 環境ファイルを作成
    uWSGIを実行するswebユーザーと、前記事でswebグループへ追加したvpsuserだけが読み取れるよう、/etc/sweb.envの所有者と権限を制限する。

    $ sudo install -o root -g sweb -m 640 /dev/null /etc/sweb.env
    $ sudoedit /etc/sweb.env
    

    DJANGO_SECRET_KEY用には、データベースパスワードとは別のURLセーフなランダム値を生成する。

    $ /var/www/venvs/sweb/bin/python -c 'import secrets; print(secrets.token_urlsafe(64))'
    

    次の値は例示用の文字列ではなく、生成した値へ置き換える。単一引用符で囲むことで、systemdと管理コマンド用のシェルの両方で同じファイルを安全に読み込める。

    DJANGO_SECRET_KEY='REPLACE_WITH_A_LONG_RANDOM_VALUE'
    DJANGO_DB_NAME=exampledb
    DJANGO_DB_USER=appuser
    DJANGO_DB_PASSWORD='REPLACE_WITH_THE_DATABASE_PASSWORD'
    
  • uWSGIサービスから環境ファイルを読み込む
    前記事で作成したsweb.serviceを編集し、任意ファイルを意味する先頭の-を外して、環境ファイルを必須にする。環境ファイル自体はGitへ追加しない。

    $ sudoedit /etc/systemd/system/sweb.service
    
    EnvironmentFile=/etc/sweb.env
    
    $ sudo systemctl daemon-reload
    
  • 管理コマンド用のシェルへ読み込む
    マイグレーションなどを実行する間だけ現在のシェルへ読み込み、作業後に環境変数を削除する。

    $ set -a
    $ source /etc/sweb.env
    $ set +a
    

settings.pyの本番設定

  • PostgreSQLと公開ドメインを設定
    /var/www/projs/sweb/config/settings.pyでは秘密情報を環境変数から読み込む。DEBUGを無効化し、許可するホストとオリジンを具体的に指定する。

    import os
    
    SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
    DEBUG = False
    
    ALLOWED_HOSTS = ["example.com"]
    CSRF_TRUSTED_ORIGINS = [
        "https://example.com",
    ]
    
    DATABASES = {
        "default": {
            "ENGINE": "django.db.backends.postgresql",
            "NAME": os.environ["DJANGO_DB_NAME"],
            "USER": os.environ["DJANGO_DB_USER"],
            "PASSWORD": os.environ["DJANGO_DB_PASSWORD"],
            "HOST": "127.0.0.1",
            "PORT": "5432",
        }
    }
    
  • HTTPS関連の設定
    NginxがHTTP_X_FORWARDED_PROTOを必ず上書きし、uWSGIがループバックアドレスだけで待ち受ける構成であることを前提に、Djangoへ信頼するヘッダーを設定する。

    SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
    SECURE_SSL_REDIRECT = True
    SESSION_COOKIE_SECURE = True
    CSRF_COOKIE_SECURE = True
    
  • 静的ファイルの出力先を設定

    STATIC_URL = "/static/"
    STATIC_ROOT = Path("/var/www/static/sweb")
    

    startprojectで作成したsettings.pyでは、ファイル先頭でPathがインポートされている。独自の設定ファイルへ移す場合はfrom pathlib import Pathも引き継ぐ。

    MEDIA_ROOTへ保存するアップロードファイルは信頼できない入力となる。利用する場合は静的ファイルと分け、実行されないNginx設定やオブジェクトストレージを別途設計する。

Nginxの転送ヘッダーと静的ファイル設定

  • 静的ファイル用ディレクトリを作成
    ソースコードの親ディレクトリはNginxから参照させず、collectstaticの出力先を分離する。所有者のvpsuserが更新し、nginxグループは読み取りと通過だけを行う。setgidによって配下にもnginxグループを引き継ぎ、収集時はumask 0027でグループの書込みを禁止する。

    $ sudo install -d -o root -g root -m 0755 /var/www/static
    $ sudo install -d -o vpsuser -g nginx -m 2750 /var/www/static/sweb
    $ sudo restorecon -Rv /var/www/static
    
  • HTTPSのserverコンテキストを確認
    Djangoへ転送するスキームをNginx側で上書きし、STATIC_ROOTへ収集したファイルをNginxから直接配信する。

    location / {
        include /etc/nginx/uwsgi_params;
        uwsgi_param HTTP_X_FORWARDED_PROTO $scheme;
        uwsgi_pass 127.0.0.1:8001;
    }
    
    location /static/ {
        alias /var/www/static/sweb/;
    }
    

    location /static/aliasの末尾の/を対応させる。Nginxの実行ユーザーにはstatic配下の読み取り権限と、親ディレクトリを通過する権限が必要になる。
    uWSGIは127.0.0.1:8001だけで待ち受け、8001番ポートをfirewalldやVPS側のパケットフィルターで外部へ公開しない。

  • Nginxの構文を確認

    $ sudo nginx -t
    $ sudo systemctl reload nginx
    

マイグレーションと静的ファイルの収集

  • マイグレーションファイルの不足を確認
    マイグレーションファイルは開発環境で作成してGitへ追加する。本番環境では、モデル変更に対応する未作成ファイルがないことを確認する。

    $ cd /var/www/projs/sweb
    $ /var/www/venvs/sweb/bin/python manage.py makemigrations --check --dry-run
    
  • データベースへ反映
    適用内容を確認し、必要に応じてバックアップを取得してからマイグレーションを実行する。

    $ /var/www/venvs/sweb/bin/python manage.py migrate --plan
    $ /var/www/venvs/sweb/bin/python manage.py migrate
    
  • Django管理ユーザーを作成
    createsuperuserで作るのはPostgreSQLの管理ユーザーではなく、Django管理サイトへログインするユーザーとなる。

    $ /var/www/venvs/sweb/bin/python manage.py createsuperuser
    
  • 静的ファイルを収集

    $ umask 0027
    $ /var/www/venvs/sweb/bin/python manage.py collectstatic --noinput
    $ sudo chown -R vpsuser:nginx /var/www/static/sweb
    $ sudo find /var/www/static/sweb -type d -exec chmod 2750 {} +
    $ sudo find /var/www/static/sweb -type f -exec chmod 0640 {} +
    $ sudo find /var/www/static/sweb -perm /0022 -print
    

    最後のfindで何も表示されないことを確認する。これにより、Nginxは静的ファイルを読み取れるが書き換えられない。

本番環境の起動確認

  • DjangoとNginxの事前診断
    check --deployの警告は機械的に無視せず、採用した構成に応じて一つずつ確認する。runserverは本番公開には使用しない。

    $ /var/www/venvs/sweb/bin/python manage.py check --deploy
    $ /var/www/venvs/sweb/bin/python manage.py migrate --check
    $ sudo nginx -t
    
  • サービスを再起動

    $ sudo systemctl restart postgresql
    $ sudo systemctl restart sweb
    $ sudo systemctl restart nginx
    $ sudo systemctl enable postgresql sweb nginx
    
  • サービスとuWSGIの待受け先を確認
    uWSGIが外部アドレスではなく127.0.0.1:8001で待ち受けていることを確認する。

    $ sudo systemctl status postgresql sweb nginx --no-pager
    $ sudo ss -lntp | grep 8001
    
  • HTTPS経由で確認
    https://example.com/が正常応答し、http://example.com/がHTTPSへリダイレクトされることを確認する。管理画面を利用する場合は、ブラウザからhttps://example.com/admin/login/を開いてログインも確認する。

    $ curl -I https://example.com/
    $ curl -I http://example.com/
    
  • ログを確認
    502エラーではuWSGIサービスと8001番ポートの待受け、500エラーではDjangoとデータベース接続、静的ファイルの404ではNginxのaliasと権限を順に確認する。

    $ sudo journalctl -u postgresql -n 50 --no-pager
    $ sudo journalctl -u sweb -n 50 --no-pager
    $ sudo journalctl -u nginx -n 50 --no-pager
    $ sudo tail -n 50 /var/log/nginx/error.log
    
  • 作業用の環境変数を削除

    $ unset DJANGO_SECRET_KEY DJANGO_DB_NAME DJANGO_DB_USER DJANGO_DB_PASSWORD
    

まとめ

  • Django 5.2からPostgreSQLへ接続する場合は、対応するPostgreSQL 14以降とPsycopg 3を使用する。
  • 同一VPS上のPostgreSQLはローカルホストだけで待ち受け、SCRAM認証を使い、5432番ポートを外部へ公開しない。
  • SECRET_KEYとデータベースパスワードは/etc/sweb.envへ分離し、Git管理下のソースコードへ書かない。
  • 本番ではDEBUG=False、公開ホスト、CSRF対象オリジン、HTTPS判定、CookieのSecure属性を設定する。
  • マイグレーションは事前に内容を確認して適用し、静的ファイルはソースコードの外へcollectstaticした後にNginxのaliasから配信する。
  • 最後にcheck --deploy、Nginxの構文、各サービス、uWSGIの待受け先、HTTPS応答、ログを順に確認する。

参考文献

この記事を共有
Xで共有 Facebookで共有 LINEで共有



Copyright SIGMA-SE All Rights Reserved.
s-hama@sigma-se.jp