Skip to content

Deploying Dazzle to Heroku

Prerequisites

  • Heroku CLI installed
  • Git repository with your Dazzle project
  • dazzle.toml at the repository root

Quick Start

# Create Heroku app
heroku create my-dazzle-app

# Add PostgreSQL and Redis
heroku addons:create heroku-postgresql:essential-0
heroku addons:create heroku-redis:mini

# Set required env vars
heroku config:set DAZZLE_ENV=production
heroku config:set DAZZLE_SECRET_KEY=$(python -c "import secrets; print(secrets.token_urlsafe(32))")

# Deploy
git push heroku main

DATABASE_URL and REDIS_URL are set automatically by the add-ons.

Deploy Files (uv buildpack)

Heroku's Python buildpack natively supports uv (since May 2025), which gives reproducible, hash-pinned builds instead of re-resolving dependencies on every deploy. Dazzle scaffolds the uv layout for you:

dazzle deploy heroku        # writes Procfile, pyproject.toml, uv.lock, .python-version
File Purpose
pyproject.toml declares dazzle-dsl[serve] (pinned to your installed version) + Postgres/Redis deps; requires-python = ">=3.12"; [tool.uv] package = false (a DSL app, not a wheel)
uv.lock hash-pinned resolution of the full tree (generated by uv lock)
.python-version 3.12 — the interpreter Heroku provisions
Procfile web: dazzle serve --production

Commit all four, then deploy:

git add Procfile pyproject.toml uv.lock .python-version
git commit -m "Heroku deploy config"
git push heroku main

