This guide covers deploying a Suprnova application to a VPS using Hetzner Cloud. The same principles apply to any single-box host - Linode, Vultr, AWS EC2, or a dedicated server you already own. Choose this path when you want full control of the box, predictable monthly cost, and the ability to colocate Postgres / Redis on the same machine.
Throughout the guide we use myapp as the project name and myapp.com as the domain - substitute your own.
Prerequisites
- A VPS running Ubuntu 22.04 or Debian 12
- SSH access to your server
- A domain name pointed to your server's IP address
- A Suprnova project - either a working source tree, or a Dockerfile generated with
suprnova docker:init(see Docker)
Server Setup
1. Create a VPS
- Go to Hetzner Cloud Console
- Create a new project and add a server
- Choose Ubuntu 22.04 as the image
- Select your server size (CX11 is fine for small apps)
- Add your SSH key for secure access
2. Initial Server Configuration
SSH into your server and run initial setup:
# Update packages
&&
# Create a non-root user for your app
# Install required packages
3. Configure PostgreSQL
# Create database and user
Tip:
For production, consider using a managed database service like Hetzner's upcoming managed PostgreSQL, or services like Neon, Supabase, or AWS RDS for better reliability and backups.
Deploy Options
Choose one of the following deployment methods. Each one ends with a binary (or container) named app sitting at /opt/myapp/app, which the systemd unit below knows how to run.
Option A: Build Locally
Build on your machine and upload the binary. Replace myapp with your actual project name - cargo build names the binary after the [package].name in Cargo.toml:
# On your local machine - cross-compile for Linux (if on macOS)
# Or build with Docker for Linux (the Dockerfile renames the binary to `app`)
# Upload to the server, renaming to `app` on landing
# or, if you went the Docker route:
Option B: Build on Server
Install Rust 1.91.1+ (Suprnova uses the 2024 edition) and build directly on the server:
# Install Rust
|
# Clone, build, and place the binary at the standard path
Option C: Use Docker
Run your app in a Docker container - the scaffolded Dockerfile already names the runtime binary app (see Docker):
# Install Docker
|
# Pull and run your image
If you went with Docker, skip past the systemd section to Caddy Reverse Proxy - Docker handles process supervision.
Environment Configuration
First, generate a production APP_KEY on the server (or locally - the value is what matters). APP_KEY is a 32-byte AES-256 key used by suprnova::Crypt for session cookies and signed URLs. Suprnova fails closed at boot when APP_ENV is not local/dev/test and APP_KEY is unset - so this is non-optional in production:
# -> APP_KEY=base64-url-safe-32-bytes
Then write the env file:
# Secure the file - only the app user should be able to read it
See Configuration for the full env surface and how it becomes typed config.
systemd Services
A Suprnova binary supports multiple commands - ./app (serve, with auto-migrate), ./app schedule:work (scheduler daemon), ./app queue:work (queue worker), ./app workflow:work (workflow runner). Each long-running process gets its own systemd unit using the same binary and env file.
Web Server Service
Create /etc/systemd/system/myapp.service:
[Unit]
Description=Suprnova Application
After=network.target postgresql.service redis.service
Requires=postgresql.service
[Service]
Type=simple
User=app
Group=app
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/app
Restart=always
RestartSec=5
# Environment
EnvironmentFile=/opt/myapp/.env.production
# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/opt/myapp
[Install]
WantedBy=multi-user.target
The default ExecStart=/opt/myapp/app runs serve with auto-migration. If you prefer migrations to be a separate deploy step, use ExecStart=/opt/myapp/app serve --no-migrate and run ./app migrate from your deploy script before flipping the binary.
Scheduler Service
If your app has tasks registered via Schedule::call(...) (see the Scheduling chapter), run exactly one scheduler process to avoid duplicate task execution. Create /etc/systemd/system/myapp-scheduler.service:
[Unit]
Description=Suprnova Scheduler
After=network.target myapp.service
Requires=myapp.service
[Service]
Type=simple
User=app
Group=app
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/app schedule:work
Restart=always
RestartSec=5
# Environment
EnvironmentFile=/opt/myapp/.env.production
# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/opt/myapp
[Install]
WantedBy=multi-user.target
Queue Worker (optional)
If you dispatch jobs to a queue, add /etc/systemd/system/myapp-queue.service:
[Unit]
Description=Suprnova Queue Worker
After=network.target myapp.service
Requires=myapp.service
[Service]
Type=simple
User=app
Group=app
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/app queue:work
Restart=always
RestartSec=5
EnvironmentFile=/opt/myapp/.env.production
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/opt/myapp
[Install]
WantedBy=multi-user.target
You can scale queue workers horizontally - multiple myapp-queue.service instances on the same or different boxes is safe.
Enable and Start Services
# Reload systemd after writing unit files
# Enable services so they start on boot
# Start them now
# Verify
Caddy Reverse Proxy
Caddy automatically handles HTTPS certificates with Let's Encrypt.
Install Caddy
|
|
Configure Caddy
Edit /etc/caddy/Caddyfile:
myapp.com {
reverse_proxy localhost:8765
# Enable compression
encode gzip
# Logging
log {
output file /var/log/caddy/myapp.log
}
}
Replace myapp.com with your actual domain.
Start Caddy
Caddy will automatically obtain and renew SSL certificates.
Health Checks
Suprnova ships a built-in /_suprnova/health endpoint that short-circuits before the middleware chain and never collides with your routes:
Check Database Connectivity
Add ?db=true to also verify the database:
Healthy response (HTTP 200):
If the database check fails, the endpoint flips to HTTP 503 with "status": "degraded" and a "database_error" field - wire this into a livenessProbe / readinessProbe style health check so the load balancer can remove an unhealthy instance from rotation.
External Monitoring
Use the health endpoint with monitoring services:
- UptimeRobot: Add HTTP monitor for
https://myapp.com/_suprnova/health - Better Stack (formerly Better Uptime): Configure health check endpoint with the 503 trigger
- Prometheus / Grafana: Scrape the JSON body for
status+databasefields
Deployment Script
Create a deployment script for atomic updates. Replace myapp with your project name (the [package].name in Cargo.toml) - that's what cargo build names the output binary:
#!/bin/bash
# deploy.sh - Run on your local machine
PROJECT="myapp" # the Cargo package name
SERVER="root@your-server"
APP_PATH="/opt/myapp"
BIN="target/x86_64-unknown-linux-gnu/release/"
Make it executable:
Logs and Monitoring
View Logs
# Web server logs
# Scheduler logs
# Caddy access logs
Log Rotation
systemd's journald handles log rotation automatically. For long-term storage, consider:
- Loki + Grafana: Self-hosted log aggregation
- Papertrail: Cloud-based logging service
- Logtail: Simple log management
Firewall Configuration
Secure your server with UFW:
# Allow SSH
# Allow HTTP/HTTPS (Caddy)
# Enable firewall
Warning:
Never expose port 8765 directly. Always use Caddy as a reverse proxy to handle SSL and security headers.
Scaling
A single Suprnova binary is very efficient - a small VPS handles a surprising amount of traffic before you need to scale out. When you do:
Vertical Scaling
Upgrade the VPS to a larger instance for more CPU/memory. The binary, env file, and systemd units come with you unchanged.
Horizontal Scaling
For multiple application instances:
- Set up a load balancer (Hetzner Load Balancer, HAProxy, or Caddy on a dedicated node)
- Move Postgres to a managed service or a dedicated node so app boxes are stateless
- Move sessions, cache, and broadcasting to Redis so any app instance can serve any request
- Deploy multiple app instances; each one safely runs its own auto-migrate on boot (the migration runner takes a lock so concurrent boots don't collide)
- Keep one scheduler (
schedule:work) running across the whole fleet - queue workers are safe to run in parallel, the scheduler isn't
Why Suprnova diverges
Laravel typically runs PHP-FPM behind nginx, with cron triggering schedule:run once a minute and Horizon (or supervisord) managing queue workers. Suprnova collapses this into one binary with subcommands. ./app is a long-lived Tokio process - it doesn't need a process pool in front of it, doesn't need a separate cron, and stays warm across requests. systemd is the supervisor for both the web process and the workers, and Caddy is doing only what nginx couldn't avoid: terminating TLS and proxying.
Sizing
Pick a VPS based on workload, not on a marketing tier name. Hetzner's lineup changes periodically; the sizing logic doesn't:
| Workload | Rough fit |
|---|---|
| Small site, low traffic, SQLite or shared DB | Smallest shared-vCPU instance (1 vCPU / 2 GB) |
| Moderate traffic with Postgres + Redis on the same box | 2 vCPU / 4 GB |
| Heavier API + scheduler + queue workers + Postgres | 2–4 vCPU / 8 GB |
| Production at scale | Dedicated CPU instance, or split DB onto its own node |
Check Hetzner's current pricing for the live catalogue. Suprnova's idle memory footprint is small (single-digit MB), so RAM is mostly database working set plus your domain code.
Troubleshooting
Service Won't Start
Check logs for errors:
Common issues:
- Missing environment variables
- Database connection failed
- Port already in use
Caddy Certificate Errors
Ensure:
- Domain DNS points to your server
- Ports 80 and 443 are open
- No other service is using port 80
Database Connection Issues
Test connection manually:
Health Check Failing
# Check if app is running
# Test health endpoint directly
# Check with database
A 503 response with "status": "degraded" means the app is up but the database health check failed - inspect database_error in the body and check the DATABASE_URL, Postgres logs, and connection limits.
Next
- Deployment Overview - the platform-agnostic story for single-binary deploys
- Docker -
docker:initanddocker:composedetails - Configuration - full env surface and typed config
- Deploy to Railway - PaaS alternative with automatic builds
- Deploy to Digital Ocean - App Platform with managed infrastructure
