Skip to content

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

  1. Install Rust (if not already installed):

    Use rustup for version management:

    Terminal window
    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
    source "$HOME/.cargo/env"
    rustup default stable
  2. Create a new Rocket project:

    Terminal window
    cargo new my-rocket-app
    cd my-rocket-app
  3. 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"
  4. Build the project:

    Terminal window
    cargo build
  5. Start the development server:

    Terminal window
    cargo run

    Your 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

  1. Prepare your Rocket app for production:

    Create a Rocket.toml for production configuration:

    [default]
    address = "0.0.0.0"
    port = 8000
    workers = 4
    log_level = "normal"
    [production]
    address = "0.0.0.0"
    port = 8000
    workers = 4
    log_level = "normal"
  2. Optimize your Cargo.toml for release builds:

    [profile.release]
    opt-level = 3
    lto = true
    codegen-units = 1
    strip = true
  3. Commit your code to GitHub:

    Terminal window
    git add .
    git commit -m "Initial Rocket app setup"
    git push origin main
  4. Log in to Klutch.sh dashboard:

    Visit https://klutch.sh/app

  5. Create a new project:

    • Click “Create Project”
    • Enter your project name (e.g., “My Rocket App”)
    • Select your organization or personal account
  6. Create a new app:

    • Click “Create App”
    • Select your Rocket GitHub repository
    • Select the branch (typically main or master)
    • 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
  7. Add environment variables:

    In the app creation form, add these essential environment variables:

    ROCKET_ENV=production
    PORT=8000
    DATABASE_URL=postgresql://user:password@host:5432/myapp
    REDIS_URL=redis://user:password@host:6379/0
    LOG_LEVEL=normal
  8. 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 --release
NIXPACKS_START_CMD=./target/release/my-rocket-app

Deploying 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

  1. Create a Dockerfile in your project root:

    # Build stage
    FROM rust:1.75-slim as builder
    WORKDIR /app
    # Install build dependencies
    RUN apt-get update && apt-get install -y build-essential pkg-config libssl-dev && rm -rf /var/lib/apt/lists/*
    # Copy manifests
    COPY Cargo.toml Cargo.lock ./
    # Build dependencies - this is the caching layer
    RUN mkdir src && echo "fn main() {}" > src/main.rs && cargo build --release && rm -rf src
    # Copy source code
    COPY src ./src
    # Build application
    RUN cargo build --release
    # Runtime stage
    FROM debian:bookworm-slim
    RUN apt-get update && apt-get install -y ca-certificates libssl3 && rm -rf /var/lib/apt/lists/*
    WORKDIR /app
    # Copy binary from builder
    COPY --from=builder /app/target/release/my-rocket-app .
    # Copy configuration
    COPY Rocket.toml .
    # Health check
    HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:8000/health || exit 1
    # Set environment
    ENV PORT=8000 ROCKET_ENV=production
    # Expose port
    EXPOSE 8000
    # Start application
    CMD ["./my-rocket-app"]
  2. Commit the Dockerfile to GitHub:

    Terminal window
    git add Dockerfile Rocket.toml
    git commit -m "Add production Dockerfile"
    git push origin main
  3. Log in to Klutch.sh dashboard:

    Visit https://klutch.sh/app

  4. 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
  5. Add environment variables:

    ROCKET_ENV=production
    PORT=8000
    DATABASE_URL=postgresql://user:password@host:5432/myapp
    REDIS_URL=redis://user:password@host:6379/0
  6. 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

  1. Add SQLx to your Cargo.toml with PostgreSQL support:

    sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "macros", "migrate"] }
  2. 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(())
    }
  3. 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 */])
    }
  4. During app creation on Klutch.sh, set:

    DATABASE_URL=postgresql://username:password@postgres-host:5432/myapp_production

MySQL Configuration

  1. Add SQLx with MySQL support:

    sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "mysql"] }
  2. 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
    }
  3. 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=production
PORT=8000
DATABASE_URL=postgresql://user:password@host:5432/dbname
REDIS_URL=redis://:password@redis-host:6379/0
API_KEY=<your-api-key>
JWT_SECRET=<your-jwt-secret>
LOG_LEVEL=normal

Loading 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:

  1. Add Redis crate to Cargo.toml:

    redis = { version = "0.25", features = ["aio"] }
  2. 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
    }
  3. 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 first
    if let Ok(Some(cached)) = cache_get(redis, &cache_key).await {
    return Ok(Json(serde_json::from_str(&cached).unwrap()));
    }
    // Fetch from database and cache
    if 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)
    }
  4. 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

  1. 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
    }
  2. 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)
    }
  3. 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)
  4. 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:

Terminal window
# .env (local development only)
DATABASE_URL=postgres://localhost/myapp
JWT_SECRET=development_secret_key

7. 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:

  1. Access the app settings in your Klutch.sh dashboard

  2. Navigate to the “Domains” section

  3. Add your custom domain (e.g., example.com)

  4. Update your DNS records at your domain registrar:

    Create a CNAME record pointing to example-app.klutch.sh

    Name: www
    Type: CNAME
    Value: example-app.klutch.sh
  5. Wait for DNS propagation (usually 5-30 minutes)

  6. 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.toml and Cargo.lock are committed to Git
  • Run cargo build --release locally 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_URL is 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.0 in Rocket.toml instead of 127.0.0.1
  • Verify the PORT environment variable is set to 8000 or your chosen port
  • Check that your Dockerfile or Nixpacks config exposes the correct port
  • Confirm the internal port in Klutch.sh matches your application (8000 is recommended)
  • Review your Rocket.toml configuration 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 FromRequest correctly

5. Slow Build Times

Problem: Deployment takes longer than expected during the build phase

Solution:

  • Create a Dockerfile with multi-stage builds for faster iterations
  • Cache cargo build layer: build dependencies separately before copying source
  • Use --release flag 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

  1. Leverage Rocket’s type system - Use request guards and strong typing to catch errors at compile time rather than runtime.

  2. Use responders for responses - Implement custom responders for consistent error handling and response formatting across your API.

  3. Implement proper error handling - Use Rocket’s error catcher functionality to provide meaningful error responses to clients.

  4. Organize code with modules - Structure your application with separate modules for routes, database, models, and middleware.

  5. Test async code thoroughly - Use #[rocket::async_trait] in tests and verify concurrent behavior before deployment.

  6. Monitor startup performance - The Rocket launch phase can be slow; log startup times and optimize expensive initialization.

  7. Use connection pooling - SQLx provides pooling by default; configure pool size based on your concurrency needs.

  8. Implement structured logging - Use log levels appropriately (debug for development, info/warn for production).

  9. Keep dependencies up to date - Regularly update Rocket and other crates to benefit from performance improvements and security patches.

  10. Test with production-like environment - Use the same Rocket.toml settings and environment variables locally to catch configuration issues early.


External Resources