uv vs pip. Heroku activates the uv path only when pyproject.toml + uv.lock + .python-version are present and requirements.txt/Pipfile/poetry.lock are absent. Migrating from the old pip layout? Delete requirements.txt and runtime.txt first (dazzle deploy heroku warns you if they're still present). To stay on pip instead, run dazzle deploy heroku --pip (emits requirements.txt + runtime.txt).

After changing dependencies, re-run uv lock and commit the updated uv.lock — Heroku installs from the lock, not a live resolve.

Procfile

dazzle deploy heroku writes a web: dazzle serve --production Procfile, which is enough for most apps. For explicit multi-worker control (and the WEB_CONCURRENCY pool math below), use the uvicorn form instead:

web: uvicorn dazzle_http.runtime.app_factory:create_app_factory --factory --host 0.0.0.0 --port $PORT --workers ${WEB_CONCURRENCY:-4}

This uses uvicorn's multi-worker mode. Each worker gets its own process and connection pool.

Environment Variables

Variable Required Default Description
DATABASE_URL Yes (auto) PostgreSQL URL (set by Heroku Postgres add-on)
REDIS_URL Yes (auto) Redis URL (set by Heroku Redis add-on)
DAZZLE_ENV Yes development Set to production
DAZZLE_SECRET_KEY Yes - Secret key for sessions and tokens
DAZZLE_DB_POOL_MIN No 2 Minimum connection pool size
DAZZLE_DB_POOL_MAX No 10 Maximum connection pool size
WEB_CONCURRENCY No 4 Number of uvicorn worker processes

Connection Pool Tuning

Match DAZZLE_DB_POOL_MAX to your Heroku Postgres plan's connection limit. The full per-process budget is ~14 connections (pool ceiling + ~3-4 event-framework + ~1-2 startup-migration transients), so the formula has a hidden multiplier:

Plan Max Connections Recommended DAZZLE_DB_POOL_MAX (with 4 workers) Total footprint
Essential-0 20 2 (2+4)*4 = 24 — over; use WEB_CONCURRENCY=1
Essential-1 40 5 (5+4)*4 = 36
Standard-0 120 15 (15+4)*4 = 76
Standard-2+ 400+ 25 (25+4)*4 = 116

Formula: (DAZZLE_DB_POOL_MAX + 4) * WEB_CONCURRENCY < plan_max_connections - 5

The +4 accounts for non-pool framework connections; the -5 leaves headroom for heroku pg:psql debug sessions.

# Conservative defaults for Essential-1 (single dyno, 4 workers):
heroku config:set DAZZLE_DB_POOL_MAX=5 WEB_CONCURRENCY=4

# Or simplify the footprint with a single worker per dyno + horizontal scaling:
heroku config:set DAZZLE_DB_POOL_MAX=10 WEB_CONCURRENCY=1
heroku ps:scale web=4

See docs/reference/databases.mdConnection Pool for diagnostic commands and full symptom → fix table.

Scaling

Start with a single standard dyno:

heroku ps:scale web=1:standard-1x

Before scaling horizontally, try vertical scaling:

heroku ps:scale web=1:standard-2x

Horizontal scaling works out of the box — each dyno runs independent workers with their own connection pools. The advisory lock on migrations ensures only one worker runs schema changes.

Asset Bundling

DAZZLE_ENV=production flips the framework's CSS/JS loading mode from individual files (live-reload friendly, dev-only) to a single bundled file (/static/dist/dazzle.min.{js,css}). The bundle is precomputed at framework release time and ships in the dazzle-dsl wheel — no Node toolchain required at slug build time.

Configure via [ui] assets in dazzle.toml:

[ui]
# "auto"   = bundle when DAZZLE_ENV=production, individual in dev (default)
# "always" = bundle in every environment (perf testing / staging)
# "never"  = individual scripts always (live-reload during prod debugging)
assets = "auto"

CLI overrides for one-off testing:

dazzle serve --bundle      # force bundled regardless of manifest
dazzle serve --no-bundle   # force individual regardless of manifest

Smoke-test bundled mode locally before deploying so you catch any project-side custom JS/CSS that doesn't survive bundling:

DAZZLE_ENV=production dazzle serve
# Then load the app in a browser, open DevTools → Network, verify
# /static/dist/dazzle.min.js and /static/dist/dazzle.min.css both
# return 200 with the framework banner at the top.

On Heroku the bundle ships in the wheel; no slug-build step is required. Once you've set DAZZLE_ENV=production and deployed v0.61.141 or later, every page automatically loads the bundle. If you ever need to revert to individual scripts (e.g. to reproduce a dev-mode bug), set assets = "never" in dazzle.toml or pass --no-bundle to dazzle serve.

Trade-offs

Mode Pros Cons
Bundled One CSS + one JS request; smaller wire transfer; faster first paint on cold connections Source maps less granular; live-reload no longer per-file
Individual Per-file source maps; live-reload reloads only what changed ~24 separate requests; slower cold first paint on real RTT

The default auto mode picks bundled in production and individual in dev — appropriate for most projects. Override only if you've measured a specific reason.

File Storage

Heroku's filesystem is ephemeral. For file uploads, configure S3:

heroku config:set DAZZLE_FILE_STORAGE=s3
heroku config:set AWS_ACCESS_KEY_ID=...
heroku config:set AWS_SECRET_ACCESS_KEY=...
heroku config:set AWS_S3_BUCKET=my-dazzle-uploads

Framework Version Pinning

Pin the framework version in dazzle.toml to prevent unexpected changes:

[project]
name = "my-app"
version = "1.0.0"
framework_version = "~0.38"

The server refuses to start if the installed version doesn't match.

Backups

Heroku Postgres provides automatic daily backups. For manual backups:

# Heroku's built-in backup
heroku pg:backups:capture

# Dazzle's backup (includes uploads and metadata)
heroku run dazzle backup create
heroku run dazzle backup create --output /tmp/backup.tar.gz

Custom Domains and SSL

heroku domains:add app.example.com
# SSL is automatic with ACM (Automated Certificate Management)
heroku certs:auto:enable

Monitoring

# Live logs
heroku logs --tail

# Metrics dashboard
heroku open --app my-dazzle-app
# Navigate to "Metrics" tab in Heroku Dashboard

Troubleshooting

Port already in use: Heroku assigns $PORT dynamically. Never hardcode the port.

Connection pool exhaustion: Check heroku pg:info for connection count. Reduce DAZZLE_DB_POOL_MAX or scale down workers.

Migration conflicts: The advisory lock prevents concurrent migrations across workers. If a migration hangs, check heroku pg:locks.

Memory issues: Monitor with heroku logs --tail | grep "Memory quota". Reduce WEB_CONCURRENCY if needed.