バーチャル仏壇「推死活(おしかつ)」の技術メモ:Django + Docker + Nginx Proxy Managerで“推しを祀る”
この記事は、バーチャル仏壇「推死活(おしかつ)」をどんな技術スタックで作り、どう運用しているかのメモです。対象読者は「Django + Dockerは触ったことある」「リバプロ配下で動かす時の地雷を踏みたくない」くらいの中級者想定。
本番: https://oshikatu.unitygamebox.com/
全体構成(ざっくり)
推死活は「既存サイト(WordPress等)と同居しやすい」ことを重視していて、Nginx Proxy Manager(以下NPM)の配下でDjangoアプリを動かす前提の構成です。
- Nginx Proxy Manager: SSL終端 / リバースプロキシ(外側)
- Django: Webアプリ本体
- MySQL: データ永続化
- 静的ファイル: WhiteNoiseで配信(Django側で完結)
- メディア(アップロード画像): Djangoの
serveで配信(シンプル優先)
ルーティングは config/urls.py で、トップは TopPageView を "/" に割り当てています。
path('', TopPageView.as_view(), name='home')
path('characters/', include('characters.urls'))
...
# MEDIA_URL配下をDjango serveで配信
path(f'{media_url_pattern}/<path:path>', serve, {'document_root': settings.MEDIA_ROOT})
(該当: config/urls.py)
リクエストの流れ(イメージ)
Browser
↓ HTTPS
Nginx Proxy Manager(SSL終端)
↓ http://127.0.0.1:8000 等へ転送
Django(Gunicorn想定)
↓
MySQL
ポイントは、Django側が「インターネットに直面しない」形にしやすいこと。NPM側で証明書更新やホスト追加を吸収できます。
静的ファイル/メディアの割り切り
静的ファイルはWhiteNoise
config/settings.py で whitenoise.middleware.WhiteNoiseMiddleware を入れていて、Nginxが無い環境でも管理画面CSSが崩れにくいようにしています。
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
...
]
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
メディアはDjangoで配信(まずはシンプルに)
推死活はアップロード画像(遺影/サムネ)を扱いますが、メディア配信はまず運用の簡単さ優先でDjango側のserveに寄せています(※大規模化したらNPM側でalias配信に寄せるのが定番)。
画像アップロードとサムネ生成(Pillow)
キャラ画像は characters.models.Character の ImageField として保存し、保存後にサムネを自動生成する構成です。
image = models.ImageField('画像', upload_to=character_image_path)
thumbnail = models.ImageField('サムネイル', upload_to=character_thumbnail_path, blank=True, null=True)
def save(self, *args, **kwargs):
...
super().save(*args, **kwargs)
if current_image_name and (not self.thumbnail or previous_image_name != current_image_name):
self._generate_thumbnail()
サムネ生成では、400×400に収まるように縮小してJPEG化しています。
image = Image.open(self.image).convert('RGB')
image.thumbnail((400, 400), Image.LANCZOS)
image.save(buffer, format='JPEG', quality=85)
(該当: characters/models.py)
トップの「最近/人気/トレンド」集計
トップページは TopPageView で3種類の並びを作っています。
- 最近: 単純に
created_at降順 - 人気: 供養(線香/鈴)と閲覧数からスコア化
- トレンド: 直近7日分の供養/閲覧ログを集計してスコア化
人気スコアは(線香×3 + 鈴×2 + 閲覧×1)のように重み付けしていて、実装はこの形です。
popular_score=3 * F('incense_count') + 2 * F('bell_count') + F('view_count')
トレンドは「直近7日」の供養と閲覧を Sum(Case(When(...))) で数えています。
since = timezone.now() - timedelta(days=7)
... offerings__created_at__gte=since ...
... view_logs__created_at__gte=since ...
trend_score=F('incense_week') * 3 + F('bell_week') * 2 + F('view_week')
(該当: characters/views.py)
ログ収集:AccessLogと「変更系だけの操作ログ」
「何が使われているか」を後から見られるように、2種類のログをミドルウェアで取っています。
- AccessLog: ほぼ全リクエスト(ただし静的/メディアは除外)
- UserActivityLog: 認証済みユーザーのPOST/PUT/PATCH/DELETEを中心に記録
アクセスログは静的/メディアを除外してDBへ記録します。
if path.startswith(static_url) or path.startswith(media_url):
return response
AccessLog.objects.create(...)
(該当: accounts/middleware.py)
Nginx Proxy Manager配下での地雷:CSRF
NPMでSSL終端すると、Djangoから見ると「httpに見える」「Origin/Refererが期待通りにならない」などでCSRFが躓きがちです。
推死活では、環境変数で CSRF_TRUSTED_ORIGINS を明示する方針になっています(本番は実ドメインを列挙)。
CSRF_TRUSTED_ORIGINS=https://oshikatu.unitygamebox.com,https://www.oshikatu.unitygamebox.com
さらに、プロキシ配下前提として USE_X_FORWARDED_HOST/SECURE_PROXY_SSL_HEADER を設定しています。
USE_X_FORWARDED_HOST = True
USE_X_FORWARDED_PORT = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
(該当: config/settings.py)
本番更新の流れ(運用メモ)
運用の基本は「コード更新→必要ならビルド→migrate→collectstatic」です。ドキュメント上の“よくある手順”は以下。
# 依存関係の変更がある場合(requirements.txt更新など)
git pull origin main
docker compose -f docker-compose.prod-standalone.yml up -d --build web
docker compose -f docker-compose.prod-standalone.yml exec web python manage.py migrate
docker compose -f docker-compose.prod-standalone.yml exec web python manage.py collectstatic --noinput
(該当: docs/本番環境更新手順.md)
メンテナンス表示は「NPM側」が強い
更新時にDockerを止める運用だと、Django側でメンテ画面を返すのは難しいです(アプリが落ちるので)。この場合は、NPM側で 503 + メンテHTML を返す運用が現実的。
- 更新前にNPM側で「メンテ用ページ」へ切り替え
- アプリ更新(コンテナ停止/再起動)
- 確認後、NPM側を元に戻す
今後の改善案(やりたいこと)
- 不適切画像対策: アップロード時にSafeSearch等で弾く/要確認フラグを立てる
- メディア配信の最適化: NPM側aliasやオブジェクトストレージへ移行
- 非同期化: サムネ生成や重い集計をジョブ化
- 運用セキュリティ: 環境情報ページ等の公開範囲を見直す

コメント