この記事は、バーチャル仏壇「推死活(おしかつ)」をどんな技術スタックで作り、どう運用しているかのメモです。対象読者は「Django + Dockerは触ったことある」「リバプロ配下で動かす時の地雷を踏みたくない」くらいの中級者想定。
本番: https://oshikatu.unitygamebox.com/
推死活は「既存サイト(WordPress等)と同居しやすい」ことを重視していて、Nginx Proxy Manager(以下NPM)の配下で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側で証明書更新やホスト追加を吸収できます。
config/settings.py で whitenoise.middleware.WhiteNoiseMiddleware を入れていて、Nginxが無い環境でも管理画面CSSが崩れにくいようにしています。
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
...
]
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
推死活はアップロード画像(遺影/サムネ)を扱いますが、メディア配信はまず運用の簡単さ優先でDjango側のserveに寄せています(※大規模化したらNPM側でalias配信に寄せるのが定番)。
キャラ画像は 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 降順人気スコアは(線香×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)
「何が使われているか」を後から見られるように、2種類のログをミドルウェアで取っています。
アクセスログは静的/メディアを除外してDBへ記録します。
if path.startswith(static_url) or path.startswith(media_url):
return response
AccessLog.objects.create(...)
(該当: accounts/middleware.py)
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)
更新時にDockerを止める運用だと、Django側でメンテ画面を返すのは難しいです(アプリが落ちるので)。この場合は、NPM側で 503 + メンテHTML を返す運用が現実的。