Deployment Architecture
Complete guide to ModestWear’s production deployment on free-tier services.
Overview
ModestWear is deployed using a cost-effective, scalable architecture leveraging free tiers:
Backend: Render (Free tier)
Database: Neon PostgreSQL (Serverless, Free tier)
Redis: Upstash Redis (Free tier)
Media Storage: Cloudinary (Free tier)
Frontend: Vercel (Free tier)
Total Monthly Cost: $0 (within free tier limits)
Architecture Diagram
┌─────────────────┐
│ Vercel CDN │ Frontend (Next.js)
│ modestwear-app │ https://modestwear-app.vercel.app
└────────┬────────┘
│ HTTPS
↓
┌─────────────────┐
│ Render Web │ Backend (Django + Gunicorn)
│ Service │ https://modestwear.onrender.com
└────┬───┬───┬────┘
│ │ │
│ │ └──────→ ┌──────────────┐
│ │ │ Cloudinary │ Media Storage
│ │ └──────────────┘
│ │
│ └──────────→ ┌──────────────┐
│ │ Upstash Redis│ Cache + Celery
│ └──────────────┘
│
└──────────────→ ┌──────────────┐
│Neon PostgreSQL│ Database
└──────────────┘
Backend Deployment (Render)
Service Configuration
Service Type: Web Service
Environment: Python 3.11
Build Command: pip install -r requirements.txt
Start Command: bash deploy.sh
deploy.sh Script
#!/bin/bash
# Run database migrations
python modestwear/manage.py migrate --no-input
# Collect static files
python modestwear/manage.py collectstatic --no-input
# Create superuser if doesn't exist
python modestwear/manage.py shell << EOF
from django.contrib.auth import get_user_model
import os
User = get_user_model()
email = os.getenv('DJANGO_SUPERUSER_EMAIL')
if email and not User.objects.filter(email=email).exists():
User.objects.create_superuser(
email=email,
username=os.getenv('DJANGO_SUPERUSER_USERNAME'),
password=os.getenv('DJANGO_SUPERUSER_PASSWORD')
)
print(f'Superuser {email} created')
EOF
# Start Gunicorn server
cd modestwear
gunicorn core.wsgi:application \
--bind 0.0.0.0:$PORT \
--workers 2 \
--threads 4 \
--timeout 120 \
--access-logfile - \
--error-logfile - \
--log-level info
Gunicorn Configuration
Workers: 2 (optimal for free tier: 512MB RAM)
Threads: 4 per worker
Timeout: 120 seconds
Worker Class: sync (default)
Formula: workers = (2 × CPU cores) + 1
For Render free tier (0.5 CPU): 2 workers
Environment Variables
# Django
SECRET_KEY=<generate-with-django>
DEBUG=False
ALLOWED_HOSTS=modestwear.onrender.com,modestwear-app.vercel.app
# Database
DATABASE_URL=postgresql://user:pass@host:5432/db?sslmode=require
# Redis
REDIS_URL=rediss://default:token@host:6379
CELERY_BROKER_URL=rediss://default:token@host:6379
CELERY_RESULT_BACKEND=rediss://default:token@host:6379
# Email
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_HOST_USER=your-email@gmail.com
EMAIL_HOST_PASSWORD=your-app-password
# Cloudinary
CLOUDINARY_URL=cloudinary://key:secret@cloud
# Social Auth
GOOGLE_CLIENT_ID=your-client-id
FACEBOOK_APP_ID=your-app-id
# Frontend
FRONTEND_URL=https://modestwear-app.vercel.app
# Superuser (auto-created on deploy)
DJANGO_SUPERUSER_EMAIL=admin@modestwear.com
DJANGO_SUPERUSER_USERNAME=admin
DJANGO_SUPERUSER_PASSWORD=<secure-password>
Free Tier Limitations
RAM: 512 MB
CPU: 0.5 vCPU
Bandwidth: 100 GB/month
Build Minutes: 500 minutes/month
Sleep: Service sleeps after 15 minutes of inactivity
Preventing Sleep
Solution: External cron job pinging health endpoint
# UptimeRobot or cron-job.org
GET https://modestwear.onrender.com/healthz
Interval: Every 10 minutes
Health Endpoints:
# /healthz - Lightweight
def healthz(request):
return JsonResponse({"status": "ok"})
# /health - Comprehensive
def health_check(request):
# Check database connection
# Check Redis connection
# Return detailed status
Database (Neon PostgreSQL)
Configuration
Provider: Neon (Serverless PostgreSQL)
Version: PostgreSQL 15
Region: EU Central (Frankfurt)
Connection Pooling: Enabled
SSL: Required
Connection String
postgresql://user:password@ep-xxx.eu-central-1.aws.neon.tech/neondb?sslmode=require
Django Settings
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'neondb',
'USER': 'neondb_owner',
'PASSWORD': '<password>',
'HOST': 'ep-xxx.eu-central-1.aws.neon.tech',
'PORT': 5432,
'OPTIONS': {'sslmode': 'require'},
'CONN_MAX_AGE': 600, # Connection pooling
'CONN_HEALTH_CHECKS': True,
}
}
Free Tier Limits
Storage: 512 MB
Compute: 0.25 vCPU
Active Time: 100 hours/month
Branches: 10
Optimization
Connection Pooling: Reuse connections (CONN_MAX_AGE=600)
Indexes: All foreign keys and frequently queried fields
Query Optimization: Use select_related() and prefetch_related()
Pagination: Limit result sets
Redis (Upstash)
Configuration
Provider: Upstash
Type: Redis 7.0
Region: Global (edge locations)
TLS: Required (rediss://)
Connection String
rediss://default:token@host.upstash.io:6379
Django Settings
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'rediss://default:token@host:6379',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
'SOCKET_CONNECT_TIMEOUT': 5,
'SOCKET_TIMEOUT': 5,
'IGNORE_EXCEPTIONS': True, # Graceful degradation
}
}
}
# Celery
CELERY_BROKER_URL = 'rediss://default:token@host:6379'
CELERY_RESULT_BACKEND = 'rediss://default:token@host:6379'
CELERY_BROKER_USE_SSL = {'ssl_cert_reqs': ssl.CERT_NONE}
Free Tier Limits
Storage: 256 MB
Commands: 10,000/day
Bandwidth: 1 GB/month
Celery Configuration
Free Tier Optimization: Run tasks synchronously
CELERY_TASK_ALWAYS_EAGER = True # Execute immediately
CELERY_TASK_EAGER_PROPAGATES = True
Why?
Render free tier: Single service only
No separate worker process
Tasks run in web process
Suitable for low-volume async tasks (emails)
Production Alternative:
Upgrade to paid tier
Run separate worker service
Set CELERY_TASK_ALWAYS_EAGER = False
Media Storage (Cloudinary)
Configuration
Provider: Cloudinary
Plan: Free tier
Features: Image optimization, transformations, CDN
Django Settings
import cloudinary
cloudinary.config(
cloudinary_url=os.getenv('CLOUDINARY_URL')
)
DEFAULT_FILE_STORAGE = 'cloudinary_storage.storage.MediaCloudinaryStorage'
Free Tier Limits
Storage: 25 GB
Bandwidth: 25 GB/month
Transformations: 25,000/month
Features Used
Auto-optimization: Compress images
Thumbnails: Auto-generate on upload
CDN: Global delivery
Transformations: Resize, crop, format conversion
Frontend (Vercel)
Configuration
Framework: Next.js 14
Deployment: Automatic from Git
Domain: modestwear-app.vercel.app
Environment Variables
NEXT_PUBLIC_API_URL=https://modestwear.onrender.com
NEXT_PUBLIC_GOOGLE_CLIENT_ID=your-client-id
Free Tier Limits
Bandwidth: 100 GB/month
Build Minutes: 6,000 minutes/month
Serverless Functions: 100 GB-hours
CI/CD Pipeline
Automatic Deployment
Git Push → GitHub → Render/Vercel → Deploy
Render:
Detects push to main branch
Runs build command
Executes migrations
Starts new service
Health check
Routes traffic to new version
Vercel:
Detects push to main branch
Builds Next.js app
Deploys to CDN
Preview deployments for PRs
Manual Deployment
# Render
git push origin main
# Or trigger from dashboard
# Render Dashboard → Manual Deploy
Monitoring & Logging
Render Logs
# View logs in dashboard
Render Dashboard → Service → Logs
# Or via CLI
render logs -f
Django Logging
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
},
},
'root': {
'handlers': ['console'],
'level': 'INFO',
},
}
Health Monitoring
UptimeRobot: Ping every 10 minutes
Alerts: Email on downtime
Status Page: Public status page
Security
HTTPS/TLS
Render: Automatic SSL certificates
Vercel: Automatic SSL certificates
Neon: TLS required
Upstash: TLS required (rediss://)
Django Security Settings
if not DEBUG:
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
CORS Configuration
CORS_ALLOWED_ORIGINS = [
'https://modestwear-app.vercel.app',
'https://modestwear.onrender.com',
]
CSRF_TRUSTED_ORIGINS = [
'https://modestwear-app.vercel.app',
'https://modestwear.onrender.com',
]
Scaling Strategy
Current (Free Tier)
Users: ~100 concurrent
Requests: ~10,000/day
Storage: ~500 MB database
Scaling Path
Phase 1: Optimize Free Tier
Add caching (Redis)
Optimize queries
CDN for static files
Phase 2: Paid Tier ($7-20/month)
Render: Starter plan ($7/month)
Neon: Scale plan ($19/month)
Separate Celery worker
Phase 3: Horizontal Scaling
Multiple Render instances
Load balancer
Read replicas (Neon)
Dedicated Redis
Phase 4: Enterprise
AWS/GCP migration
Kubernetes
Auto-scaling
Multi-region
Backup & Recovery
Database Backups
Neon: Automatic daily backups (7-day retention)
Manual Backup:
pg_dump $DATABASE_URL > backup.sql
Restore:
psql $DATABASE_URL < backup.sql
Media Backups
Cloudinary: Automatic backups included
Manual Backup:
# Download all media
cloudinary-cli download --all
Troubleshooting
Service Won’t Start
Check:
Build logs for errors
Environment variables set correctly
Database connection string
Migrations completed
Database Connection Errors
Solutions:
Check DATABASE_URL format
Verify SSL mode:
?sslmode=requireTest connection:
psql $DATABASE_URLCheck Neon active hours
Redis Connection Errors
Solutions:
Verify rediss:// (with double ‘s’)
Check SSL configuration
Test:
redis-cli -u $REDIS_URL pingEnable IGNORE_EXCEPTIONS for graceful degradation
Out of Memory
Solutions:
Reduce Gunicorn workers
Optimize queries
Add pagination
Upgrade to paid tier
Cost Optimization
Current Setup (Free)
Service |
Plan |
Cost |
|---|---|---|
Render |
Free |
$0 |
Neon |
Free |
$0 |
Upstash |
Free |
$0 |
Cloudinary |
Free |
$0 |
Vercel |
Free |
$0 |
Total |
$0/month |
Paid Upgrade Path
Service |
Plan |
Cost |
|---|---|---|
Render |
Starter |
$7/month |
Neon |
Scale |
$19/month |
Upstash |
Pay-as-you-go |
~$5/month |
Cloudinary |
Plus |
$89/month |
Vercel |
Pro |
$20/month |
Total |
$140/month |
Next Steps
Review Security Architecture
Learn about Database Design
Explore API Documentation