9.5 KiB
CapRover & Cloudflare CORS Configuration Guide
This guide covers deploying CodeForge with separate frontend and backend domains, properly configured for CORS.
Architecture
Frontend: https://frontend.example.com (nginx serving React SPA)
Backend: https://backend.example.com (Flask API)
Frontend Configuration
1. Nginx CORS Setup
The nginx.conf has been configured with proper CORS headers:
- Access-Control-Allow-Origin: Allows all origins (can be restricted)
- Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
- Access-Control-Allow-Headers: All necessary headers including Authorization
- Preflight Handling: OPTIONS requests are handled with 204 response
2. CapRover Frontend Setup
Create a new app in CapRover for the frontend:
# App Settings
App Name: codeforge-frontend
Deployment Method: Docker Image Registry or Git
Port: 80
Environment Variables (Optional)
# If you need to configure backend URL at build time
VITE_BACKEND_URL=https://backend.example.com
Captain Definition File
Create captain-definition in project root:
{
"schemaVersion": 2,
"dockerfilePath": "./Dockerfile"
}
3. Cloudflare Settings for Frontend
- SSL/TLS Mode: Full (strict) or Full
- Always Use HTTPS: Enabled
- Minimum TLS Version: TLS 1.2
- Automatic HTTPS Rewrites: Enabled
Security Headers (Optional but Recommended)
Go to Cloudflare Dashboard → Security → Settings → Security Headers:
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Note: Don't add CORS headers in Cloudflare if nginx is already handling them.
Backend Configuration
1. Flask CORS Setup
The backend has been configured with flask-cors:
ALLOWED_ORIGINS = os.environ.get('ALLOWED_ORIGINS', '*').split(',')
CORS(app,
resources={r"/api/*": {
"origins": ALLOWED_ORIGINS,
"methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
"allow_headers": ["Content-Type", "Authorization", "X-Requested-With"],
"expose_headers": ["Content-Type", "X-Total-Count"],
"supports_credentials": True,
"max_age": 3600
}})
2. CapRover Backend Setup
Create a new app in CapRover for the backend:
# App Settings
App Name: codeforge-backend
Deployment Method: Docker Image Registry or Git
Port: 5001 (or as configured)
Environment Variables
# Required
ALLOWED_ORIGINS=https://frontend.example.com,https://www.frontend.example.com
# Optional
PORT=5001
DATABASE_PATH=/data/codeforge.db
DEBUG=false
Persistent Data Volume
Enable persistent storage for SQLite database:
Container Path: /data
Host Path: /captain/data/codeforge-backend
Captain Definition File for Backend
Create backend/captain-definition:
{
"schemaVersion": 2,
"dockerfilePath": "./Dockerfile"
}
3. Cloudflare Settings for Backend
- SSL/TLS Mode: Full (strict) or Full
- Always Use HTTPS: Enabled
- Minimum TLS Version: TLS 1.2
- API Shield (if available): Enable for API endpoints
Frontend Code Configuration
Environment-based Backend URL
Create .env.production:
VITE_BACKEND_URL=https://backend.example.com
VITE_USE_BACKEND=true
Runtime Configuration
The app auto-detects backend availability. Update your storage configuration:
// src/lib/storage-config.ts
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL ||
window.RUNTIME_CONFIG?.backendUrl ||
'http://localhost:5001'
const USE_BACKEND = import.meta.env.VITE_USE_BACKEND === 'true' ||
window.RUNTIME_CONFIG?.useBackend === true
Testing CORS Configuration
1. Test Frontend CORS
curl -X OPTIONS https://frontend.example.com \
-H "Origin: https://other-domain.com" \
-H "Access-Control-Request-Method: GET" \
-v
Expected response should include:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
2. Test Backend CORS
curl -X OPTIONS https://backend.example.com/api/storage/keys \
-H "Origin: https://frontend.example.com" \
-H "Access-Control-Request-Method: GET" \
-v
Expected response should include:
Access-Control-Allow-Origin: https://frontend.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Credentials: true
3. Test Cross-Origin Request
curl -X GET https://backend.example.com/api/storage/keys \
-H "Origin: https://frontend.example.com" \
-v
Common Issues & Solutions
Issue 1: CORS Error in Browser Console
Error: No 'Access-Control-Allow-Origin' header is present
Solutions:
- Verify
ALLOWED_ORIGINSenvironment variable is set correctly on backend - Check nginx config is properly loaded (restart CapRover app)
- Ensure Cloudflare is not stripping CORS headers
Issue 2: Preflight Request Failing
Error: Response to preflight request doesn't pass access control check
Solutions:
- Verify OPTIONS method is allowed in both nginx and Flask
- Check that preflight response includes all required headers
- Increase
max_ageif requests are frequent
Issue 3: Credentials Not Sent
Error: Cookies/credentials not sent with request
Solutions:
- Enable
credentials: 'include'in fetch requests - Set
supports_credentials: Truein Flask CORS config - Don't use wildcard
*for origin when credentials are needed
Issue 4: Cloudflare Blocking Requests
Error: CF-RAY header present with 403/520 errors
Solutions:
- Whitelist your backend domain in Cloudflare Firewall
- Disable "Browser Integrity Check" temporarily for testing
- Check Cloudflare Security Level (set to "Low" for API endpoints)
Security Best Practices
1. Restrict Origins in Production
Instead of *, specify exact origins:
# Backend .env
ALLOWED_ORIGINS=https://frontend.example.com,https://www.frontend.example.com
2. Use HTTPS Only
Ensure all requests use HTTPS. Update nginx to redirect HTTP to HTTPS:
server {
listen 80;
server_name _;
return 301 https://$host$request_uri;
}
3. Implement Rate Limiting
Add rate limiting to backend:
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
limiter = Limiter(
app=app,
key_func=get_remote_address,
default_limits=["200 per hour", "50 per minute"]
)
@app.route('/api/storage/<key>', methods=['GET'])
@limiter.limit("100 per minute")
def get_value(key):
# ...
4. Add API Authentication
For production, add token-based authentication:
from functools import wraps
from flask import request
def require_api_key(f):
@wraps(f)
def decorated_function(*args, **kwargs):
api_key = request.headers.get('Authorization')
if not api_key or not verify_api_key(api_key):
return jsonify({'error': 'Unauthorized'}), 401
return f(*args, **kwargs)
return decorated_function
@app.route('/api/storage/<key>', methods=['PUT'])
@require_api_key
def set_value(key):
# ...
CapRover Deployment Steps
Deploy Frontend
- Ensure Docker image builds successfully locally
- Push code to Git repository or Docker registry
- In CapRover:
- Create new app:
codeforge-frontend - Enable HTTPS
- Connect custom domain:
frontend.example.com - Deploy from Git/Registry
- Verify deployment via browser
- Create new app:
Deploy Backend
- Build and test backend Docker image
- In CapRover:
- Create new app:
codeforge-backend - Enable HTTPS
- Connect custom domain:
backend.example.com - Add environment variables (especially
ALLOWED_ORIGINS) - Add persistent volume at
/data - Deploy from Git/Registry
- Verify health endpoint:
https://backend.example.com/health
- Create new app:
Configure Cloudflare
- Add DNS records for both domains (point to CapRover server)
- Enable Cloudflare proxy (orange cloud)
- Configure SSL/TLS settings
- Test both domains
Monitoring & Debugging
Check Nginx Logs (Frontend)
# In CapRover app terminal
tail -f /var/log/nginx/access.log
tail -f /var/log/nginx/error.log
Check Flask Logs (Backend)
# In CapRover app logs view or terminal
tail -f /var/log/app.log
Browser DevTools
- Open Network tab
- Enable "Preserve log"
- Filter by domain to see cross-origin requests
- Check Response headers for CORS headers
- Look for preflight (OPTIONS) requests
Example Fetch Configuration
Frontend API Client
const API_BASE_URL = import.meta.env.VITE_BACKEND_URL || 'http://localhost:5001'
async function apiRequest(endpoint: string, options: RequestInit = {}) {
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
...options,
credentials: 'include', // Include cookies if needed
headers: {
'Content-Type': 'application/json',
...options.headers,
},
})
if (!response.ok) {
throw new Error(`API error: ${response.statusText}`)
}
return response.json()
}
// Usage
const data = await apiRequest('/api/storage/keys')