Digital Ocean has two production targets that suit a Suprnova app: App Platform (a managed Docker PaaS - push and forget) and a Droplet (your own VPS, you manage everything). This chapter walks through both. Use App Platform when you want managed databases, automatic deploys, and SSL handled for you. Use a Droplet when you want full control, already run other services on the box, or want to keep the bill flat regardless of traffic.
Prerequisites
- A Digital Ocean account
- A Suprnova project with a Dockerfile - generate one with:
- An
APP_KEYfor production. Generate one and keep it somewhere safe:
Suprnova fails closed on boot whenAPP_ENVis anything other thanlocal/development/testingandAPP_KEYis unset. - A git repository (GitHub or GitLab) - required for App Platform; for Droplets you can also push a prebuilt image to a registry.
App Platform
App Platform builds your Dockerfile, runs the single Suprnova binary, and gives you a managed Postgres if you want one.
1. Create the app
- Go to Digital Ocean Apps.
- Click Create App, connect GitHub/GitLab, and pick the repo and branch.
- App Platform auto-detects the
Dockerfileat the repo root.
2. Configure the web service
| Setting | Value |
|---|---|
| Resource type | Web Service |
| HTTP port | 8765 |
| Run command | leave empty - the Dockerfile's CMD runs ./app |
| Health check (HTTP path) | /_suprnova/health/live |
The default Suprnova binary runs serve with auto-migrations, so the
container will run migrations on startup and then bind the listener.
3. Add a managed Postgres
- Add Resource -> Database -> PostgreSQL.
- Pick a plan (Dev Database for testing; a Production plan for real traffic).
App Platform injects DATABASE_URL into every component automatically
via the ${db.DATABASE_URL} binding.
4. Environment variables
In the Environment Variables section of your web component, set:
| Variable | Value | Notes |
|---|---|---|
APP_ENV |
production |
triggers the fail-closed APP_KEY check |
APP_KEY |
output of suprnova key:generate --show |
mark as encrypted |
SERVER_HOST |
0.0.0.0 |
bind to all interfaces |
SERVER_PORT |
8765 |
matches the Dockerfile's EXPOSE |
APP_URL |
https://your-app.ondigitalocean.app |
used by Inertia + signed URLs |
DATABASE_URL is provided automatically by the managed database
binding; do not set it manually.
If you use Redis for cache/sessions, add a managed Redis cluster and
set REDIS_URL to its binding value (${redis.REDIS_URL}).
5. Deploy
Click Create Resources. The first build takes a few minutes (Rust release build + frontend build); subsequent builds use the Dockerfile layer cache and run much faster.
Add a scheduler worker
Scheduled tasks (#[derive(Task)] handlers registered via
Schedule::call) need a long-lived process. Add a Worker component
that runs the same image with a different command:
- Create -> Add Resource -> Detect from source code, select the same repository.
- Set resource type to Worker.
- Run command:
- The worker inherits env vars from the app, including
DATABASE_URLandAPP_KEY.
Workers don't receive HTTP traffic. Run exactly one worker instance - multiple schedulers would run each task multiple times.
For queue workers (./app queue:work) the pattern is identical;
you can usually run more than one queue worker safely because the
queue driver coordinates which worker takes which job. See
Queues.
App spec (infrastructure as code)
For repeatable deploys, commit a .do/app.yaml:
name: my-suprnova-app
services:
- name: web
dockerfile_path: Dockerfile
github:
repo: your-username/your-repo
branch: main
deploy_on_push: true
http_port: 8765
instance_count: 1
instance_size_slug: basic-xxs
health_check:
# Liveness only - App Platform restarts the container when this
# fails, so it must not depend on Postgres. See the health-check
# note under Troubleshooting.
http_path: /_suprnova/health/live
envs:
- key: APP_ENV
value: production
- key: APP_KEY
scope: RUN_TIME
type: SECRET
value: ${APP_KEY}
- key: SERVER_HOST
value: 0.0.0.0
- key: SERVER_PORT
value: "8765"
- key: APP_URL
value: https://your-app.ondigitalocean.app
- key: DATABASE_URL
scope: RUN_TIME
value: ${db.DATABASE_URL}
workers:
- name: scheduler
dockerfile_path: Dockerfile
github:
repo: your-username/your-repo
branch: main
deploy_on_push: true
instance_count: 1
instance_size_slug: basic-xxs
run_command: ./app schedule:work
envs:
- key: APP_ENV
value: production
- key: APP_KEY
scope: RUN_TIME
type: SECRET
value: ${APP_KEY}
- key: DATABASE_URL
scope: RUN_TIME
value: ${db.DATABASE_URL}
databases:
- name: db
engine: PG
version: "16"
size: db-s-dev-database
Deploy with the doctl CLI:
Set the secret APP_KEY separately via the Apps UI or:
Custom domain
In Settings -> Domains -> Add Domain, enter your domain and follow the DNS instructions. App Platform issues and renews a Let's Encrypt certificate automatically.
After the domain is live, update APP_URL to match - Inertia uses it
for the X-Inertia-Location header and signed URLs use it for the
hash input.
Scaling
- Horizontal: bump Instance Count on the web service. Each instance shares the managed Postgres; multiple instances running auto-migrations on startup is safe - Suprnova uses SeaORM's advisory-locked migrator.
- Vertical: change Instance Size. The Rust binary is happy on the smallest slug for low-traffic apps; bump up when you start serving WebSockets or long-lived connections at scale.
Keep the scheduler worker at instance count 1.
Droplet (VPS)
A Droplet is the path when you want to run Suprnova on your own VPS. The mechanics are identical to any other Linux VPS - systemd service, Caddy reverse proxy, managed or self-hosted Postgres. The Hetzner VPS chapter is the canonical walkthrough for that pattern; everything there applies verbatim on a Droplet. The only differences worth calling out:
- Image: pick Ubuntu 24.04 or Debian 12 in the Droplet console.
- Database: you can use Digital Ocean's Managed Databases for
Postgres / MySQL / Redis instead of running them on the Droplet -
same
DATABASE_URL/REDIS_URLstory, point them at the managed endpoint and Suprnova doesn't notice the difference. - Backups: enable Droplet snapshots and managed DB daily backups in the DO console.
- Networking: use a DO VPC to keep the Droplet and any managed
databases on a private network; bind the listener to
127.0.0.1and put Caddy in front for TLS.
If you want Docker on a Droplet (instead of a system binary), the docker-compose pattern from Docker drops in cleanly - swap the self-hosted Postgres for the managed database and you're done.
Why Suprnova diverges
Laravel's typical PHP deploy needs PHP-FPM + an opcache + a queue
runner + a scheduler cron entry - at least three moving pieces, each
with its own restart semantics. A Suprnova deploy is a single binary
plus an optional worker process. The binary runs migrations, serves
HTTP, handles WebSockets, and lives behind a reverse proxy. The same
binary, invoked with ./app schedule:work or ./app queue:work, is
your scheduler or queue worker. App Platform's "one image, multiple
components" model fits this naturally - same Dockerfile for every
component, different run_command per role.
Troubleshooting
Build fails
The first thing to check is whether the Dockerfile builds locally:
Common causes when the local build works but App Platform's doesn't:
- Missing build context files: check
.dockerignoreisn't excludingCargo.lockor themigrations/directory. - Out-of-memory during cargo build: bump the build instance size in App Settings -> Resources -> Build. Rust release builds are memory-hungry.
App boots, then crashes on startup
Check the runtime logs in the Runtime Logs tab. The two most common Suprnova boot failures are:
APP_KEY is required when APP_ENV=production- generate one withsuprnova key:generate --showand add it as an encrypted env var.SERVER_HOST=…value invalid - must be0.0.0.0for App Platform, not127.0.0.1(the loopback isn't reachable from the load balancer).
Health check failing
The platform pings /_suprnova/health/live and expects a 200 within the
configured timeout. If it's failing:
-
Confirm the path is
/_suprnova/health/liveexactly (not/health). The older/_suprnova/healthstill works if that is what your spec already names. -
Confirm the port is
8765and matchesSERVER_PORT. -
To tell "can't bind" from "can't reach Postgres", probe the database by hand from the console rather than from the health check:
# Healthy: 200 {"status":"ok","database":"connected"} # Degraded: 503 {"status":"degraded","database":"error"}A degraded response means the app bound but cannot reach Postgres - check the
DATABASE_URLbinding. Don't pass-f: it makes curl exit silently on the 503, which is the case you are trying to read.
Do not put the database probe in the app spec's health_check. App
Platform restarts the container when that check fails, so a database
blip would take the app down with it - the failure mode is a restart
loop during exactly the incident you need the app to survive. See Use
the right probe for the right
question.
Database migrations not running
Migrations run automatically as part of the default ./app boot. If
they're not, check the runtime logs for SeaORM errors. To run them
manually from the App Platform console:
- Open the Console tab on the web component.
- Run
./app migrate.
If you prefer to keep migrations out of the boot path, set the run
command to ./app serve --no-migrate and add a one-shot Job to
the app spec that runs ./app migrate pre-deploy.
Next
- Deployment Overview - the cross-platform deploy primer (binary, migrations, scheduler, health)
- Docker - what
suprnova docker:initanddocker:composegenerate - Configuration - every env var Suprnova reads
- Environment Variables - full reference, including the production-required ones
- Deploy to Hetzner VPS - Droplet walkthrough applies here verbatim
