604 lines
12 KiB
Markdown
604 lines
12 KiB
Markdown
# Render Configuration Guide
|
|
|
|
Common configuration patterns, best practices, and troubleshooting for Render deployments.
|
|
|
|
## Environment Variables
|
|
|
|
### Required vs Optional Variables
|
|
|
|
**Always declare ALL environment variables in render.yaml**, even if values are provided by user later.
|
|
|
|
**Three categories:**
|
|
|
|
1. **Configuration values** (hardcoded):
|
|
```yaml
|
|
envVars:
|
|
- key: NODE_ENV
|
|
value: production
|
|
- key: LOG_LEVEL
|
|
value: info
|
|
- key: API_URL
|
|
value: https://api.example.com
|
|
```
|
|
|
|
2. **Secrets** (user provides):
|
|
```yaml
|
|
envVars:
|
|
- key: JWT_SECRET
|
|
sync: false
|
|
- key: STRIPE_SECRET_KEY
|
|
sync: false
|
|
- key: API_KEY
|
|
sync: false
|
|
```
|
|
|
|
3. **Auto-generated** (Render provides):
|
|
```yaml
|
|
envVars:
|
|
- key: SESSION_SECRET
|
|
generateValue: true
|
|
- key: ENCRYPTION_KEY
|
|
generateValue: true
|
|
```
|
|
|
|
### Database Connection Patterns
|
|
|
|
**PostgreSQL:**
|
|
```yaml
|
|
envVars:
|
|
- key: DATABASE_URL
|
|
fromDatabase:
|
|
name: postgres
|
|
property: connectionString
|
|
```
|
|
|
|
**Redis:**
|
|
```yaml
|
|
envVars:
|
|
- key: REDIS_URL
|
|
fromDatabase:
|
|
name: redis
|
|
property: connectionString
|
|
```
|
|
|
|
**Multiple databases:**
|
|
```yaml
|
|
envVars:
|
|
- key: PRIMARY_DB_URL
|
|
fromDatabase:
|
|
name: postgres-primary
|
|
property: connectionString
|
|
- key: ANALYTICS_DB_URL
|
|
fromDatabase:
|
|
name: postgres-analytics
|
|
property: connectionString
|
|
- key: CACHE_URL
|
|
fromDatabase:
|
|
name: redis
|
|
property: connectionString
|
|
```
|
|
|
|
### Cross-Service References
|
|
|
|
Reference other services in your account:
|
|
|
|
```yaml
|
|
services:
|
|
- type: web
|
|
name: frontend
|
|
runtime: node
|
|
envVars:
|
|
- key: API_URL
|
|
fromService:
|
|
name: backend-api
|
|
type: web
|
|
property: host # or hostport, port
|
|
|
|
- type: web
|
|
name: backend-api
|
|
runtime: node
|
|
```
|
|
|
|
**Available properties:**
|
|
- `host`: Service hostname
|
|
- `port`: Service port
|
|
- `hostport`: Combined `host:port`
|
|
|
|
### Environment Variable Groups
|
|
|
|
Share common configuration across services:
|
|
|
|
```yaml
|
|
envVarGroups:
|
|
- name: common-config
|
|
envVars:
|
|
- key: NODE_ENV
|
|
value: production
|
|
- key: LOG_LEVEL
|
|
value: info
|
|
- key: TZ
|
|
value: UTC
|
|
|
|
services:
|
|
- type: web
|
|
name: web-app
|
|
runtime: node
|
|
envVars:
|
|
- fromGroup: common-config
|
|
- key: PORT
|
|
value: 10000
|
|
|
|
- type: worker
|
|
name: worker
|
|
runtime: node
|
|
envVars:
|
|
- fromGroup: common-config
|
|
```
|
|
|
|
---
|
|
|
|
## Port Binding
|
|
|
|
### The Port Binding Requirement
|
|
|
|
**CRITICAL:** Web services must bind to `0.0.0.0:$PORT`
|
|
|
|
**Why this matters:**
|
|
- Render sets `PORT` environment variable (default: 10000)
|
|
- Services must bind to `0.0.0.0` (not `localhost` or `127.0.0.1`)
|
|
- Health checks fail if port binding is incorrect
|
|
- Deployment will fail or service won't receive traffic
|
|
|
|
### Code Examples by Language
|
|
|
|
**Node.js / Express:**
|
|
```javascript
|
|
const express = require('express');
|
|
const app = express();
|
|
|
|
const PORT = process.env.PORT || 3000;
|
|
|
|
app.listen(PORT, '0.0.0.0', () => {
|
|
console.log(`Server running on port ${PORT}`);
|
|
});
|
|
```
|
|
|
|
**Python / Flask:**
|
|
```python
|
|
import os
|
|
from flask import Flask
|
|
|
|
app = Flask(__name__)
|
|
|
|
if __name__ == '__main__':
|
|
port = int(os.environ.get('PORT', 5000))
|
|
app.run(host='0.0.0.0', port=port)
|
|
```
|
|
|
|
**Python / Django:**
|
|
|
|
In `settings.py`:
|
|
```python
|
|
# Django runs on port specified by environment
|
|
ALLOWED_HOSTS = ['*']
|
|
```
|
|
|
|
Start command in render.yaml:
|
|
```yaml
|
|
startCommand: gunicorn config.wsgi:application --bind 0.0.0.0:$PORT
|
|
```
|
|
|
|
**Python / FastAPI:**
|
|
```python
|
|
import os
|
|
import uvicorn
|
|
from fastapi import FastAPI
|
|
|
|
app = FastAPI()
|
|
|
|
if __name__ == "__main__":
|
|
port = int(os.environ.get("PORT", 8000))
|
|
uvicorn.run(app, host="0.0.0.0", port=port)
|
|
```
|
|
|
|
Start command:
|
|
```yaml
|
|
startCommand: uvicorn main:app --host 0.0.0.0 --port $PORT
|
|
```
|
|
|
|
**Go:**
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
)
|
|
|
|
func main() {
|
|
port := os.Getenv("PORT")
|
|
if port == "" {
|
|
port = "3000"
|
|
}
|
|
|
|
http.HandleFunc("/", handler)
|
|
fmt.Printf("Server starting on port %s\n", port)
|
|
http.ListenAndServe(":"+port, nil)
|
|
}
|
|
```
|
|
|
|
**Ruby / Rails:**
|
|
|
|
In `config/puma.rb`:
|
|
```ruby
|
|
port ENV.fetch("PORT") { 3000 }
|
|
bind "tcp://0.0.0.0:#{ENV.fetch('PORT', 3000)}"
|
|
```
|
|
|
|
**Rust / Actix:**
|
|
```rust
|
|
use actix_web::{App, HttpServer};
|
|
use std::env;
|
|
|
|
#[actix_web::main]
|
|
async fn main() -> std::io::Result<()> {
|
|
let port = env::var("PORT").unwrap_or_else(|_| "8080".to_string());
|
|
let addr = format!("0.0.0.0:{}", port);
|
|
|
|
HttpServer::new(|| App::new())
|
|
.bind(&addr)?
|
|
.run()
|
|
.await
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Build Commands
|
|
|
|
### Non-Interactive Flags
|
|
|
|
**Always use non-interactive flags** to prevent builds from hanging waiting for input.
|
|
|
|
**npm (Node.js):**
|
|
```yaml
|
|
buildCommand: npm ci
|
|
# NOT: npm install
|
|
```
|
|
|
|
**pip (Python):**
|
|
```yaml
|
|
buildCommand: pip install -r requirements.txt
|
|
# Already non-interactive
|
|
```
|
|
|
|
**apt (System packages):**
|
|
```yaml
|
|
buildCommand: apt-get update && apt-get install -y libpq-dev
|
|
# Use -y flag to auto-confirm
|
|
```
|
|
|
|
**bundler (Ruby):**
|
|
```yaml
|
|
buildCommand: bundle install --jobs=4 --retry=3
|
|
```
|
|
|
|
### Build with Additional Steps
|
|
|
|
**Node.js with build step:**
|
|
```yaml
|
|
buildCommand: npm ci && npm run build
|
|
```
|
|
|
|
**Python Django with static files:**
|
|
```yaml
|
|
buildCommand: pip install -r requirements.txt && python manage.py collectstatic --no-input
|
|
```
|
|
|
|
**Ruby Rails with assets:**
|
|
```yaml
|
|
buildCommand: bundle install && bundle exec rails assets:precompile
|
|
```
|
|
|
|
### Build Timeouts
|
|
|
|
**Free tier:** 15 minutes
|
|
**Paid tiers:** Configurable
|
|
|
|
**If builds timeout:**
|
|
1. Optimize dependencies (remove unused packages)
|
|
2. Use build caching
|
|
3. Consider pre-building in CI/CD
|
|
4. Upgrade to paid tier for longer timeouts
|
|
|
|
---
|
|
|
|
## Database Connections
|
|
|
|
### Internal vs External URLs
|
|
|
|
**Use internal URLs for better performance:**
|
|
|
|
When using `fromDatabase`, Render automatically provides internal `.render-internal.com` URLs:
|
|
|
|
```yaml
|
|
envVars:
|
|
- key: DATABASE_URL
|
|
fromDatabase:
|
|
name: postgres
|
|
property: connectionString
|
|
```
|
|
|
|
This provides: `postgresql://user:pass@postgres.render-internal.com:5432/db`
|
|
|
|
**Benefits:**
|
|
- Lower latency (same data center)
|
|
- No external bandwidth charges
|
|
- Automatic internal DNS
|
|
|
|
### Connection Pooling
|
|
|
|
**Node.js / PostgreSQL:**
|
|
```javascript
|
|
const { Pool } = require('pg');
|
|
|
|
const pool = new Pool({
|
|
connectionString: process.env.DATABASE_URL,
|
|
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
|
|
max: 20, // Maximum pool size
|
|
idleTimeoutMillis: 30000,
|
|
connectionTimeoutMillis: 2000,
|
|
});
|
|
```
|
|
|
|
**Python / PostgreSQL:**
|
|
```python
|
|
import psycopg2.pool
|
|
|
|
pool = psycopg2.pool.SimpleConnectionPool(
|
|
minconn=1,
|
|
maxconn=20,
|
|
dsn=os.environ['DATABASE_URL']
|
|
)
|
|
```
|
|
|
|
**Django Settings:**
|
|
```python
|
|
DATABASES = {
|
|
'default': {
|
|
'ENGINE': 'django.db.backends.postgresql',
|
|
'URL': os.environ['DATABASE_URL'],
|
|
'CONN_MAX_AGE': 600, # Connection pooling
|
|
}
|
|
}
|
|
```
|
|
|
|
### Database Migrations
|
|
|
|
**Run migrations during build:**
|
|
|
|
**Django:**
|
|
```yaml
|
|
buildCommand: pip install -r requirements.txt && python manage.py migrate
|
|
```
|
|
|
|
**Rails:**
|
|
```yaml
|
|
buildCommand: bundle install && bundle exec rails db:migrate
|
|
```
|
|
|
|
**Node.js / Prisma:**
|
|
```yaml
|
|
buildCommand: npm ci && npx prisma migrate deploy
|
|
```
|
|
|
|
---
|
|
|
|
## Free Tier Limitations
|
|
|
|
### What's Included
|
|
|
|
**Free tier provides:**
|
|
- 1 web service
|
|
- 1 PostgreSQL database (1 GB storage, 97 MB RAM)
|
|
- 750 hours/month compute
|
|
- 512 MB RAM per service
|
|
- 0.5 CPU per service
|
|
- 100 GB bandwidth/month
|
|
|
|
### Resource Limits
|
|
|
|
**Memory (512 MB):**
|
|
- Monitor memory usage in logs
|
|
- Optimize for memory-constrained environments
|
|
- Use lightweight dependencies
|
|
|
|
**CPU (0.5 cores):**
|
|
- Suitable for low-traffic applications
|
|
- Consider upgrading for higher traffic
|
|
|
|
**Spin Down (Free services):**
|
|
- Services spin down after 15 minutes of inactivity
|
|
- First request after spin down takes ~30 seconds (cold start)
|
|
- Upgrade to paid tier for always-on services
|
|
|
|
### When to Upgrade
|
|
|
|
**Upgrade to paid plan when:**
|
|
- Need more than 1 web service
|
|
- Need always-on services (no spin down)
|
|
- Traffic exceeds free tier limits
|
|
- Need more memory/CPU
|
|
- Need faster build times
|
|
- Need preview environments
|
|
|
|
---
|
|
|
|
## Health Checks
|
|
|
|
### Adding Health Check Endpoints
|
|
|
|
**Node.js / Express:**
|
|
```javascript
|
|
app.get('/health', (req, res) => {
|
|
res.status(200).json({
|
|
status: 'ok',
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
});
|
|
```
|
|
|
|
**Python / Flask:**
|
|
```python
|
|
@app.route('/health')
|
|
def health():
|
|
return {'status': 'ok'}, 200
|
|
```
|
|
|
|
**Python / FastAPI:**
|
|
```python
|
|
@app.get("/health")
|
|
async def health():
|
|
return {"status": "ok"}
|
|
```
|
|
|
|
**Go:**
|
|
```go
|
|
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"status":"ok"}`))
|
|
})
|
|
```
|
|
|
|
### Configure in render.yaml
|
|
|
|
```yaml
|
|
services:
|
|
- type: web
|
|
name: my-app
|
|
runtime: node
|
|
healthCheckPath: /health
|
|
```
|
|
|
|
**Benefits:**
|
|
- Faster deployment detection
|
|
- Better monitoring
|
|
- Automatic restart on health check failures
|
|
|
|
---
|
|
|
|
## Common Deployment Issues
|
|
|
|
### Issue 1: Missing Environment Variables
|
|
|
|
**Symptom:** Service crashes with "undefined variable" errors
|
|
|
|
**Solution:** Add all required env vars to render.yaml:
|
|
```yaml
|
|
envVars:
|
|
- key: DATABASE_URL
|
|
fromDatabase:
|
|
name: postgres
|
|
property: connectionString
|
|
- key: JWT_SECRET
|
|
sync: false # User fills in Dashboard
|
|
```
|
|
|
|
### Issue 2: Port Binding Errors
|
|
|
|
**Symptom:** `EADDRINUSE` or health check timeout errors
|
|
|
|
**Solution:** Ensure app binds to `0.0.0.0:$PORT`:
|
|
```javascript
|
|
const PORT = process.env.PORT || 3000;
|
|
app.listen(PORT, '0.0.0.0');
|
|
```
|
|
|
|
### Issue 3: Build Hangs
|
|
|
|
**Symptom:** Build times out after 15 minutes
|
|
|
|
**Solution:** Use non-interactive build commands:
|
|
```yaml
|
|
buildCommand: npm ci # NOT npm install
|
|
```
|
|
|
|
### Issue 4: Database Connection Fails
|
|
|
|
**Symptom:** `ECONNREFUSED` on port 5432
|
|
|
|
**Solutions:**
|
|
1. Use `fromDatabase` for automatic internal URLs
|
|
2. Enable SSL for external connections
|
|
3. Check `ipAllowList` settings
|
|
|
|
### Issue 5: Static Site 404s
|
|
|
|
**Symptom:** Client-side routes return 404
|
|
|
|
**Solution:** Add SPA rewrite rules:
|
|
```yaml
|
|
routes:
|
|
- type: rewrite
|
|
source: /*
|
|
destination: /index.html
|
|
```
|
|
|
|
### Issue 6: Out of Memory (OOM)
|
|
|
|
**Symptom:** Service crashes with `JavaScript heap out of memory`
|
|
|
|
**Solutions:**
|
|
1. Optimize application memory usage
|
|
2. Reduce dependency size
|
|
3. Upgrade to higher plan with more RAM
|
|
|
|
---
|
|
|
|
## Best Practices Checklist
|
|
|
|
**Environment Variables:**
|
|
- [ ] All env vars declared in render.yaml
|
|
- [ ] Secrets marked with `sync: false`
|
|
- [ ] Database URLs use `fromDatabase` references
|
|
|
|
**Port Binding:**
|
|
- [ ] App binds to `process.env.PORT`
|
|
- [ ] Bind to `0.0.0.0` (not `localhost`)
|
|
|
|
**Build Commands:**
|
|
- [ ] Use non-interactive flags (`npm ci`, `-y`, etc.)
|
|
- [ ] Build completes under 15 minutes (free tier)
|
|
|
|
**Start Commands:**
|
|
- [ ] Command starts HTTP server correctly
|
|
- [ ] Server binds to correct port
|
|
|
|
**Health Checks:**
|
|
- [ ] `/health` endpoint implemented
|
|
- [ ] Returns 200 status code
|
|
|
|
**Database:**
|
|
- [ ] Connection pooling configured
|
|
- [ ] Using internal URLs (`.render-internal.com`)
|
|
- [ ] SSL enabled if needed
|
|
|
|
**Plans:**
|
|
- [ ] Using `plan: free` by default
|
|
- [ ] Documented upgrade path for users
|
|
|
|
**Git Repository:**
|
|
- [ ] render.yaml committed to repository
|
|
- [ ] Pushed to git remote (GitHub/GitLab/Bitbucket)
|
|
- [ ] Branch specified in render.yaml (if not main)
|
|
|
|
---
|
|
|
|
## Additional Resources
|
|
|
|
- Blueprint Specification: [blueprint-spec.md](blueprint-spec.md)
|
|
- Service Types: [service-types.md](service-types.md)
|
|
- Runtimes: [runtimes.md](runtimes.md)
|
|
- Official Render Docs: https://render.com/docs
|