Deployment Overview
Ultimo compiles to a single static binary — no runtime, no interpreter, no
node_modules. This makes deployment straightforward on any platform that runs
Linux containers or bare-metal binaries.
Build for release
cargo build --releaseThe optimized binary lands at target/release/<your-app> (typically 10–15 MB).
Copy it anywhere and run it — zero dependencies beyond libc.
Guides
| Platform | Best for |
|---|---|
| Docker | Reproducible builds, any hosting |
| Fly.io | Global edge deployment, simple CLI |
| Railway | Git-push deploys, zero config |
| AWS | Enterprise, ECS/Fargate |
| Google Cloud Run | Serverless containers, scale-to-zero |
| Azure | Container Apps, enterprise Azure |
| DigitalOcean | App Platform, simple PaaS |
| Kubernetes | Full orchestration, multi-cloud |
Production checklist
Before going live:
- Health check endpoint — add a
GET /healthroute returning 200:
app.get("/health", |ctx: Context| async move {
ctx.response().text("ok")
});- Environment variables — don't hardcode secrets; use env vars or a secret manager
let db_url = std::env::var("DATABASE_URL")
.expect("DATABASE_URL must be set");-
Logging — set
RUST_LOG=info(orRUST_LOG=ultimo=info,my_app=debug) -
HTTPS — terminate TLS at the load balancer/reverse proxy (Caddy, nginx, cloud LB) — Ultimo serves HTTP, your infra adds the S
-
Security headers — enable the built-in middleware:
app.use_middleware(ultimo::middleware::builtin::security_headers());- Trust proxy — if behind a load balancer, enable so
client_ip()readsX-Forwarded-For:
app.trust_proxy(true);- IP filtering — restrict access if needed:
use ultimo::middleware::builtin::IpFilter;
app.use_middleware(IpFilter::allow(&["10.0.0.0/8"]).build());-
Graceful shutdown + request timeout — use
listen_with_shutdownsoSIGTERM(what Docker, Kubernetes, Fly.io, etc. send) drains in-flight requests instead of dropping them, and set arequest_timeout— see Graceful shutdown & timeouts below -
Resource limits — Ultimo binaries are lean; 64–128 MB RAM handles most workloads; set container limits accordingly
Graceful shutdown & timeouts
app.listen(...) runs until the process is killed. For deployments, use
listen_with_shutdown so a stop signal drains work instead of cutting it off:
use std::time::Duration;
let mut app = Ultimo::new();
app.request_timeout(Duration::from_secs(30)); // 408 if a handler takes longer
app.shutdown_grace_period(Duration::from_secs(25)); // keep below your platform's kill timeout
app.listen_with_shutdown("0.0.0.0:3000", ultimo::shutdown_signal()).await?;On SIGTERM or Ctrl+C the server:
- stops accepting new connections;
- sends a
1001 Going Awayclose frame to connected WebSocket clients (websocketfeature); - lets in-flight HTTP requests finish, up to the grace period, then returns.
Set the grace period below your platform's own kill timeout (Kubernetes
terminationGracePeriodSeconds defaults to 30s; Docker's stop to 10s) so
Ultimo exits on its own terms rather than being SIGKILLed.
request_timeoutbounds the time to produce a response. Streamed bodies (ctx.stream,ctx.sse) keep flowing after the handler returns, and WebSocket upgrades aren't covered — so long-lived connections aren't cut off. It applies to the whole app; a handler that legitimately needs longer (e.g. long-polling) should return a stream or SSE channel instead of blocking.- Upgraded WebSocket connections are told to close but aren't awaited by the grace period — clients are expected to honor the close frame.
Environment variables
Ultimo apps typically read configuration from environment variables. Here's a common pattern:
use std::env;
let port: u16 = env::var("PORT")
.unwrap_or_else(|_| "3000".to_string())
.parse()
.expect("PORT must be a number");
let host = env::var("HOST").unwrap_or_else(|_| "0.0.0.0".to_string());