Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

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 --release

The optimized binary lands at target/release/<your-app> (typically 10–15 MB). Copy it anywhere and run it — zero dependencies beyond libc.

Guides

PlatformBest for
DockerReproducible builds, any hosting
Fly.ioGlobal edge deployment, simple CLI
RailwayGit-push deploys, zero config
AWSEnterprise, ECS/Fargate
Google Cloud RunServerless containers, scale-to-zero
AzureContainer Apps, enterprise Azure
DigitalOceanApp Platform, simple PaaS
KubernetesFull orchestration, multi-cloud

Production checklist

Before going live:

  • Health check endpoint — add a GET /health route 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 (or RUST_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() reads X-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_shutdown so SIGTERM (what Docker, Kubernetes, Fly.io, etc. send) drains in-flight requests instead of dropping them, and set a request_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:

  1. stops accepting new connections;
  2. sends a 1001 Going Away close frame to connected WebSocket clients (websocket feature);
  3. 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.

Notes
  • request_timeout bounds 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());