Deploying a Rocket App
Rocket is a fast, ergonomic, and type-safe web framework for Rust that emphasizes developer experience and compile-time guarantees. Built with modern Rust practices, Rocket combines high performance with strong compile-time safety, comprehensive error handling, and an intuitive API that makes building scalable web applications a pleasure. Its macro system provides elegant routing, automatic serialization, and request guards that prevent entire classes of bugs at compile time.
This guide explains how to deploy a Rocket application to Klutch.sh, both with and without a Dockerfile, along with database configuration, environment setup, persistent storage, security patterns, and production deployment strategies.
Prerequisites
- Rust 1.70 or higher
- Cargo package manager
- Git and a GitHub account
- Klutch.sh account
- Basic knowledge of Rust and web framework concepts
Getting Started: Installing Rust and Creating a Rocket App
-
Install Rust (if not already installed):
Use rustup for version management:
Terminal window curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shsource "$HOME/.cargo/env"rustup default stable -
Create a new Rocket project:
Terminal window cargo new my-rocket-appcd my-rocket-app -
Add dependencies to
Cargo.toml:[package]name = "my-rocket-app"version = "0.1.0"edition = "2021"[dependencies]rocket = { version = "0.5.0", features = ["json"] }serde = { version = "1.0", features = ["derive"] }serde_json = "1.0"tokio = { version = "1.0", features = ["full"] }sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres"] }uuid = { version = "1.0", features = ["v4", "serde"] }chrono = { version = "0.4", features = ["serde"] }dotenv = "0.15"log = "0.4" -
Build the project:
Terminal window cargo build -
Start the development server:
Terminal window cargo runYour app should be running at http://localhost:8000.
Sample Code for Getting Started
Create a complete Rocket application with models, routes, and request guards:
#[macro_use]extern crate rocket;
use rocket::{State, serde::json::Json, http::Status};use serde::{Deserialize, Serialize};use uuid::Uuid;use std::sync::Mutex;use std::collections::HashMap;
#[derive(Serialize, Deserialize, Clone, Debug)]#[serde(crate = "rocket::serde")]pub struct Article { pub id: String, pub title: String, pub content: String, pub published: bool,}
#[derive(Deserialize)]#[serde(crate = "rocket::serde")]pub struct NewArticle { pub title: String, pub content: String,}
pub struct AppState { articles: Mutex<HashMap<String, Article>>,}
#[get("/")]fn index() -> &'static str { "Welcome to Rocket on Klutch.sh"}
#[get("/health")]fn health_check() -> &'static str { "OK"}
#[post("/articles", format = "json", data = "<article>")]fn create_article( article: Json<NewArticle>, state: &State<AppState>,) -> (Status, Json<Article>) { let new_article = Article { id: Uuid::new_v4().to_string(), title: article.title.clone(), content: article.content.clone(), published: false, };
let mut articles = state.articles.lock().unwrap(); articles.insert(new_article.id.clone(), new_article.clone());
(Status::Created, Json(new_article))}
#[get("/articles")]fn list_articles(state: &State<AppState>) -> Json<Vec<Article>> { let articles = state.articles.lock().unwrap(); let list: Vec<Article> = articles.values().cloned().collect(); Json(list)}
#[get("/articles/<id>")]fn get_article(id: String, state: &State<AppState>) -> Result<Json<Article>, Status> { let articles = state.articles.lock().unwrap(); articles .get(&id) .cloned() .map(Json) .ok_or(Status::NotFound)}
#[launch]fn rocket() -> _ { let app_state = AppState { articles: Mutex::new(HashMap::new()), };
rocket::build() .manage(app_state) .mount("/", routes![index, health_check]) .mount("/api", routes![create_article, list_articles, get_article])}Deploying Without a Dockerfile (Using Nixpacks)
Klutch.sh uses Nixpacks to automatically detect and build your Rocket application. Nixpacks analyzes your project and determines the necessary dependencies, build steps, and runtime configuration.
Steps to Deploy with Nixpacks
-
Prepare your Rocket app for production:
Create a
Rocket.tomlfor production configuration:[default]address = "0.0.0.0"port = 8000workers = 4log_level = "normal"[production]address = "0.0.0.0"port = 8000workers = 4log_level = "normal" -
Optimize your
Cargo.tomlfor release builds:[profile.release]opt-level = 3lto = truecodegen-units = 1strip = true -
Commit your code to GitHub:
Terminal window git add .git commit -m "Initial Rocket app setup"git push origin main -
Log in to Klutch.sh dashboard:
Visit https://klutch.sh/app
-
Create a new project:
- Click “Create Project”
- Enter your project name (e.g., “My Rocket App”)
- Select your organization or personal account
-
Create a new app:
- Click “Create App”
- Select your Rocket GitHub repository
- Select the branch (typically
mainormaster) - Select HTTP as the traffic type
- Set the internal port to 8000 (the default Rocket port)
- Choose your desired region, compute power, and number of instances
-
Add environment variables:
In the app creation form, add these essential environment variables:
ROCKET_ENV=productionPORT=8000DATABASE_URL=postgresql://user:password@host:5432/myappREDIS_URL=redis://user:password@host:6379/0LOG_LEVEL=normal -
Deploy:
Click “Create” to deploy. Klutch.sh will automatically:
- Detect your Rust/Rocket project
- Build with
cargo build --release - Start the application on port 8000
Your app will be available at example-app.klutch.sh.
Customizing Build and Start Commands
If you need to customize the build or start command for Nixpacks, add these environment variables:
NIXPACKS_BUILD_CMD=cargo build --releaseNIXPACKS_START_CMD=./target/release/my-rocket-appDeploying With a Dockerfile
For more control over your build process and runtime environment, you can provide a custom Dockerfile. Klutch.sh will automatically detect and use a Dockerfile in your repository’s root directory.
Creating a Multi-Stage Dockerfile
-
Create a
Dockerfilein your project root:# Build stageFROM rust:1.75-slim as builderWORKDIR /app# Install build dependenciesRUN apt-get update && apt-get install -y build-essential pkg-config libssl-dev && rm -rf /var/lib/apt/lists/*# Copy manifestsCOPY Cargo.toml Cargo.lock ./# Build dependencies - this is the caching layerRUN mkdir src && echo "fn main() {}" > src/main.rs && cargo build --release && rm -rf src# Copy source codeCOPY src ./src# Build applicationRUN cargo build --release# Runtime stageFROM debian:bookworm-slimRUN apt-get update && apt-get install -y ca-certificates libssl3 && rm -rf /var/lib/apt/lists/*WORKDIR /app# Copy binary from builderCOPY --from=builder /app/target/release/my-rocket-app .# Copy configurationCOPY Rocket.toml .# Health checkHEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \CMD curl -f http://localhost:8000/health || exit 1# Set environmentENV PORT=8000 ROCKET_ENV=production# Expose portEXPOSE 8000# Start applicationCMD ["./my-rocket-app"] -
Commit the Dockerfile to GitHub:
Terminal window git add Dockerfile Rocket.tomlgit commit -m "Add production Dockerfile"git push origin main -
Log in to Klutch.sh dashboard:
Visit https://klutch.sh/app
-
Create a new app:
- Click “Create App”
- Select your Rocket GitHub repository with the Dockerfile
- Select the branch
- Select HTTP as the traffic type
- Set the internal port to 8000
- Choose region, compute, and instances
-
Add environment variables:
ROCKET_ENV=productionPORT=8000DATABASE_URL=postgresql://user:password@host:5432/myappREDIS_URL=redis://user:password@host:6379/0 -
Deploy:
Click “Create”. Klutch.sh will automatically build your Docker image and deploy your app.
Database Configuration
Rocket applications commonly use PostgreSQL or MySQL for data persistence. Here’s how to configure databases with Klutch.sh.
PostgreSQL Configuration
-
Add SQLx to your
Cargo.tomlwith PostgreSQL support:sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "macros", "migrate"] } -
Create a database module (
src/db.rs):use sqlx::postgres::PgPool;use std::env;pub async fn create_pool() -> Result<PgPool, sqlx::Error> {let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");PgPool::connect(&database_url).await}pub async fn run_migrations(pool: &PgPool) -> Result<(), sqlx::Error> {sqlx::query("CREATE TABLE IF NOT EXISTS articles (id UUID PRIMARY KEY DEFAULT gen_random_uuid(),title VARCHAR NOT NULL,content TEXT NOT NULL,published BOOLEAN DEFAULT false,created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)").execute(pool).await?;Ok(())} -
Use the pool in your Rocket app:
mod db;use rocket::State;#[launch]async fn rocket() -> _ {let pool = db::create_pool().await.expect("Failed to create pool");db::run_migrations(&pool).await.expect("Failed to run migrations");rocket::build().manage(pool).mount("/", routes![/* your routes */])} -
During app creation on Klutch.sh, set:
DATABASE_URL=postgresql://username:password@postgres-host:5432/myapp_production
MySQL Configuration
-
Add SQLx with MySQL support:
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "mysql"] } -
Update your database connection code:
use sqlx::mysql::MySqlPool;pub async fn create_pool() -> Result<MySqlPool, sqlx::Error> {let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");MySqlPool::connect(&database_url).await} -
Set the DATABASE_URL environment variable:
DATABASE_URL=mysql://username:password@mysql-host:3306/myapp_production
Environment Variables
Rocket applications use environment variables for configuration. Here are the essential variables:
ROCKET_ENV=productionPORT=8000DATABASE_URL=postgresql://user:password@host:5432/dbnameREDIS_URL=redis://:password@redis-host:6379/0API_KEY=<your-api-key>JWT_SECRET=<your-jwt-secret>LOG_LEVEL=normalLoading Environment Variables
Use the dotenv crate to load .env files locally:
use dotenv;
fn main() { dotenv::dotenv().ok(); // ... rest of your app}Type-Safe Routing and Request Guards
Rocket’s type system provides compile-time safety for routing and request handling:
Using Request Guards
use rocket::request::{self, FromRequest, Request};use rocket::outcome::Outcome;
pub struct ApiKey(String);
#[rocket::async_trait]impl<'r> FromRequest<'r> for ApiKey { type Error = ();
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> { match request.headers().get_one("X-API-Key") { Some(key) => Outcome::Success(ApiKey(key.to_string())), None => Outcome::Error((rocket::http::Status::Unauthorized, ())), } }}
#[get("/protected")]fn protected_route(_key: ApiKey) -> &'static str { "Access granted!"}Dynamic Route Parameters
#[get("/articles/<id>/<action>")]fn article_action(id: u32, action: String) -> String { format!("Article {} action: {}", id, action)}
#[get("/users/<name>", format = "json")]fn get_user(name: String) -> Json<User> { Json(User { name })}Caching with Redis
Rocket applications can leverage Redis for caching to improve performance:
-
Add Redis crate to
Cargo.toml:redis = { version = "0.25", features = ["aio"] } -
Create a cache utility module (
src/cache.rs):use redis::aio::Connection;use std::env;pub async fn get_redis_connection() -> Result<Connection, redis::RedisError> {let redis_url = env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1".to_string());let client = redis::Client::open(redis_url)?;client.get_async_connection().await}pub async fn cache_get(conn: &mut Connection, key: &str) -> Result<Option<String>, redis::RedisError> {redis::cmd("GET").arg(key).query_async(conn).await}pub async fn cache_set_ex(conn: &mut Connection,key: &str,value: &str,ttl: usize,) -> Result<(), redis::RedisError> {redis::cmd("SETEX").arg(key).arg(ttl).arg(value).query_async(conn).await} -
Use caching in handlers:
use rocket::State;use redis::aio::Connection;#[get("/articles/<id>")]async fn get_cached_article(id: String,redis: &State<Connection>,) -> Result<Json<Article>, Status> {let cache_key = format!("article:{}", id);// Try cache firstif let Ok(Some(cached)) = cache_get(redis, &cache_key).await {return Ok(Json(serde_json::from_str(&cached).unwrap()));}// Fetch from database and cacheif let Ok(article) = fetch_article(&id).await {let serialized = serde_json::to_string(&article).unwrap();let _ = cache_set_ex(redis, &cache_key, &serialized, 3600).await;return Ok(Json(article));}Err(Status::NotFound)} -
Set the REDIS_URL environment variable on Klutch.sh:
REDIS_URL=redis://:password@redis-host:6379/0
Persistent Storage
Rocket applications may need persistent storage for uploaded files or logs. Klutch.sh supports mounting persistent volumes.
Configuring File Storage
-
Create a file handling module (
src/storage.rs):use std::path::Path;use tokio::fs;pub async fn save_file(file_path: &str, content: &[u8]) -> Result<(), std::io::Error> {fs::write(file_path, content).await}pub async fn read_file(file_path: &str) -> Result<Vec<u8>, std::io::Error> {fs::read(file_path).await}pub async fn delete_file(file_path: &str) -> Result<(), std::io::Error> {fs::remove_file(file_path).await} -
Use persistent storage in handlers:
use rocket::data::{Data, ToByteUnit};#[post("/upload", data = "<data>")]async fn upload_file(data: Data<'_>) -> Result<String, std::io::Error> {let storage_path = std::env::var("STORAGE_PATH").unwrap_or_else(|_| "/tmp".to_string());let file_path = format!("{}/uploaded_file_{}", storage_path, uuid::Uuid::new_v4());let stream = data.open(100.megabytes());save_file(&file_path, &stream.into_bytes().await.unwrap().to_vec()).await?;Ok(file_path)} -
On Klutch.sh, attach a persistent volume:
- During app creation, navigate to the “Storage” section
- Click “Add Volume”
- Set mount path to
/app/storage - Allocate size (e.g., 10GB)
-
Set the STORAGE_PATH environment variable:
STORAGE_PATH=/app/storage
Security Best Practices
1. Enforce HTTPS
Configure security headers:
use rocket::http::Header;use rocket::response::Responder;
pub struct SecureResponder<R>(pub R);
impl<'r, 'o: 'r, R: Responder<'r, 'o>> Responder<'r, 'o> for SecureResponder<R> { async fn respond_to(self, request: &'r Request<'_>) -> rocket::response::Result<'o> { let mut response = self.0.respond_to(request).await?; response.set_header(Header::new("Strict-Transport-Security", "max-age=31536000; includeSubDomains")); response.set_header(Header::new("X-Content-Type-Options", "nosniff")); response.set_header(Header::new("X-Frame-Options", "DENY")); Ok(response) }}2. Implement CORS
Configure CORS for APIs:
use rocket::fairing::{Fairing, Info, Kind};use rocket::response::{self, Responder};use rocket::Request;
pub struct Cors;
#[rocket::async_trait]impl Fairing for Cors { fn info(&self) -> Info { Info { name: "CORS", kind: Kind::Response, } }
async fn on_response<'r>(&self, _request: &'r Request<'_>, response: &mut response::Response<'r>) { response.set_header(Header::new("Access-Control-Allow-Origin", "*")); response.set_header(Header::new("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")); }}3. Use Environment Variables for Secrets
Never hardcode secrets:
let jwt_secret = std::env::var("JWT_SECRET").expect("JWT_SECRET not set");let api_key = std::env::var("API_KEY").expect("API_KEY not set");4. Implement Request Validation
Use Rocket’s built-in request guards and Serde validation:
#[derive(Deserialize)]#[serde(crate = "rocket::serde")]pub struct CreateArticle { #[serde(alias = "title")] pub title: String, #[serde(alias = "content")] pub content: String,}
#[post("/articles", data = "<article>")]fn create_article(article: Json<CreateArticle>) -> String { format!("Created: {}", article.title)}5. Add Request Rate Limiting
Implement rate limiting middleware:
use rocket::fairing::{Fairing, Info, Kind};use std::sync::Mutex;use std::collections::HashMap;use std::time::{Instant, Duration};
pub struct RateLimiter { requests: Mutex<HashMap<String, Vec<Instant>>>,}
impl RateLimiter { pub fn new() -> Self { RateLimiter { requests: Mutex::new(HashMap::new()), } }}6. Secure Credential Management
Use .env files locally and environment variables in production:
# .env (local development only)DATABASE_URL=postgres://localhost/myappJWT_SECRET=development_secret_key7. Enable Structured Logging
Configure logging for security audit trails:
use log::{info, warn, error};
#[post("/articles", data = "<article>")]fn create_article(article: Json<CreateArticle>) -> String { info!("Creating article: {}", article.title); format!("Created: {}", article.title)}Monitoring and Logging
Health Check Endpoint
Add a health check route for monitoring:
#[get("/health")]fn health_check() -> Json<serde_json::json!({ "status": "healthy", "timestamp": chrono::Utc::now().to_rfc3339()}) { Json(serde_json::json!({ "status": "healthy", "timestamp": chrono::Utc::now().to_rfc3339() }))}
fn rocket() -> _ { rocket::build() .mount("/", routes![health_check])}Logging Configuration
Configure logging in your application:
use log::LevelFilter;
#[launch]fn rocket() -> _ { env_logger::builder() .filter_level(LevelFilter::Info) .init();
rocket::build()}Application Performance Monitoring
Integrate with monitoring services using middleware:
use rocket::fairing::{Fairing, Info, Kind};use std::time::Instant;
pub struct TimingFairing;
#[rocket::async_trait]impl Fairing for TimingFairing { fn info(&self) -> Info { Info { name: "Request Timing", kind: Kind::Request | Kind::Response, } }
async fn on_request(&self, request: &mut Request<'_>, _: &mut Data<'_>) { request.local_cache(|| Instant::now()); }
async fn on_response<'r>(&self, request: &'r Request<'_>, _: &mut Response<'r>) { if let Some(start) = request.local_cache(|| None::<Instant>) { let duration = start.elapsed(); info!("{} {} - {}ms", request.method(), request.uri(), duration.as_millis()); } }}Custom Domains
To use a custom domain with your Rocket app on Klutch.sh:
-
Access the app settings in your Klutch.sh dashboard
-
Navigate to the “Domains” section
-
Add your custom domain (e.g.,
example.com) -
Update your DNS records at your domain registrar:
Create a CNAME record pointing to
example-app.klutch.shName: wwwType: CNAMEValue: example-app.klutch.sh -
Wait for DNS propagation (usually 5-30 minutes)
-
Verify the domain in Klutch.sh
Your app will be accessible at www.example.com.
Troubleshooting
1. App Won’t Start - Cargo Build Errors
Problem: Deployment fails with “error: could not compile my-rocket-app”
Solution:
- Ensure
Cargo.tomlandCargo.lockare committed to Git - Run
cargo build --releaselocally to verify the build works - Check for platform-specific dependencies that need to be in a
Dockerfile - Verify all feature flags are correct in your dependencies
- Check that Rocket version is compatible (0.5.0 for async/await support)
2. Database Connection Failures
Problem: Application crashes with “connection refused” or “failed to connect to database”
Solution:
- Verify
DATABASE_URLis set correctly with proper credentials and host - Ensure the database is accessible from the application container
- Test the connection string locally:
sqlx database create - Check database firewall rules allow connections from Klutch.sh
- Add retry logic to your database connection code
3. Port Binding Issues
Problem: Application fails with “address already in use” or “permission denied”
Solution:
- Ensure your app binds to
0.0.0.0inRocket.tomlinstead of127.0.0.1 - Verify the PORT environment variable is set to 8000 or your chosen port
- Check that your
Dockerfileor Nixpacks config exposes the correct port - Confirm the internal port in Klutch.sh matches your application (8000 is recommended)
- Review your
Rocket.tomlconfiguration for proper binding
4. Type System Compilation Errors
Problem: Complex type errors during compilation related to async code
Solution:
- Ensure you’re using Rocket 0.5.0+ which has proper async/await support
- Use
#[launch]macro instead of#[launch_fn]for better type inference - Verify all futures are properly awaited
- Check that request guards implement
FromRequestcorrectly
5. Slow Build Times
Problem: Deployment takes longer than expected during the build phase
Solution:
- Create a
Dockerfilewith multi-stage builds for faster iterations - Cache
cargo buildlayer: build dependencies separately before copying source - Use
--releaseflag for optimized but slower builds - Consider using incremental compilation:
CARGO_INCREMENTAL=1 - Check if you have feature-heavy dependencies that could be simplified
Best Practices for Rocket on Klutch.sh
-
Leverage Rocket’s type system - Use request guards and strong typing to catch errors at compile time rather than runtime.
-
Use responders for responses - Implement custom responders for consistent error handling and response formatting across your API.
-
Implement proper error handling - Use Rocket’s error catcher functionality to provide meaningful error responses to clients.
-
Organize code with modules - Structure your application with separate modules for routes, database, models, and middleware.
-
Test async code thoroughly - Use
#[rocket::async_trait]in tests and verify concurrent behavior before deployment. -
Monitor startup performance - The Rocket launch phase can be slow; log startup times and optimize expensive initialization.
-
Use connection pooling - SQLx provides pooling by default; configure pool size based on your concurrency needs.
-
Implement structured logging - Use log levels appropriately (debug for development, info/warn for production).
-
Keep dependencies up to date - Regularly update Rocket and other crates to benefit from performance improvements and security patches.
-
Test with production-like environment - Use the same Rocket.toml settings and environment variables locally to catch configuration issues early.