Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2885aa5
initial pass at scaffolding/designing DAWn_EE activity
mfwolffe Oct 17, 2025
98bffe0
more contrived activity things, migrations
mfwolffe Oct 17, 2025
0c26a44
fix race condition, transaction locking, audio state fields, update s…
mfwolffe Oct 17, 2025
5393b0f
switch to laptop
mfwolffe Oct 17, 2025
7a76420
better admin panel things for logs, fixes to logging
espadonne Dec 2, 2025
02325e9
fixes
espadonne Dec 2, 2025
8cbd606
set up for railway
espadonne Dec 2, 2025
30f59f8
Explicitly set DJANGO_SETTINGS_MODULE in Procfile
espadonne Dec 2, 2025
7da2a7e
Fix Railway deployment: rename Dockerfile, use nixpacks
espadonne Dec 2, 2025
2823d91
fix railway deployment: use autodetected python,set railway requirements
espadonne Dec 2, 2025
4673dbf
Add media files temporarily for Railway volume seeding
espadonne Dec 2, 2025
911676b
Add debug output to start command
espadonne Dec 2, 2025
d00b4f8
Add debug output to Procfile
espadonne Dec 2, 2025
992fdf5
Fix Procfile parsing - remove colons
espadonne Dec 2, 2025
5648c7c
Add nixpacks.toml to force custom start command
espadonne Dec 2, 2025
04d6252
Disable nixpacks auto-detection
espadonne Dec 2, 2025
5a27c72
Simplify media setup - use teleband/media directly
espadonne Dec 3, 2025
7be1333
Override only start phase in nixpacks, use volume for media
espadonne Dec 3, 2025
9225bc8
Fix media file serving in production (use serve() instead of static())
espadonne Dec 3, 2025
23197a6
Use shell script for startup with debug output
espadonne Dec 3, 2025
4625de2
Add debug-media endpoint to check file locations
espadonne Dec 3, 2025
928aca1
Remove media files from git (now persisted in Railway volume)
espadonne Dec 3, 2025
ae3cc32
Clean up start.sh - only seed media if needed
espadonne Dec 3, 2025
0b6d9b3
Update deployment docs - terse guide
espadonne Dec 3, 2025
7689625
Merge branch 'Lab-Lab-Lab:main' into main
mfwolffe Feb 20, 2026
99495eb
Change requirements file to reference local.txt
hcientist Feb 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
python 3.11.6
File renamed without changes.
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: bash start.sh
2 changes: 2 additions & 0 deletions config/api_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
SubmissionViewSet,
AttachmentViewSet,
TeacherSubmissionViewSet,
ActivityProgressViewSet,
)
from teleband.musics.api.views import PieceViewSet
from teleband.instruments.api.views import InstrumentViewSet
Expand Down Expand Up @@ -45,6 +46,7 @@

assignments_router = nested_cls(courses_router, "assignments", lookup="assignment")
assignments_router.register("submissions", SubmissionViewSet)
assignments_router.register("activity-progress", ActivityProgressViewSet, basename="activity-progress")

attachments_router = nested_cls(assignments_router, "submissions", lookup="submission")
attachments_router.register("attachments", AttachmentViewSet)
Expand Down
149 changes: 149 additions & 0 deletions config/settings/railway.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
"""
Django settings for Railway deployment.

Simplified production settings without AWS dependencies.
Uses whitenoise for static files and local filesystem for media.
"""

from .base import * # noqa
from .base import env, ROOT_DIR
import os

# GENERAL
# ------------------------------------------------------------------------------
SECRET_KEY = env("DJANGO_SECRET_KEY")
DEBUG = env.bool("DJANGO_DEBUG", default=False)

# Allow Railway's domain and custom domains
ALLOWED_HOSTS = env.list(
"DJANGO_ALLOWED_HOSTS",
default=["localhost", ".railway.app", ".up.railway.app"]
)

# DATABASES
# ------------------------------------------------------------------------------
# Railway provides DATABASE_URL automatically when you add Postgres
# Falls back to SQLite for simpler setups
if env("DATABASE_URL", default=None):
DATABASES = {
"default": env.db("DATABASE_URL"),
}
DATABASES["default"]["ATOMIC_REQUESTS"] = True
DATABASES["default"]["CONN_MAX_AGE"] = env.int("CONN_MAX_AGE", default=60)
else:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": ROOT_DIR / "db.sqlite3",
}
}

# CACHES
# ------------------------------------------------------------------------------
# Use local memory cache instead of Redis for simplicity
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"LOCATION": "unique-snowflake",
}
}

# SECURITY
# ------------------------------------------------------------------------------
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SECURE_SSL_REDIRECT = env.bool("DJANGO_SECURE_SSL_REDIRECT", default=True)
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 60
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_CONTENT_TYPE_NOSNIFF = True

