static and media url fix
This commit is contained in:
1 parent
3a8f428461
commit
372f962f0f
7 files changed
+47
-9
No files matched your search
@@ -3,6 +3,7 @@ DJANGO_SECRET_KEY=replace-with-a-unique-random-secret-of-at-least-50-characters
|
||||
DJANGO_ALLOWED_HOSTS=support.example.com
|
||||
CSRF_TRUSTED_ORIGINS=https://support.example.com
|
||||
PUBLIC_BASE_URL=https://support.example.com
|
||||
ASSET_HOST=connect.loolegostar.com
|
||||
WEBOTP_HOST=support.example.com
|
||||
TIME_ZONE=Asia/Tehran
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ if not env_list("DJANGO_ALLOWED_HOSTS"):
|
||||
raise ImproperlyConfigured("DJANGO_ALLOWED_HOSTS must contain the public production host.")
|
||||
if not CSRF_TRUSTED_ORIGINS:
|
||||
raise ImproperlyConfigured("CSRF_TRUSTED_ORIGINS must contain the public HTTPS origin.")
|
||||
if len(MEDIA_SIGNING_SECRET) < 32:
|
||||
if MEDIA_SIGNING_SECRET == "Set MEDIA_SIGNING_SECRET in Coolify" or len(MEDIA_SIGNING_SECRET) < 32:
|
||||
raise ImproperlyConfigured("MEDIA_SIGNING_SECRET must be a random secret of at least 32 characters.")
|
||||
|
||||
SESSION_COOKIE_SECURE = True
|
||||
|
||||
+19
-2
@@ -12,6 +12,7 @@ x-django-service: &django-service
|
||||
DJANGO_ALLOWED_HOSTS: ${DJANGO_ALLOWED_HOSTS:?Set DJANGO_ALLOWED_HOSTS in Coolify}
|
||||
CSRF_TRUSTED_ORIGINS: ${CSRF_TRUSTED_ORIGINS:?Set CSRF_TRUSTED_ORIGINS in Coolify}
|
||||
PUBLIC_BASE_URL: ${PUBLIC_BASE_URL:?Set PUBLIC_BASE_URL in Coolify}
|
||||
ASSET_HOST: ${ASSET_HOST:-connect.loolegostar.com}
|
||||
WEBOTP_HOST: ${WEBOTP_HOST:-}
|
||||
TIME_ZONE: ${TIME_ZONE:-Asia/Tehran}
|
||||
DATABASE_URL: ${DATABASE_URL:-}
|
||||
@@ -27,7 +28,7 @@ x-django-service: &django-service
|
||||
PRODUCTION_SEED_DIR: /app/production-seed
|
||||
REQUIRE_PRODUCTION_SEED: ${REQUIRE_PRODUCTION_SEED:-1}
|
||||
MEDIA_ROOT: /app/media
|
||||
MEDIA_SIGNING_SECRET: ${MEDIA_SIGNING_SECRET:?Set MEDIA_SIGNING_SECRET in Coolify}
|
||||
MEDIA_SIGNING_SECRET: ${MEDIA_SIGNING_SECRET:?}
|
||||
USE_NGINX_MEDIA: "true"
|
||||
SMS_PROVIDER_CLASS: ${SMS_PROVIDER_CLASS:-apps.integrations.sms.sms0098.ZeroZeroNinetyEightSMSProvider}
|
||||
SMS0098_USERNAME: ${SMS0098_USERNAME:-}
|
||||
@@ -86,6 +87,22 @@ services:
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- coolify
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=coolify"
|
||||
- "traefik.http.middlewares.customer-ticket-assets-redirect-${COOLIFY_RESOURCE_UUID:-local}.redirectscheme.scheme=https"
|
||||
- "traefik.http.routers.customer-ticket-assets-http-${COOLIFY_RESOURCE_UUID:-local}.entrypoints=http"
|
||||
- "traefik.http.routers.customer-ticket-assets-http-${COOLIFY_RESOURCE_UUID:-local}.rule=Host(`${ASSET_HOST:-connect.loolegostar.com}`) && (PathPrefix(`/static/`) || PathPrefix(`/media/branding/`) || PathPrefix(`/protected-media/`))"
|
||||
- "traefik.http.routers.customer-ticket-assets-http-${COOLIFY_RESOURCE_UUID:-local}.priority=1000"
|
||||
- "traefik.http.routers.customer-ticket-assets-http-${COOLIFY_RESOURCE_UUID:-local}.middlewares=customer-ticket-assets-redirect-${COOLIFY_RESOURCE_UUID:-local}"
|
||||
- "traefik.http.routers.customer-ticket-assets-http-${COOLIFY_RESOURCE_UUID:-local}.service=customer-ticket-assets-${COOLIFY_RESOURCE_UUID:-local}"
|
||||
- "traefik.http.routers.customer-ticket-assets-https-${COOLIFY_RESOURCE_UUID:-local}.entrypoints=https"
|
||||
- "traefik.http.routers.customer-ticket-assets-https-${COOLIFY_RESOURCE_UUID:-local}.rule=Host(`${ASSET_HOST:-connect.loolegostar.com}`) && (PathPrefix(`/static/`) || PathPrefix(`/media/branding/`) || PathPrefix(`/protected-media/`))"
|
||||
- "traefik.http.routers.customer-ticket-assets-https-${COOLIFY_RESOURCE_UUID:-local}.priority=1000"
|
||||
- "traefik.http.routers.customer-ticket-assets-https-${COOLIFY_RESOURCE_UUID:-local}.service=customer-ticket-assets-${COOLIFY_RESOURCE_UUID:-local}"
|
||||
- "traefik.http.routers.customer-ticket-assets-https-${COOLIFY_RESOURCE_UUID:-local}.tls=true"
|
||||
- "traefik.http.routers.customer-ticket-assets-https-${COOLIFY_RESOURCE_UUID:-local}.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.customer-ticket-assets-${COOLIFY_RESOURCE_UUID:-local}.loadbalancer.server.port=80"
|
||||
expose:
|
||||
- "80"
|
||||
depends_on:
|
||||
@@ -96,7 +113,7 @@ services:
|
||||
- static_data:/app/staticfiles:ro
|
||||
- media_data:/app/media:ro
|
||||
environment:
|
||||
MEDIA_SIGNING_SECRET: ${MEDIA_SIGNING_SECRET:?Set MEDIA_SIGNING_SECRET in Coolify}
|
||||
MEDIA_SIGNING_SECRET: ${MEDIA_SIGNING_SECRET:?}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1/healthz/ || exit 1"]
|
||||
interval: 15s
|
||||
|
||||
@@ -40,8 +40,12 @@ if [ "${RUN_STARTUP_TASKS:-0}" = "1" ]; then
|
||||
|
||||
seed_dir="${PRODUCTION_SEED_DIR:-/app/production-seed}"
|
||||
seed_available=0
|
||||
if [ -f "$seed_dir/manifest.json" ]; then
|
||||
echo "==> Importing production seed (idempotent)..."
|
||||
seed_applied="$(python manage.py shell -c 'from apps.core.models import ProductionSeedState; print("1" if ProductionSeedState.objects.exists() else "0")')"
|
||||
if [ "$seed_applied" = "1" ]; then
|
||||
echo "==> Production seed was already applied; skipping seed import."
|
||||
seed_available=1
|
||||
elif [ -f "$seed_dir/manifest.json" ]; then
|
||||
echo "==> Importing production seed for the first and only time..."
|
||||
python manage.py import_production_seed "$seed_dir"
|
||||
seed_available=1
|
||||
elif [ "${REQUIRE_PRODUCTION_SEED:-1}" = "1" ]; then
|
||||
|
||||
@@ -24,9 +24,9 @@ customer_ticket_seed
|
||||
|
||||
## 2. دامنهها و Traefik خود Coolify
|
||||
|
||||
دامنه اصلی HTTPS را در Coolify مستقیماً به سرویس `web` و پورت `8000` متصل کنید. Traefik خود Coolify TLS، reverse proxy و هدر `X-Forwarded-Proto` را مدیریت میکند؛ Nginx جلوی Django قرار نمیگیرد.
|
||||
دامنه اصلی HTTPS را در Coolify مستقیماً به سرویس `web` و پورت `8000` متصل کنید. مقدار Domain سرویس web باید دقیقاً یک URL و بدون فاصله یا کامای انتهایی باشد. Traefik خود Coolify TLS، reverse proxy و هدر `X-Forwarded-Proto` را مدیریت میکند؛ Nginx جلوی Django قرار نمیگیرد.
|
||||
|
||||
برای همان دامنه، routeهای `/static/`، `/media/branding/` و `/protected-media/` را به سرویس `nginx` و پورت `80` بدهید و مطمئن شوید routeهای path-based نسبت به route عمومی web اولویت بالاتری دارند. برای `notification-worker` هیچ domain یا host port عمومی نسازید. اگر نسخه Coolify شما تعریف چند route روی یک دامنه را از UI پشتیبانی نمیکند، routing rule متناظر Traefik را برای این سه path به nginx اضافه کنید؛ معماری containerها همچنان بدون proxy_pass بین Nginx و Django است.
|
||||
فیلد Domain سرویسهای `nginx` و `notification-worker` را در UI کاملاً خالی بگذارید. Compose برای Nginx routerهای صریح HTTP/HTTPS با priority برابر 1000 میسازد و `/static/`، `/media/branding/` و `/protected-media/` را بدون StripPrefix به پورت 80 میفرستد. hostname این ruleها از `ASSET_HOST` خوانده میشود. معماری containerها همچنان بدون `proxy_pass` بین Nginx و Django است.
|
||||
|
||||
فایلهای آپلودی تیکت عمومی نیستند: Django ابتدا مجوز را بررسی میکند و سپس کاربر را با یک URL امضاشده پنجدقیقهای به `/protected-media/` روی Nginx میفرستد. branding عمومی زیر `/media/branding/` مستقیماً از Nginx سرو میشود.
|
||||
|
||||
@@ -122,7 +122,7 @@ SECURE_HSTS_PRELOAD=false
|
||||
1. volumeها و database خالی را ایجاد کنید.
|
||||
2. طبق `docs/PRODUCTION_SEED.md` سه فایل seed را داخل volume `customer_ticket_seed` قرار دهید.
|
||||
3. Environment Variables را در Coolify وارد کنید.
|
||||
4. Compose را deploy، دامنه اصلی را به `web:8000` و pathهای static/media گفتهشده را به `nginx:80` متصل کنید.
|
||||
4. `ASSET_HOST` را روی hostname عمومی بگذارید، Domain سرویس web را بدون کامای انتهایی تنظیم کنید و Domainهای nginx/worker را خالی نگه دارید؛ سپس Compose را deploy کنید.
|
||||
5. log سرویس `web` را برای موفقیت migrate، import production seed، collectstatic و Gunicorn بررسی کنید. وقتی seed موجود است، bootstrap عمداً اجرا نمیشود تا snapshot تغییر نکند.
|
||||
6. log سرویس `notification-worker` را بررسی کنید؛ container باید Running بماند.
|
||||
7. با superuser منتقلشده از SQLite وارد شوید و با گیرنده آزمایشی مجاز یک SMS و یک پیام Bale واقعی بفرستید.
|
||||
|
||||
@@ -65,7 +65,7 @@ docker run --rm \
|
||||
8. رد کردن `bootstrap` برای حفظ دقیق snapshot بهعنوان منبع authoritative
|
||||
9. اجرای `collectstatic` و Gunicorn
|
||||
|
||||
در deployهای بعدی checksum قبلاً ثبتشده تشخیص داده میشود و import دوباره انجام نمیشود. اگر bundle نباشد، با `REQUIRE_PRODUCTION_SEED=1` سرویس عمداً متوقف میشود تا production ناقص بالا نیاید.
|
||||
در deployهای بعدی ابتدا marker دیتابیسی `ProductionSeedState` بررسی میشود. اگر marker وجود داشته باشد، فرمان import اصلاً اجرا نمیشود و نبودن bundle نیز مانع startup نخواهد شد. `REQUIRE_PRODUCTION_SEED=1` فقط تا پیش از اولین import موفق الزامآور است.
|
||||
|
||||
## محدودیتهای ایمنی
|
||||
|
||||
|
||||
@@ -45,6 +45,22 @@ def test_nginx_serves_assets_without_proxying_django():
|
||||
assert "media_data:/app/media:ro" in compose
|
||||
assert "notification-worker:" in compose
|
||||
assert " db:" not in compose
|
||||
assert "PathPrefix(`/static/`)" in compose
|
||||
assert "PathPrefix(`/media/branding/`)" in compose
|
||||
assert "PathPrefix(`/protected-media/`)" in compose
|
||||
assert "priority=1000" in compose
|
||||
assert "stripprefix" not in compose.lower()
|
||||
assert "Set MEDIA_SIGNING_SECRET in Coolify" not in compose
|
||||
|
||||
|
||||
def test_startup_imports_production_seed_only_when_database_has_no_seed_marker():
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
entrypoint = (root / "docker" / "entrypoint.sh").read_text(encoding="utf-8")
|
||||
marker_check = "ProductionSeedState.objects.exists()"
|
||||
import_command = 'python manage.py import_production_seed "$seed_dir"'
|
||||
assert marker_check in entrypoint
|
||||
assert entrypoint.index(marker_check) < entrypoint.index(import_command)
|
||||
assert "Production seed was already applied; skipping seed import." in entrypoint
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
||||
Reference in new issue
Block a user