StudyBuddy is configured for deployment on Render as a Docker-backed Django application with managed PostgreSQL. The deployment is defined as code in:
render.yaml
Live MVP URL:
https://studybuddy-django-app.onrender.com
Repository:
https://github.com/AAdewunmi/StudyBuddy-Study-Planner-Project
Render is appropriate for the StudyBuddy SaaS MVP because it gives the project a low-operations production path without requiring custom cloud infrastructure. The application already has a Docker runtime, PostgreSQL persistence, strict environment-driven production settings, and a deployment health endpoint. Render matches that shape with Docker web services, managed PostgreSQL, Blueprint as code, health checks, and environment variable and secret injection.
For an MVP, this keeps the deployment surface small: the repository defines the application runtime and production contract, while Render handles the platform work needed to host it. That lets the project validate the product workflow, data model, and operational checks before introducing more complex cloud infrastructure.
The app and repository own:
- Django application code and tests
Dockerfilerender.yaml- database migrations
- static collection and WhiteNoise static file configuration
/health/endpoint behavior- production settings and runtime contract
Render owns:
- web service hosting
- routing traffic to the container
PORT - TLS termination and certificate management
- deploy orchestration
- managed PostgreSQL lifecycle
- environment variable and secret injection
- health check execution
The app intentionally does not own:
- manual server provisioning
- PostgreSQL host operating-system management
- TLS certificate provisioning
- load balancer setup
The production container is defined by:
Dockerfile
The Render service and database are defined by:
render.yaml
The image uses:
- Python 3.13 slim
config.settings.production- Gunicorn
- WhiteNoise compressed manifest static files
PORT, defaulting to8000
The Docker build collects static assets into staticfiles/ with production
settings and build-time placeholder values. Runtime secrets are still provided
by Render.
The container command is:
gunicorn config.wsgi:application --bind 0.0.0.0:${PORT}Render runs database migrations before each deploy with:
python manage.py migrate --noinputProduction settings live in:
config/settings/production.py
Production settings are intentionally environment-driven. The application fails fast if required production values are missing.
Required environment variables:
DJANGO_SECRET_KEY
DJANGO_ALLOWED_HOSTS
DATABASE_URL
DJANGO_DEFAULT_FROM_EMAIL
DJANGO_EMAIL_HOST
Recommended environment variables:
DJANGO_SETTINGS_MODULE=config.settings.production
DJANGO_CSRF_TRUSTED_ORIGINS=https://studybuddy-django-app.onrender.com
DJANGO_SECURE_SSL_REDIRECT=True
DATABASE_SSL_REQUIRE=True
DJANGO_SECURE_HSTS_SECONDS=31536000
DJANGO_LOG_LEVEL=INFO
DJANGO_EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
DJANGO_EMAIL_PORT=587
DJANGO_EMAIL_USE_TLS=True
DJANGO_EMAIL_USE_SSL=False
DJANGO_EMAIL_TIMEOUT=10
RELEASE_SHA=<optional explicit release identifier>
Render provides DATABASE_URL from the managed PostgreSQL service declared in
render.yaml. DJANGO_SECRET_KEY and email provider-specific values are
declared with sync: false, so Render prompts for them during Blueprint
creation instead of storing secrets in the repository. Render also provides
RENDER_GIT_COMMIT at runtime; StudyBuddy uses that value for the /health/
release field when RELEASE_SHA is not explicitly set.
The Blueprint creates:
- a Docker web service named
studybuddy-django-app - a managed PostgreSQL database named
studybuddy-postgres - explicit free Render plans for both resources
- an HTTP health check at
/health/ - a pre-deploy migration command
- non-secret production environment variables
- a
DATABASE_URLvalue derived from the managed database DJANGO_SECRET_KEYand email provider prompts through Render's secret injection flow
The service uses autoDeployTrigger: checksPass, so Render waits for linked
Git checks before deploying commits.
Blueprint-managed environment values:
DJANGO_SETTINGS_MODULE=config.settings.production
DJANGO_ALLOWED_HOSTS=studybuddy-django-app.onrender.com
DJANGO_CSRF_TRUSTED_ORIGINS=https://studybuddy-django-app.onrender.com
DATABASE_URL=<from studybuddy-postgres connectionString>
DATABASE_SSL_REQUIRE=True
DJANGO_SECURE_SSL_REDIRECT=True
DJANGO_SECURE_HSTS_SECONDS=31536000
DJANGO_LOG_LEVEL=INFO
DJANGO_EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
DJANGO_EMAIL_PORT=587
DJANGO_EMAIL_USE_TLS=True
DJANGO_EMAIL_USE_SSL=False
DJANGO_EMAIL_TIMEOUT=10
Secret or generated values:
DJANGO_SECRET_KEY=<prompted by Render because sync: false>
DATABASE_URL=<generated by the managed Render PostgreSQL database>
DJANGO_EMAIL_HOST=<provided by the email provider>
DJANGO_EMAIL_HOST_USER=<provided by the email provider>
DJANGO_EMAIL_HOST_PASSWORD=<provided by the email provider>
DJANGO_DEFAULT_FROM_EMAIL=<verified sender address>
DJANGO_SERVER_EMAIL=<optional server error sender address>
RENDER_GIT_COMMIT=<provided by Render for the deployed commit>
Do not use .env.example credentials in production. They are local-only
placeholders for Docker Compose development. Keep production secret values in
Render, not in the repository.
Production static files use Django's STORAGES setting with:
whitenoise.storage.CompressedManifestStaticFilesStorage
The Docker image runs collectstatic during build. To verify static collection
locally:
docker compose exec -T web env \
DJANGO_SECRET_KEY='local-deploy-check-only-long-random-looking-secret-1234567890' \
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1 \
DATABASE_URL=postgres://studybuddy:studybuddy@db:5432/studybuddy_local \
DJANGO_CSRF_TRUSTED_ORIGINS=http://localhost:8000,http://127.0.0.1:8000 \
DATABASE_SSL_REQUIRE=False \
DJANGO_DEFAULT_FROM_EMAIL=local-deploy-check@example.com \
DJANGO_EMAIL_HOST=localhost \
python manage.py collectstatic --noinput --settings=config.settings.productionExpected local receipt:
0 static files copied to '/app/staticfiles', 128 unmodified, 357 post-processed.
The exact copied/unmodified counts can change when static assets change.
StudyBuddy exposes a deployment health endpoint:
GET /health/
Successful response:
{
"status": "ok",
"service": "studybuddy",
"release": "local",
"checks": {
"database": "ok"
}
}If the database check fails, the endpoint returns HTTP 503 with:
{
"status": "degraded",
"service": "studybuddy",
"release": "local",
"checks": {
"database": "unavailable"
}
}Run a production-settings deployment check locally through Docker Compose:
docker compose exec -T web env \
DJANGO_SECRET_KEY='local-deploy-check-only-long-random-looking-secret-1234567890' \
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1 \
DATABASE_URL=postgres://studybuddy:studybuddy@db:5432/studybuddy_local \
DJANGO_CSRF_TRUSTED_ORIGINS=http://localhost:8000,http://127.0.0.1:8000 \
DATABASE_SSL_REQUIRE=False \
DJANGO_DEFAULT_FROM_EMAIL=local-deploy-check@example.com \
DJANGO_EMAIL_HOST=localhost \
python manage.py check --deploy --settings=config.settings.productionExpected clean receipt:
System check identified no issues (0 silenced).
Run the health endpoint test:
docker compose exec -T web env \
DJANGO_SETTINGS_MODULE=config.settings.test \
TEST_DATABASE_URL=postgres://studybuddy:studybuddy@db:5432/studybuddy_test \
pytest tests/test_health_check.py -qExpected receipt:
3 passed
GitHub Actions validates the project with:
- Django system checks
- migration drift checks
- migrations
- Ruff
- Black
- isort
- Docker image build
- pytest with coverage
- Codecov upload
See Continuous Integration for the full CI quality gate.