# STATIC FILES (whitenoise)
# ------------------------------------------------------------------------------
INSTALLED_APPS = ["whitenoise.runserver_nostatic"] + INSTALLED_APPS # noqa F405
MIDDLEWARE.insert(1, "whitenoise.middleware.WhiteNoiseMiddleware") # After SecurityMiddleware

STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
STATIC_URL = "/static/"
STATIC_ROOT = ROOT_DIR / "staticfiles"

# MEDIA FILES (local filesystem)
# ------------------------------------------------------------------------------
# Media files stored in Railway volume at /app/mediafiles
# Sample audio copied from git on first deploy, user recordings persist in volume
MEDIA_URL = "/media/"
MEDIA_ROOT = env("MEDIA_ROOT", default="/app/mediafiles")

# EMAIL
# ------------------------------------------------------------------------------
# Use console backend for development/testing (emails print to console)
EMAIL_BACKEND = env(
"DJANGO_EMAIL_BACKEND",
default="django.core.mail.backends.console.EmailBackend"
)
DEFAULT_FROM_EMAIL = env(
"DJANGO_DEFAULT_FROM_EMAIL",
default="MusicCPR <noreply@musiccpr.org>"
)

# ADMIN
# ------------------------------------------------------------------------------
ADMIN_URL = env("DJANGO_ADMIN_URL", default="admin/")

# LOGGING
# ------------------------------------------------------------------------------
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"verbose": {
"format": "%(levelname)s %(asctime)s %(name)s %(message)s"
}
},
"handlers": {
"console": {
"level": "DEBUG",
"class": "logging.StreamHandler",
"formatter": "verbose",
},
},
"root": {"level": "INFO", "handlers": ["console"]},
"loggers": {
"django": {
"handlers": ["console"],
"level": "WARNING",
"propagate": False,
},
"django.request": {
"handlers": ["console"],
"level": "ERROR",
"propagate": False,
},
},
}

# CORS
# ------------------------------------------------------------------------------
# Allow Vercel frontend domains
CORS_ALLOWED_ORIGIN_REGEXES = env.list(
"CORS_ALLOWED_ORIGINS_REGEX",
default=[
r"^https://.*\.vercel\.app$",
r"^https://.*\.railway\.app$",
r"^http://localhost:\d+$",
r"^http://127\.0\.0\.1:\d+$",
]
)

# Also allow specific origins if set
CORS_ALLOWED_ORIGINS = env.list(
"CORS_ALLOWED_ORIGINS",
default=[]
)

# CSRF trusted origins (needed for admin)
CSRF_TRUSTED_ORIGINS = env.list(
"CSRF_TRUSTED_ORIGINS",
default=["https://*.railway.app", "https://*.vercel.app"]
)
49 changes: 47 additions & 2 deletions config/urls.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,51 @@
import os
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.urls import include, path
from django.urls import include, path, re_path
from django.views import defaults as default_views
from django.views.generic import TemplateView
from django.views.static import serve
from django.http import JsonResponse
from teleband.users.api.views import obtain_delete_auth_token
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView


def debug_media(request):
"""Diagnostic endpoint to check media files."""
import subprocess
result = {
"MEDIA_ROOT": str(settings.MEDIA_ROOT),
"MEDIA_ROOT_exists": os.path.exists(settings.MEDIA_ROOT),
"cwd": os.getcwd(),
}

# List files in MEDIA_ROOT
if os.path.exists(settings.MEDIA_ROOT):
try:
files = []
for root, dirs, filenames in os.walk(settings.MEDIA_ROOT):
for f in filenames[:20]: # Limit to first 20
files.append(os.path.join(root, f).replace(settings.MEDIA_ROOT, ""))
result["files"] = files
result["file_count"] = sum(len(f) for _, _, f in os.walk(settings.MEDIA_ROOT))
except Exception as e:
result["error"] = str(e)
else:
result["files"] = []

# Also check teleband/media
teleband_media = os.path.join(os.getcwd(), "teleband", "media")
result["teleband_media_exists"] = os.path.exists(teleband_media)
if os.path.exists(teleband_media):
result["teleband_media_count"] = sum(len(f) for _, _, f in os.walk(teleband_media))

return JsonResponse(result)

urlpatterns = [
path("", TemplateView.as_view(template_name="pages/home.html"), name="home"),
path("debug-media/", debug_media, name="debug-media"),
path(
"about/", TemplateView.as_view(template_name="pages/about.html"), name="about"
),
Expand All @@ -27,7 +63,16 @@
name="swagger-ui",
),
path("dashboards/", include("teleband.dashboards.urls", namespace="dashboards")),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
]

# Serve media files - in production with S3 this is handled by S3,
# but for Railway/local deployments we serve from filesystem
# Note: static() only works with DEBUG=True, so we use serve() directly for non-S3 deployments
if not hasattr(settings, 'DEFAULT_FILE_STORAGE') or 'S3' not in getattr(settings, 'DEFAULT_FILE_STORAGE', ''):
urlpatterns += [
re_path(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),
]

