Deploy to Render
Render is a fully managed cloud platform with a straightforward YAML configuration. You define your service, database, environment variables, and health checks in a single render.yaml file committed to your repository. This guide walks through a complete setup.
Prerequisites
- A bunpy project with
pyproject.tomland a committeduv.lock - A Render account at render.com
- The repository pushed to GitHub or GitLab (Render deploys from Git)
render.yaml
Create render.yaml in the repository root. This is the Infrastructure as Code file that Render reads when you connect the repository.
services:
- type: web
name: myapp
runtime: python
buildCommand: |
curl -fsSL https://tamnd.github.io/bunpy/install.sh | bash
export PATH="$HOME/.bunpy/bin:$PATH"
bunpy install --frozen
startCommand: bunpy server.py
healthCheckPath: /healthz
envVars:
- key: PORT
value: "10000"
- key: LOG_LEVEL
value: info
- key: DATABASE_URL
fromDatabase:
name: myapp-db
property: connectionString
- key: SECRET_KEY
generateValue: true
autoDeploy: true
plan: starter
databases:
- name: myapp-db
databaseName: myapp
user: myapp
plan: starterKey fields
buildCommand runs once during each deploy. This is where you install bunpy and run bunpy install --frozen. Render caches the build environment between deploys, but the cache is keyed on the build command output. If uv.lock changes, the entire install runs again.
startCommand is what Render runs to start the web process. Render injects PORT into the environment; your app must listen on that port.
healthCheckPath - Render pings this endpoint after deploy. The service is considered healthy when it returns HTTP 200. Render routes traffic to the new instance only after the health check passes. If it fails within the timeout, Render rolls back to the previous deploy automatically.
autoDeploy: true - triggers a new deploy every time you push to the connected branch.
Environment variables
There are three ways to set environment variables in render.yaml:
Static value:
- key: LOG_LEVEL
value: infoGenerated value (Render creates a random secret):
- key: SECRET_KEY
generateValue: trueRender generates this once and reuses it across deploys. It is visible in the dashboard but not in source control.
From a database:
- key: DATABASE_URL
fromDatabase:
name: myapp-db
property: connectionStringRender injects the connection string from the managed Postgres instance. The available property values are connectionString, host, port, user, password, and database.
Sensitive variables in the dashboard:
For secrets that cannot be in render.yaml (API keys, third-party credentials), add them in the Render dashboard under Environment > Environment Variables. These are not stored in source control.
Managed PostgreSQL
The databases block in render.yaml provisions a managed Postgres instance:
databases:
- name: myapp-db
databaseName: myapp
user: myapp
plan: starterRender handles backups, patching, and connection pooling. The starter plan includes 1 GB storage and is free during development. Use standard or pro for production.
Connect from Python:
import os
from sqlalchemy import create_engine
engine = create_engine(os.environ["DATABASE_URL"])The DATABASE_URL is injected via the fromDatabase reference in render.yaml, so you do not need to configure it manually.
Health check endpoint
Render requires the health check endpoint to respond within 30 seconds of startup. Add /healthz to your app:
# FastAPI
from fastapi import FastAPI
app = FastAPI()
@app.get("/healthz")
def health():
return {"status": "ok"}# Django
# urls.py
from django.http import JsonResponse
from django.urls import path
def health(request):
return JsonResponse({"status": "ok"})
urlpatterns = [
path("healthz", health),
# ... other routes
]If your app runs database migrations on startup, the health check should wait until migrations complete before returning 200. A simple approach:
import os
from sqlalchemy import text
@app.get("/healthz")
def health():
try:
with engine.connect() as conn:
conn.execute(text("SELECT 1"))
return {"status": "ok"}
except Exception as e:
return {"status": "error", "detail": str(e)}, 503This verifies the database is reachable, not just that the HTTP server started.
Autoscaling
Render’s autoscaling is configured in the dashboard or via render.yaml on paid plans:
services:
- type: web
name: myapp
# ... other config
scaling:
minInstances: 1
maxInstances: 5
targetMemoryPercent: 75
targetCPUPercent: 70Render scales up when CPU or memory exceeds the target and scales down when load drops. The minInstances: 1 setting prevents scale-to-zero, which is appropriate for services that need low-latency responses.
For development or low-traffic services, set minInstances: 0 (requires plan: starter or higher on the new billing model).
Build caching
Render caches build artifacts between deploys. To take advantage of this, structure buildCommand so that the expensive steps (installing bunpy, running bunpy install --frozen) only re-run when necessary.
Render does not support arbitrary cache keys like GitHub Actions does. The cache is invalidated when the build command changes or when you manually clear it in the dashboard. In practice, bunpy install --frozen is fast enough on warm cache (~1–2 seconds for a resolved lockfile) that build caching matters less here.
Deploy process
After committing render.yaml and pushing:
- Go to dashboard.render.com
- Click “New” > “Blueprint”
- Select your repository
- Render reads
render.yamland shows a preview of the resources it will create - Confirm, and Render provisions the database and deploys the web service
Subsequent deploys happen automatically on push (with autoDeploy: true) or manually via the dashboard.
Watch deploy logs in the dashboard or via the Render API:
# Using the Render API with curl
curl -H "Authorization: Bearer $RENDER_API_KEY" \
"https://api.render.com/v1/services/$SERVICE_ID/deploys?limit=5"Full project structure
myapp/
pyproject.toml
uv.lock
render.yaml
src/
myapp/
__init__.py
server.pyThe render.yaml file is the single source of truth for the infrastructure. Committing it to source control means anyone on the team can reproduce the exact same Render setup in a new account by connecting the repository.