Deployment Guide¶
Production deployment for HyperDjango applications: building, configuring, running as a service, and pre-flight checks.
Complete working example: See
examples/deployment/for a ready-to-use deployment kit with systemd unit, nginx config, env template, logrotate config, health probes, and step-by-step instructions inDEPLOY.md.
Table of Contents¶
- Production Build
- Environment Configuration
- Running in Production
- systemd Service
- Docker Deployment
- Reverse Proxy with nginx
- Pre-Flight Checks
- Database Setup
- Static Files
- Security Checklist
- Monitoring
- Migration Notes for Django Users
Production Build¶
The native Zig extension must be compiled with release optimizations for production:
This compiles with Zig's ReleaseFast mode, producing a .so (Linux) or .dylib (macOS) with:
- Full SIMD optimizations for JSON parsing, validation, and string operations
- Optimized HTTP server with 24-thread pool
- Radix trie router (808ns per route resolution)
- Prepared statement caching for the pg.zig PostgreSQL driver
The debug build (uv run hyper-build) includes bounds checking and debug symbols. Never use it in production.
Environment Configuration¶
HyperDjango reads configuration from environment variables with the HYPER_ prefix:
# Required
HYPER_SECRET_KEY=your-256-bit-secret-key-here
HYPER_DATABASE_URL=postgres://user:password@dbhost:5432/mydb
# Recommended
HYPER_DEBUG=false
HYPER_ALLOWED_HOSTS=myapp.com,api.myapp.com
# Optional
HYPER_POOL_SIZE=0 # 0 = auto (CPU cores x 2)
HYPER_CONNECT_TIMEOUT=10000 # ms
HYPER_QUERY_TIMEOUT=30000 # ms
HYPER_MAX_BODY_SIZE=10485760 # 10MB
.env File Support¶
Place a .env file in the project root:
# .env
SECRET_KEY=your-256-bit-secret-key-here
DATABASE_URL=postgres://user:password@localhost:5432/mydb
DEBUG=false
Application Configuration¶
# app.py
import os
from hyperdjango import HyperApp
app = HyperApp(
title="My Production App",
database=os.environ["HYPER_DATABASE_URL"],
debug=False,
max_body_size=10 * 1024 * 1024, # 10MB
)
Full Environment Variable Reference¶
| Variable | Default | Description |
|---|---|---|
HYPER_SECRET_KEY |
None | HMAC signing key for sessions and CSRF |
HYPER_DATABASE_URL |
None | PostgreSQL connection string |
HYPER_DEBUG |
"false" |
Debug mode (enables detailed errors, hot reload) |
HYPER_ALLOWED_HOSTS |
"*" |
Comma-separated allowed hostnames |
HYPER_POOL_SIZE |
0 |
DB connection pool size (0 = auto) |
HYPER_CONNECT_TIMEOUT |
10000 |
TCP connect timeout in ms |
HYPER_QUERY_TIMEOUT |
0 |
PostgreSQL statement_timeout in ms |
HYPER_PREPARED_STATEMENTS |
"true" |
Enable prepared statement caching |
HYPER_STATEMENT_CACHE_SIZE |
256 |
LRU prepared statement cache size |
HYPER_POOL_MAX_QUERIES |
10000 |
Rotate connections after N queries |
HYPER_POOL_MAX_LIFETIME |
3600 |
Max connection age in seconds |
Running in Production¶
Direct Execution¶
# app.py
if __name__ == "__main__":
app.run(
host="0.0.0.0",
port=8000,
prod=True, # Enables production validation checks
)
The prod=True flag enables:
- Production validation (secret key set, debug disabled, etc.)
- Graceful shutdown on SIGTERM/SIGINT
- No hot reload or debug error pages
Binding to Unix Socket¶
For reverse proxy setups (nginx):
systemd Service¶
Create /etc/systemd/system/myapp.service:
[Unit]
Description=MyApp HyperDjango Application
After=network.target postgresql.service
Requires=postgresql.service
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
Environment="HYPER_SECRET_KEY=your-production-secret-key"
Environment="HYPER_DATABASE_URL=postgres://myapp:password@localhost:5432/myapp"
Environment="HYPER_DEBUG=false"
ExecStart=/opt/myapp/.venv/bin/uv run hyper run
ExecReload=/bin/kill -HUP $MAINPID
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/myapp/media /opt/myapp/static
PrivateTmp=true
ProtectKernelTunables=true
ProtectKernelModules=true
[Install]
WantedBy=multi-user.target
Enable and start:
sudo systemctl daemon-reload
sudo systemctl enable myapp
sudo systemctl start myapp
sudo systemctl status myapp
# View logs
sudo journalctl -u myapp -f
Docker Deployment¶
Dockerfile¶
# Build stage: compile Zig native extension
FROM python:3.14-slim AS builder
# Install Zig
RUN apt-get update && apt-get install -y wget xz-utils && \
wget -q https://ziglang.org/download/0.15.0/zig-linux-x86_64-0.15.0.tar.xz && \
tar xf zig-linux-x86_64-0.15.0.tar.xz && \
mv zig-linux-x86_64-0.15.0 /opt/zig
ENV PATH="/opt/zig:$PATH"
# Install uv
RUN pip install uv
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen
COPY . .
RUN uv run hyper-build --install --release
# Runtime stage
FROM python:3.14-slim
RUN useradd -r -s /bin/false appuser
WORKDIR /app
COPY --from=builder /app /app
USER appuser
EXPOSE 8000
ENV HYPER_DEBUG=false
ENV HYPER_SECRET_KEY=change-me-in-production
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
CMD ["uv", "run", "hyper", "run"]
docker-compose.yml¶
services:
db:
image: postgres:16
environment:
POSTGRES_DB: myapp
POSTGRES_USER: myapp
POSTGRES_PASSWORD: secure-db-password
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U myapp"]
interval: 5s
timeout: 5s
retries: 5
app:
build: .
ports:
- "8000:8000"
environment:
HYPER_DATABASE_URL: postgres://myapp:secure-db-password@db:5432/myapp
HYPER_SECRET_KEY: your-production-secret-key
HYPER_DEBUG: "false"
depends_on:
db:
condition: service_healthy
restart: always
volumes:
pgdata:
Reverse Proxy with nginx¶
In production, HyperDjango should sit behind a reverse proxy. The Zig HTTP server is fast and correct, but a production reverse proxy handles concerns that application servers shouldn't: TLS termination, static file serving with sendfile(2), request buffering against slow clients, connection limits against DDoS, HTTP/2 multiplexing, and access logging at the edge.
Why a Reverse Proxy¶
| Concern | nginx handles it | HyperDjango handles it |
|---|---|---|
| TLS/SSL termination | Yes (OpenSSL, hardware offload) | No |
| HTTP/2 and HTTP/3 | Yes | No (HTTP/1.1 only) |
| Slow client buffering | Yes (spools full request before proxying) | No (reads directly from socket) |
| Connection rate limiting | Yes (limit_conn, limit_req) | Yes (RateLimitMiddleware) |
| Static file serving | Yes (sendfile, no copy to userspace) | Yes (but wastes app threads) |
| Graceful reload | Yes (binary upgrade, zero downtime) | Yes (SIGTERM drain) |
Architecture: Client → nginx (TLS, HTTP/2, static files) → HyperDjango (application logic, DB queries, templates)
TCP Proxy (localhost)¶
The simplest setup. nginx connects to HyperDjango on a TCP port:
upstream hyperdjango {
server 127.0.0.1:8000;
# Keep persistent connections to the backend.
# The Zig HTTP server handles keepalive natively.
keepalive 32;
}
server {
listen 80;
server_name myapp.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name myapp.com;
# TLS — Let's Encrypt via certbot
ssl_certificate /etc/letsencrypt/live/myapp.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/myapp.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# HSTS — tell browsers to always use HTTPS
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# Static files — served directly by nginx, never hits Python
location /static/ {
alias /opt/myapp/staticfiles/;
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# WebSocket routes — requires HTTP/1.1 upgrade
location /ws/ {
proxy_pass http://hyperdjango;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400s; # Keep WS connections alive for 24h
}
# Health check for upstream monitoring
location = /health {
proxy_pass http://hyperdjango;
proxy_set_header Host $host;
access_log off;
}
# Application — all other requests
location / {
proxy_pass http://hyperdjango;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection ""; # Enable keepalive to upstream
# Match HyperDjango's MAX_BODY_SIZE (default 10MB)
client_max_body_size 10m;
# Buffer the full response before sending to slow clients.
# This frees the Zig worker thread immediately.
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 8k;
}
}
Unix Socket Proxy (same host, lower latency)¶
When nginx and HyperDjango run on the same machine, Unix sockets eliminate TCP overhead (no SYN/ACK, no Nagle's algorithm, no port allocation). This saves ~50-100us per request under high concurrency.
HyperDjango config:
# app.py — bind to Unix socket instead of TCP
app.run(unix_socket="/run/myapp/myapp.sock", prod=True)
Or via CLI:
nginx config — only the upstream block changes:
upstream hyperdjango {
server unix:/run/myapp/myapp.sock;
keepalive 32;
}
# The rest of the server {} block is identical to the TCP example above.
Permission setup — nginx and HyperDjango must both access the socket:
# Create socket directory with shared group
sudo mkdir -p /run/myapp
sudo chown myapp:www-data /run/myapp
sudo chmod 750 /run/myapp
TLS with Let's Encrypt¶
# Install certbot
sudo apt install certbot python3-certbot-nginx
# Issue certificate (nginx must be running)
sudo certbot --nginx -d myapp.com -d www.myapp.com
# Auto-renewal (certbot installs a systemd timer by default)
sudo systemctl status certbot.timer
Certbot modifies your nginx config to add ssl_certificate and ssl_certificate_key directives and sets up automatic renewal.
Rate Limiting: nginx vs HyperDjango¶
Use both layers — they serve different purposes:
| Layer | Purpose | Key type | Storage |
|---|---|---|---|
nginx limit_req |
Protect the server from volumetric attacks | IP address | Shared memory (fast, no DB) |
HyperDjango RateLimitMiddleware |
Per-user/per-tenant business limits | User ID, API key, org | PostgreSQL UNLOGGED (shared across servers) |
# nginx layer: 50 req/sec burst with nodelay (volumetric protection)
limit_req_zone $binary_remote_addr zone=api:10m rate=50r/s;
server {
location /api/ {
limit_req zone=api burst=100 nodelay;
proxy_pass http://hyperdjango;
# ... proxy headers ...
}
}
# HyperDjango layer: 120 req/min per authenticated user (business logic)
app.use(RateLimitMiddleware(max_requests=120, window=60))
Full Production Config¶
See examples/deployment/nginx.conf for a complete, annotated production config combining all of the above.
Pre-Flight Checks¶
Run hyper doctor before deploying to catch configuration issues:
This runs 30 checks across 7 categories:
| Category | Checks |
|---|---|
| build | Native extension compiled, Zig version, release mode |
| python | Python version, free-threading, GIL status |
| database | Connection, pool health, prepared statements |
| perf | Cache config, thread pool, debug mode off |
| config | Settings validation, secret key strength |
| filesystem | File permissions, path accessibility |
| security | Secret key set, debug disabled, CORS configured |
CI Integration¶
# JSON output for CI pipelines
uv run hyper doctor --json
# Check specific category
uv run hyper doctor --category build
uv run hyper doctor --category security
Example CI step (GitHub Actions):
- name: Pre-flight checks
run: |
uv run hyper-build --install --release
uv run hyper doctor --json > doctor-report.json
if jq -e '.failed > 0' doctor-report.json; then
echo "Doctor checks failed"
cat doctor-report.json
exit 1
fi
Database Setup¶
Run Migrations¶
Create Admin User¶
Auth Tables¶
If using the auth system, ensure tables exist:
from hyperdjango.auth import PermissionChecker
from hyperdjango.auth.db_sessions import DatabaseSessionStore
checker = PermissionChecker(app.db)
await checker.ensure_tables()
store = DatabaseSessionStore(app.db)
await store.ensure_table()
Static Files¶
Collect Static Files¶
This copies static files from all configured directories into a single output directory with content-hashed filenames for cache busting.
Production Static Serving¶
In production, serve static files from a CDN or reverse proxy (nginx), not from the application server:
nginx:
server {
listen 80;
server_name myapp.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name myapp.com;
ssl_certificate /etc/letsencrypt/live/myapp.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/myapp.com/privkey.pem;
location /static/ {
alias /opt/myapp/staticfiles/;
expires 1y;
add_header Cache-Control "public, immutable";
}
location /ws/ {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 10m;
}
}
Security Checklist¶
Before deploying to production, verify:
-
HYPER_SECRET_KEYis set to a strong, unique value (256+ bits) -
HYPER_DEBUGis"false" - Database uses a strong password with limited network access
- Native extension built with
--releaseflag - HTTPS configured (via reverse proxy or load balancer)
- CORS origins restricted to your domains
- Rate limiting enabled
- Security headers middleware active (HSTS, CSP, X-Frame-Options)
- CSRF protection enabled for browser-facing endpoints
-
hyper doctorpasses all checks - Session cookies set to
secure=Trueandhttponly=True - Database connection uses SSL in production
- File upload size limited via
max_body_size - Logging configured for production (JSON format, no debug output)
Monitoring¶
Health Checks¶
Register health check endpoints:
@app.health_check("database")
async def check_db():
"""Returns True if DB is responsive."""
try:
await app.db.query("SELECT 1")
return True
except Exception:
return False
@app.health_check("disk")
async def check_disk():
"""Returns True if disk space is sufficient."""
import shutil
usage = shutil.disk_usage("/")
return usage.free > 1_000_000_000 # 1GB free
These are accessible at:
GET /health/live-- Liveness probe (always 200 if server is running)GET /health/ready-- Readiness probe (runs all registered health checks)
Performance Dashboard¶
from hyperdjango.performance import PerformanceMiddleware
app.use(PerformanceMiddleware(
slow_query_threshold_ms=100,
enable_dashboard=True, # Accessible at /debug/performance
))
Pool Statistics¶
stats = await app.db.pool_stats()
print(stats) # Pool size, active connections, idle connections, wait queue
Migration Notes for Django Users¶
| Django | HyperDjango |
|---|---|
gunicorn myapp.wsgi |
uv run hyper run |
python manage.py collectstatic |
uv run hyper collectstatic |
python manage.py migrate |
uv run hyper migrate |
python manage.py createsuperuser |
uv run hyper createsuperuser |
settings.py with DATABASES dict |
HYPER_DATABASE_URL env var |
SECRET_KEY in settings |
HYPER_SECRET_KEY env var |
ALLOWED_HOSTS list |
HYPER_ALLOWED_HOSTS comma-separated |
| PostgreSQL UNLOGGED tables for caching/sessions | Built-in |
| External task broker for background jobs | Built-in @task decorator |
manage.py check --deploy |
uv run hyper doctor |
Production Checklist¶
A comprehensive checklist of 25 items to verify before every production deployment. Run through each category systematically.
Critical Security¶
-
SECRET_KEY is set and strong.
HYPER_SECRET_KEYmust be a random value of at least 256 bits (64 hex characters). Load from an environment variable or secrets manager, never commit to source control. -
DEBUG is disabled.
HYPER_DEBUG=false. Debug mode leaks source code, local variables, settings, and internal paths in error responses. -
ALLOWED_HOSTS is restrictive.
HYPER_ALLOWED_HOSTSmust list only your actual domain names (e.g.,myapp.com,api.myapp.com). Never use*in production. -
HTTPS is enforced. All traffic must go through TLS. Configure your reverse proxy (nginx) to redirect HTTP to HTTPS. Set
HSTSheaders to prevent downgrade attacks. -
HSTS is enabled. Add
Strict-Transport-Security: max-age=31536000; includeSubDomainsviaSecurityHeadersMiddleware(hsts=True). Once set, browsers will refuse plain HTTP for the configured duration. -
CSRF protection is active. For browser-facing endpoints that accept POST/PUT/DELETE, ensure CSRF middleware is enabled. API endpoints using API keys or OAuth2 tokens do not need CSRF.
-
Session cookies are secure. Configure
SessionAuthwithcookie_secure=True(only sent over HTTPS),cookie_httponly=True(not accessible to JavaScript), andcookie_samesite="Lax"or"Strict". -
Admin access is restricted. HyperAdmin should not be accessible from the public internet. Restrict by IP, require VPN, or at minimum require
is_staff=Trueauthentication. -
CORS origins are explicit.
CORSMiddleware(origins=["https://myapp.com"])-- never useorigins=["*"]in production. -
Security headers are configured. Enable
SecurityHeadersMiddlewareto setX-Content-Type-Options: nosniff,X-Frame-Options: DENY,Referrer-Policy, andPermissions-Policy.
Database¶
-
Database password is strong and not in source control. Load from
HYPER_DATABASE_URLenvironment variable. The database user should have only the privileges needed by the application. -
Database connection uses SSL. Add
?sslmode=requireto the connection string for cloud databases. For self-hosted, configuresslmode=verify-fullwith the CA certificate. -
Connection pool is tuned. Set
HYPER_POOL_SIZEappropriate to your workload and PostgreSQLmax_connections. The default (auto = CPU cores x 2) is a reasonable starting point. -
Prepared statement cache is enabled.
HYPER_PREPARED_STATEMENTS=true(default). This caches query plans and avoids repeated parsing -- 33% faster for repeated queries. -
Query timeout is set.
HYPER_QUERY_TIMEOUT=30000(30 seconds) prevents runaway queries from holding connections indefinitely. -
Database backups are configured and tested. Use
pg_dumpon a schedule, or your cloud provider's automated backup feature. Test restoring from a backup at least once.
Application¶
-
Native extension is built with --release. Run
uv run hyper-build --install --release. The debug build includes bounds checking and debug symbols that slow production workloads significantly. -
Static files are collected. Run
uv run hyper collectstaticbefore deployment. Serve static files from a CDN or reverse proxy with long-lived cache headers (Cache-Control: public, max-age=31536000, immutable). -
Rate limiting is enabled. Configure
RateLimitMiddlewareor the rule-based rate limiter to protect against abuse. Set appropriate limits per IP, per user, and per endpoint. -
File upload size is limited. Set
max_body_sizeon the app (default 10MB). Match this with your reverse proxy'sclient_max_body_size.
Logging and Monitoring¶
-
Logging is configured for production. Use JSON-formatted logs (machine-parseable) with appropriate log levels. Disable debug-level logging. Send logs to a centralized log aggregation system.
-
Error reporting is set up. Configure an error monitoring service to capture unhandled exceptions with full context. Do not rely on email notifications for error reporting at scale.
-
Health check endpoints are registered. Register at least a database health check with
@app.health_check("database"). Expose/health/livefor liveness probes and/health/readyfor readiness probes in container orchestrators. -
Monitoring and alerting are active. Monitor response times (p50, p95, p99), error rates, database connection pool utilization, and disk usage. Set alerts for anomalies.
Operational¶
- Graceful shutdown is configured. Use
app.run(prod=True)which handlesSIGTERM/SIGINTgracefully, draining in-flight requests before stopping. In Docker or Kubernetes, ensure the stop grace period is long enough for request draining.
Running the Checklist¶
Automate the most important checks with hyper doctor:
# Run all 30 checks
uv run hyper doctor
# Fail the CI pipeline if any check fails
uv run hyper doctor --json | python -c "
import sys, json
report = json.load(sys.stdin)
if report['failed'] > 0:
print(f'FAILED: {report[\"failed\"]} checks failed')
sys.exit(1)
print(f'OK: all {report[\"passed\"]} checks passed')
"
hyper doctor covers build verification, Python version, database connectivity, pool health, prepared statements, cache configuration, secret key strength, debug mode, and CORS settings.