if settings.DEBUG:
# Static file serving when using Gunicorn + Uvicorn for local web socket development
urlpatterns += staticfiles_urlpatterns()
Expand Down
83 changes: 83 additions & 0 deletions docs/railway-deployment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Railway + Vercel Deployment

## Backend (Railway)

### Setup

1. Create project in Railway, connect GitHub repo
2. Add PostgreSQL: **+ New** → **Database** → **PostgreSQL**
3. Add Volume: **+ New** → **Volume** → mount at `/app/mediafiles`
4. Set environment variables (see below)
5. Enable public networking: **Settings** → **Networking** → **Public Networking**

### Environment Variables

| Variable | Value |
|----------|-------|
| `DJANGO_SECRET_KEY` | `python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"` |
| `MEDIA_ROOT` | `/app/mediafiles` |
| `CORS_ALLOWED_ORIGINS` | `https://your-frontend.vercel.app` |
| `CSRF_TRUSTED_ORIGINS` | `https://your-frontend.vercel.app,https://*.railway.app` |

Auto-set by Railway: `DATABASE_URL`

### Key Files

- `config/settings/railway.py` - Railway-specific settings (whitenoise, local cache, no AWS)
- `requirements/railway.txt` - Dependencies without AWS packages
- `start.sh` - Startup script (seeds media to volume, runs migrations, starts gunicorn)
- `nixpacks.toml` - Points to `start.sh`
- `Procfile` - Fallback start command

### Debug Endpoint

`/debug-media/` - Shows MEDIA_ROOT contents and file counts

---

## Frontend (Vercel)

### Setup

1. Import GitHub repo in Vercel
2. Framework: **Next.js** (auto-detected)
3. Set environment variables (see below)
4. Deploy

### Environment Variables

| Variable | Value |
|----------|-------|
| `NEXT_PUBLIC_BACKEND_HOST` | `https://your-backend.up.railway.app` |
| `NEXTAUTH_URL` | `https://your-frontend.vercel.app` |
| `SECRET` | Any random string |

### Custom Domain

1. Vercel: **Settings** → **Domains** → Add `your.domain.com`
2. DNS: Add CNAME record `your` → `cname.vercel-dns.com`
3. Update `NEXTAUTH_URL` to match

---

## Architecture

```
┌─────────────────┐ ┌──────────────────┐
│ Vercel │────▶│ Railway │
│ (Next.js) │ │ (Django) │
└─────────────────┘ └────────┬─────────┘
┌────────────┼────────────┐
▼ ▼ ▼
PostgreSQL Volume Whitenoise
(database) (media) (static)
```

| Component | Solution |
|-----------|----------|
| Static files | Whitenoise (served from container) |
| Media files | Railway Volume at `/app/mediafiles` |
| Database | Railway PostgreSQL |
| Cache | Local memory |
| Email | Console backend (logs) |
3 changes: 3 additions & 0 deletions nixpacks.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Use shell script for start command
[start]
cmd = "bash start.sh"
10 changes: 10 additions & 0 deletions railway.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"$schema": "https://railway.app/railway.schema.json",
"build": {
"builder": "NIXPACKS"
},
"deploy": {
"restartPolicyType": "ON_FAILURE",
"restartPolicyMaxRetries": 10
}
}
19 changes: 19 additions & 0 deletions requirements/railway.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Railway deployment requirements
# Simplified production without AWS dependencies

-r base.txt

# Web server
gunicorn==22.0.0

# Database (Railway provides Postgres, or use SQLite)
psycopg2-binary==2.9.9

# Static files
whitenoise==6.7.0

# Note: The following are NOT included (vs production.txt):
# - boto3 (S3)
# - django-storages (S3)
# - django-anymail (SES email)
# - Collectfast (S3 optimization)
1 change: 1 addition & 0 deletions runtime.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
python-3.11.4
23 changes: 23 additions & 0 deletions start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/bash
set -e

echo "=== Starting deployment ==="

# Copy media files to volume if source exists and volume is empty
# (Only needed on first deploy or if volume is recreated)
if [ -d "teleband/media" ] && [ ! -f "/app/mediafiles/.seeded" ]; then
echo "=== Seeding media files to volume ==="
mkdir -p /app/mediafiles
cp -rv teleband/media/* /app/mediafiles/ || echo "Copy failed"
touch /app/mediafiles/.seeded
echo "=== Media files seeded ==="
fi

echo "=== Running migrations ==="
python manage.py migrate --noinput

echo "=== Collecting static files ==="
python manage.py collectstatic --noinput

echo "=== Starting gunicorn ==="
exec gunicorn config.wsgi:application --bind 0.0.0.0:$PORT
Loading
Loading