Rocket #

Rocket is a web framework for Rust famous for its API ergonomics, strong type safety, and macro-based routing. Unlike Axum or Actix, which are flexible but verbose, Rocket hides much complexity behind macros — #[get], #[post], #[launch] — making application code feel declarative and easy to read. Rocket also has a very expressive request guard system for validation and authorization.

This article covers a Docker Compose setup for Rocket local development, from multi-stage Dockerfiles, cargo-watch hot reload, async database integration, to best practices.

Prerequisites #

Make sure you have installed:

  • Docker and Docker Compose (latest versions)
  • The Rust toolchain (optional, for host-side development) — install via rustup
  • Git

A standard Rocket project structure:

my-rocket-app/
├── src/
│   ├── main.rs
│   ├── lib.rs
│   ├── routes/
│   │   ├── mod.rs
│   │   ├── health.rs
│   │   └── users.rs
│   ├── models/
│   │   ├── mod.rs
│   │   └── user.rs
│   ├── repositories/
│   │   ├── mod.rs
│   │   └── user_repository.rs
│   ├── errors.rs
│   └── fairings.rs
├── migrations/
│   └── 001_create_users.sql
├── Rocket.toml
├── Cargo.toml
├── Cargo.lock
├── Dockerfile
├── Dockerfile.dev
├── docker-compose.yml
├── .env
├── .dockerignore
└── .cargo/
    └── config.toml

Rocket.toml is Rocket’s dedicated configuration file — analogous to application.yml in Rails or settings.py in Django. Its format is [profile]-based, with debug, release, and custom profiles.

Important Notes About Rocket #

Rocket has several unique characteristics:

AspectRocket 0.5+
RuntimeAsync (Tokio)
Async syntaxNative async fn
Request guardsBuilt-in via the FromRequest trait
ResponsesBuilt-in via the Responder trait
Databaserocket_db_pools or sqlx
State managementrocket::State<T>
Form handlingrocket::form::Form<T>
JSONrocket::serde::json::Json<T>
RoutingMacro-based (#[get], #[post])
Routing patterns/users/<id> (path params), /users/<id..> (segments)

Rocket 0.5+ is fully async with Tokio. The previous 0.4 version was sync-only and needed rocket_contrib for async. For new projects, always use 0.5+.

Multi-Stage Dockerfile #

# syntax=docker/dockerfile:1.6
FROM rust:1.75-slim AS builder

WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends \
    pkg-config \
    libssl-dev \
    ca-certificates \
    && rm -rf /var/lib/apt/lists/*

# Dummy build to cache dependencies
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs \
    && cargo build --release \
    && rm -rf src target/release/deps/rocket_app*

# Real build
COPY . .
RUN cargo build --release

# Second stage: a slim runtime
FROM debian:12-slim

RUN apt-get update && apt-get install -y --no-install-recommends \
    ca-certificates \
    libssl3 \
    curl \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

RUN addgroup -S app && adduser -S app -G app
USER app:app

COPY --from=builder /app/target/release/rocket-app /app/rocket-app
COPY --from=builder /app/Rocket.toml /app/Rocket.toml

EXPOSE 8000

ENTRYPOINT ["/app/rocket-app"]

Note: Rocket.toml is copied into the runtime image so configuration can load at startup. Configuration can be overridden via environment variables with the ROCKET_ prefix.

A Development Dockerfile #

# Dockerfile.dev
FROM rust:1.75-slim

WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends \
    pkg-config \
    libssl-dev \
    ca-certificates \
    git \
    curl \
    && rm -rf /var/lib/apt/lists/*

# Install cargo-watch
RUN cargo install cargo-watch

# Cache dependencies
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs \
    && cargo fetch

EXPOSE 8000

CMD ["cargo", "watch", "-x", "run"]

Rocket.toml Configuration #

Rocket.toml:

[default]
address = "0.0.0.0"
port = 8000
log_level = "normal"
workers = 2
ident = "my-rocket-app"
cli_colors = false

[default.databases]
postgres = { url = "postgres://app:pass@db:5432/rocketapp" }

[default.redis]
url = "redis://cache:6379/0"

[debug]
log_level = "normal"

[release]
log_level = "critical"

The default profile is the fallback. The debug profile is active during cargo run, release during cargo run --release. The ROCKET_PROFILE environment variable can override.

Configuration is overridden via environment variables with the ROCKET_ prefix:

ROCKET_ADDRESS=0.0.0.0
ROCKET_PORT=8000
ROCKET_LOG_LEVEL=normal
ROCKET_DATABASES__POSTGRES__URL=postgres://...

docker-compose.yml #

services:
  api:
    build:
      context: .
      dockerfile: Dockerfile.dev
    image: rocket-app:dev
    container_name: rocket-api
    command: cargo watch -x run
    volumes:
      - ./src:/app/src
      - ./Cargo.toml:/app/Cargo.toml
      - ./Cargo.lock:/app/Cargo.lock
      - ./Rocket.toml:/app/Rocket.toml
      - cargo-registry:/usr/local/cargo/registry
      - cargo-target:/app/target
    ports:
      - "8000:8000"
    environment:
      - ROCKET_ADDRESS=0.0.0.0
      - ROCKET_PORT=8000
      - ROCKET_LOG_LEVEL=normal
      - ROCKET_PROFILE=debug
      - ROCKET_DATABASES__POSTGRES__URL=postgres://app:pass@db:5432/rocketapp
      - ROCKET_REDIS__URL=redis://cache:6379/0
      - JWT_SECRET=local-dev-secret-change-me
      - RUST_LOG=debug
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    container_name: rocket-db
    environment:
      - POSTGRES_USER=app
      - POSTGRES_PASSWORD=dev
      - POSTGRES_DB=rocketapp
    volumes:
      - db-data:/var/lib/postgresql/data
      - ./db/init:/docker-entrypoint-initdb.d:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d rocketapp"]
      interval: 10s
      timeout: 5s
      retries: 5
    ports:
      - "5432:5432"

  cache:
    image: redis:7-alpine
    container_name: rocket-cache
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3
    volumes:
      - cache-data:/data
    ports:
      - "6379:6379"

volumes:
  db-data:
  cache-data:
  cargo-registry:
  cargo-target:

An Example Rocket Application #

Cargo.toml:

[package]
name = "rocket-app"
version = "0.1.0"
edition = "2021"

[dependencies]
rocket = { version = "0.5", features = ["json", "uuid", "secrets"] }
rocket_db_pools = { version = "0.2", features = ["sqlx_postgres"] }
sqlx = { version = "0.7", features = ["runtime-tokio", "tls-rustls", "postgres", "uuid", "chrono", "macros"] }

tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] }

serde = { version = "1", features = ["derive"] }
serde_json = "1"

uuid = { version = "1", features = ["serde", "v4"] }
chrono = { version = "0.4", features = ["serde"] }

tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

thiserror = "1"
anyhow = "1"

bcrypt = "0.15"
jsonwebtoken = "9"

src/main.rs:

#[macro_use]
extern crate rocket;

mod errors;
mod models;
mod repositories;
mod routes;

use rocket::fairing::AdHoc;
use rocket_db_pools::Database;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

use crate::errors::AppError;

#[derive(Database)]
#[database("postgres")]
pub struct PgPool(sqlx::PgPool);

#[launch]
async fn rocket() -> _ {
    // Logging
    tracing_subscriber::registry()
        .with(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| "rocket=info,rocket_app=debug,info".into()),
        )
        .with(tracing_subscriber::fmt::layer())
        .init();

    rocket::build()
        .attach(PgPool::init())
        .attach(AdHoc::try_on_ignite("DB Migrations", |rocket| async move {
            let pool = PgPool::get_one(&rocket)
                .await
                .expect("database pool");
            match sqlx::migrate!("./migrations").run(pool.inner()).await {
                Ok(_) => tracing::info!("migrations applied"),
                Err(e) => tracing::error!("migration failed: {}", e),
            }
            Ok(rocket)
        }))
        .mount("/", routes![routes::health::healthz, routes::health::readyz])
        .mount("/api/v1/users", routes::routes::routes())
        .register("/", catchers![not_found, internal_error])
}

#[catch(404)]
fn not_found() -> rocket::serde::json::Value {
    rocket::serde::json::json!({ "error": "not found" })
}

#[catch(500)]
fn internal_error() -> rocket::serde::json::Value {
    rocket::serde::json::json!({ "error": "internal server error" })
}

src/errors.rs:

use rocket::http::Status;
use rocket::response::{self, Responder, Response};
use rocket::serde::json::Json;
use rocket::Request;
use serde_json::{json, Value};
use thiserror::Error;

#[derive(Debug, Error)]
pub enum AppError {
    #[error("not found")]
    NotFound,

    #[error("validation error: {0}")]
    Validation(String),

    #[error("unauthorized")]
    Unauthorized,

    #[error("conflict: {0}")]
    Conflict(String),

    #[error("database error: {0}")]
    Database(#[from] sqlx::Error),

    #[error("bcrypt error: {0}")]
    Bcrypt(#[from] bcrypt::BcryptError),

    #[error("jwt error: {0}")]
    Jwt(#[from] jsonwebtoken::errors::Error),

    #[error("internal error: {0}")]
    Internal(#[from] anyhow::Error),
}

impl<'r> Responder<'r, 'static> for AppError {
    fn respond_to(self, _req: &'r Request<'_>) -> response::Result<'static> {
        let (status, message) = match &self {
            AppError::NotFound => (Status::NotFound, "not found".to_string()),
            AppError::Validation(msg) => (Status::UnprocessableEntity, msg.clone()),
            AppError::Unauthorized => (Status::Unauthorized, "unauthorized".to_string()),
            AppError::Conflict(msg) => (Status::Conflict, msg.clone()),
            _ => {
                tracing::error!(error = ?self, "internal error");
                (Status::InternalServerError, "internal server error".to_string())
            }
        };

        let body = json!({ "error": message }).to_string();
        Response::build()
            .status(status)
            .header(rocket::http::ContentType::JSON)
            .sized_body(body.len(), std::io::Cursor::new(body))
            .ok()
    }
}

pub type AppResult<T> = Result<T, AppError>;

src/routes/mod.rs:

pub mod health;
pub mod users;

pub fn routes() -> Vec<rocket::Route> {
    routes![users::create_user, users::get_user, users::list_users, users::update_user, users::delete_user]
}

src/routes/health.rs:

use rocket::serde::json::Json;
use rocket::State;
use serde_json::{json, Value};

use crate::PgPool;

#[get("/healthz")]
pub fn healthz() -> Json<Value> {
    Json(json!({"status": "ok"}))
}

#[get("/readyz")]
pub async fn readyz(pool: &State<PgPool>) -> (rocket::http::Status, Json<Value>) {
    let pool = pool.inner();
    let db_ok = sqlx::query("SELECT 1").execute(pool).await.is_ok();
    let status = if db_ok {
        rocket::http::Status::OK
    } else {
        rocket::http::Status::ServiceUnavailable
    };
    (status, Json(json!({
        "status": if db_ok { "ready" } else { "degraded" },
        "database": if db_ok { "up" } else { "down" },
    })))
}

src/routes/users.rs:

use rocket::serde::json::Json;
use rocket::{delete, get, post, put, State};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

use crate::errors::{AppError, AppResult};
use crate::PgPool;

#[derive(Debug, Deserialize)]
pub struct CreateUserRequest {
    pub email: String,
    pub name: String,
    pub password: String,
}

#[derive(Debug, Serialize)]
pub struct UserResponse {
    pub id: Uuid,
    pub email: String,
    pub name: String,
    pub is_verified: bool,
    pub created_at: chrono::DateTime<chrono::Utc>,
}

#[derive(Debug, Deserialize)]
pub struct UpdateUserRequest {
    pub name: Option<String>,
    pub is_verified: Option<bool>,
}

#[derive(Debug, Deserialize)]
pub struct ListQuery {
    pub limit: Option<i64>,
    pub offset: Option<i64>,
}

#[post("/", format = "json", data = "<body>")]
pub async fn create_user(
    pool: &State<PgPool>,
    body: Json<CreateUserRequest>,
) -> AppResult<(rocket::http::Status, Json<UserResponse>)> {
    if body.password.len() < 8 {
        return Err(AppError::Validation("password too short".into()));
    }

    let hash = bcrypt::hash(&body.password, bcrypt::DEFAULT_COST)?;
    let row = sqlx::query!(
        r#"
        INSERT INTO users (email, name, password_hash)
        VALUES ($1, $2, $3)
        RETURNING id, email, name, is_verified, created_at
        "#,
        body.email.to_lowercase(),
        body.name,
        hash,
    )
    .fetch_one(pool.inner())
    .await
    .map_err(|e| match e {
        sqlx::Error::Database(db_err) if db_err.constraint() == Some("users_email_key") => {
            AppError::Conflict("email already registered".into())
        }
        other => AppError::Database(other),
    })?;

    Ok((rocket::http::Status::Created, Json(UserResponse {
        id: row.id,
        email: row.email,
        name: row.name,
        is_verified: row.is_verified,
        created_at: row.created_at,
    })))
}

#[get("/<id>", rank = 1)]
pub async fn get_user(pool: &State<PgPool>, id: &str) -> AppResult<Json<UserResponse>> {
    let uuid = Uuid::parse_str(id).map_err(|_| AppError::Validation("invalid id".into()))?;
    let row = sqlx::query!(
        r#"SELECT id, email, name, is_verified, created_at FROM users WHERE id = $1"#,
        uuid,
    )
    .fetch_optional(pool.inner())
    .await?;

    match row {
        Some(r) => Ok(Json(UserResponse {
            id: r.id,
            email: r.email,
            name: r.name,
            is_verified: r.is_verified,
            created_at: r.created_at,
        })),
        None => Err(AppError::NotFound),
    }
}

#[get("/?<limit>&<offset>")]
pub async fn list_users(
    pool: &State<PgPool>,
    limit: Option<i64>,
    offset: Option<i64>,
) -> AppResult<Json<serde_json::Value>> {
    let limit = limit.unwrap_or(20).min(100);
    let offset = offset.unwrap_or(0);
    let rows = sqlx::query!(
        r#"SELECT id, email, name, is_verified, created_at
           FROM users
           ORDER BY created_at DESC
           LIMIT $1 OFFSET $2"#,
        limit,
        offset,
    )
    .fetch_all(pool.inner())
    .await?;

    let data: Vec<UserResponse> = rows
        .into_iter()
        .map(|r| UserResponse {
            id: r.id,
            email: r.email,
            name: r.name,
            is_verified: r.is_verified,
            created_at: r.created_at,
        })
        .collect();

    Ok(Json(serde_json::json!({
        "data": data,
        "limit": limit,
        "offset": offset,
    })))
}

#[put("/<id>", format = "json", data = "<body>")]
pub async fn update_user(
    pool: &State<PgPool>,
    id: &str,
    body: Json<UpdateUserRequest>,
) -> AppResult<Json<UserResponse>> {
    let uuid = Uuid::parse_str(id).map_err(|_| AppError::Validation("invalid id".into()))?;
    let row = sqlx::query!(
        r#"
        UPDATE users
        SET name = COALESCE($2, name),
            is_verified = COALESCE($3, is_verified),
            updated_at = NOW()
        WHERE id = $1
        RETURNING id, email, name, is_verified, created_at
        "#,
        uuid,
        body.name,
        body.is_verified,
    )
    .fetch_optional(pool.inner())
    .await?;

    match row {
        Some(r) => Ok(Json(UserResponse {
            id: r.id,
            email: r.email,
            name: r.name,
            is_verified: r.is_verified,
            created_at: r.created_at,
        })),
        None => Err(AppError::NotFound),
    }
}

#[delete("/<id>")]
pub async fn delete_user(pool: &State<PgPool>, id: &str) -> AppResult<rocket::http::Status> {
    let uuid = Uuid::parse_str(id).map_err(|_| AppError::Validation("invalid id".into()))?;
    let result = sqlx::query!("DELETE FROM users WHERE id = $1", uuid)
        .execute(pool.inner())
        .await?;

    if result.rows_affected() > 0 {
        Ok(rocket::http::Status::NoContent)
    } else {
        Err(AppError::NotFound)
    }
}

Migrations #

migrations/20240101000000_create_users.sql:

CREATE TABLE IF NOT EXISTS users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email VARCHAR(255) NOT NULL UNIQUE,
    name VARCHAR(255) NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    is_verified BOOLEAN NOT NULL DEFAULT FALSE,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_users_created_at ON users(created_at);

Rocket Fairings #

Fairings are Rocket’s middleware — analogous to Tower middleware in Axum. Use fairings for cross-cutting concerns: logging, CORS, request IDs, rate limiting.

src/fairings.rs:

use rocket::fairing::{Fairing, Info, Kind};
use rocket::http::Header;
use rocket::{Data, Request, Response};

pub struct CorsFairing;

#[rocket::async_trait]
impl Fairing for CorsFairing {
    fn info(&self) -> Info {
        Info {
            name: "CORS",
            kind: Kind::Request | Kind::Response,
        }
    }

    async fn on_request(&self, request: &mut Request<'_>, _: &mut Data<'_>) {
        // CORS preflight
        if request.method() == rocket::http::Method::Options {
            request.set_method(rocket::http::Method::Get);
        }
    }

    async fn on_response<'r>(&self, request: &'r Request<'_>, response: &mut Response<'r>) {
        response.set_header(Header::new("Access-Control-Allow-Origin", "*"));
        response.set_header(Header::new(
            "Access-Control-Allow-Methods",
            "GET, POST, PUT, DELETE, OPTIONS",
        ));
        response.set_header(Header::new(
            "Access-Control-Allow-Headers",
            "Content-Type, Authorization",
        ));

        if request.method() == rocket::http::Method::Options {
            response.set_status(rocket::http::Status::NoContent);
        }
    }
}

pub struct RequestLogger;

#[rocket::async_trait]
impl Fairing for RequestLogger {
    fn info(&self) -> Info {
        Info {
            name: "RequestLogger",
            kind: Kind::Request,
        }
    }

    async fn on_request(&self, request: &mut Request<'_>, _: &mut Data<'_>) {
        tracing::info!(
            method = %request.method(),
            uri = %request.uri(),
            "incoming request"
        );
    }
}

Register them in main.rs:

use crate::fairings::{CorsFairing, RequestLogger};

rocket::build()
    .attach(CorsFairing)
    .attach(RequestLogger)
    // ... route mounting

Request Guards for Authentication #

Rocket has an elegant request guard system. Implement FromRequest for custom guards.

use rocket::http::Status;
use rocket::request::{FromRequest, Outcome, Request};

pub struct AuthUser {
    pub id: Uuid,
    pub email: String,
}

#[rocket::async_trait]
impl<'r> FromRequest<'r> for AuthUser {
    type Error = ();

    async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
        let token = req
            .headers()
            .get_one("Authorization")
            .and_then(|h| h.split_whitespace().last());

        let token = match token {
            Some(t) => t,
            None => return Outcome::Error((Status::Unauthorized, ())),
        };

        let payload = match crate::services::jwt::decode(token) {
            Some(p) => p,
            None => return Outcome::Error((Status::Unauthorized, ())),
        };

        Outcome::Success(AuthUser {
            id: Uuid::parse_str(&payload["user_id"]).unwrap(),
            email: payload["email"].clone(),
        })
    }
}

// Use in a handler
#[get("/me")]
pub async fn me(user: AuthUser) -> Json<serde_json::Value> {
    Json(serde_json::json!({
        "id": user.id,
        "email": user.email,
    }))
}

Request guards are invoked automatically by Rocket when a handler runs. Return Outcome::Error to reject a request.

Form Handling #

Rocket has CSRF-safe, type-safe form handling.

use rocket::form::Form;
use serde::Deserialize;

#[derive(FromForm)]
pub struct LoginForm {
    pub email: String,
    pub password: String,
    #[field(validate = len(1..))]
    pub csrf_token: String,
}

#[post("/login", data = "<form>")]
pub async fn login(form: Form<LoginForm>) -> AppResult<Json<UserResponse>> {
    // form.email, form.password, form.csrf_token
    // Automatic validation
    todo!()
}

Build and Run #

# Build
docker compose build

# Run
docker compose up -d

# View logs
docker compose logs -f api

# Stop
docker compose down

Access:

  • API: http://localhost:8000
  • Health: http://localhost:8000/healthz
  • Ready: http://localhost:8000/readyz
  • PostgreSQL: localhost:5432
  • Redis: localhost:6379

Test:

# Create a user
curl -X POST http://localhost:8000/api/v1/users \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","name":"Ina","password":"password123"}'

# Get a user
curl http://localhost:8000/api/v1/users/<uuid>

# List users
curl "http://localhost:8000/api/v1/users?limit=10&offset=0"

When Rocket Fits #

Use Rocket if:
  ✓ You like declarative macro-based routing
  ✓ You need request guards for authorization
  ✓ Traditional form-handling applications
  ✓ Microservices with high type safety
  ✓ A Rust team that likes ergonomic APIs

Avoid Rocket if:
  ✗ You need tower middleware compatibility (use Axum)
  ✗ You need the Actix library ecosystem (use Actix Web)
  ✗ Super-minimal microservices (Axum is simpler)
  ✗ You need low-level control (warp/actix is more flexible)
  ✗ Compile times are a blocker (Rocket macros can slow builds)

Rocket is the right choice for services prioritizing developer experience. For services needing broad interoperability, Axum fits better. For the highest performance, Actix.

Best Practices #

Cache Cargo with Volumes #

Rust compile times are heavy. The cargo-target and cargo-registry volumes are mandatory.

Dummy Builds for Layer Caching #

The mkdir src && echo "fn main() {}" > src/main.rs && cargo build pattern caches the dependency layer.

Use cargo-watch #

cargo watch -x run watches .rs files and rebuilds + restarts. Essential Rust DX.

Use Rocket.toml for Configuration #

Don’t hardcode configuration in code. Use Rocket.toml with default/debug/release profiles. Override via ROCKET_* environment variables.

rocket_db_pools for Database Pools #

The rocket_db_pools crate integrates with sqlx and diesel. Use #[derive(Database)] and #[database("postgres")] for declarative setup.

AppError with Responder #

Implement the Responder trait for custom error types. The same pattern as ResponseError in Actix.

Request Guards for Auth #

Implement FromRequest for custom guards. More ergonomic than manual middleware.

The routes! Macro for Collecting Routes #

Use routes![handler1, handler2, ...] to collect multiple routes. Mount with .mount("/path", routes!).

Catchers for Error Pages #

Implement catchers for 404, 422, and 500. Return consistent JSON. Register with .register("/", catchers![...]).

Use sqlx::query! for Compile-time Checks #

sqlx::query! validates queries at compile time. Needs DATABASE_URL at build time, or cargo sqlx prepare for an offline cache.

Bind to 0.0.0.0 #

Set ROCKET_ADDRESS=0.0.0.0 or in Rocket.toml. Rocket binds to localhost by default.

Troubleshooting #

“Rocket” Not Found #

Make sure #[macro_use] extern crate rocket; is in main.rs. Or use use rocket::*;.

Database Connection Refused #

Make sure ROCKET_DATABASES__POSTGRES__URL is correct. Rocket’s env var format uses double underscores __ as the nested separator.

Very Slow Cargo Builds #

Make sure the cargo-target volume is mounted. Check that the cargo fetch cache works.

Hot Reload Not Detecting #

Make sure cargo-watch is installed. Raise the delay in the configuration.

Port 8000 Already in Use #

lsof -i :8000

Change ROCKET_PORT or the port mapping in Compose.

SQLx “No such table” at Compile Time #

Set DATABASE_URL during compilation, or generate an offline cache with cargo sqlx prepare.

Summary #

  • Rocket is ideal for Rust services prioritizing developer experience and type safety.
  • Rocket 0.5+ is fully async with Tokio. The old 0.4 version was sync-only — always use 0.5+ for new projects.
  • Macro-based routing: #[get], #[post], #[put], #[delete] — declarative and ergonomic. Easier to read than Axum/Actix.
  • Request guards for authorization: implement the FromRequest trait, automatically invoked by Rocket.
  • Catchers for custom error pages: #[catch(404)], #[catch(500)]. Return consistent JSON.
  • Type-safe form handling with the FromForm derive macro. Built-in validation.
  • Multi-stage Dockerfiles for production: a dummy build caches dependencies, the real build produces the binary. The final image is < 80 MB.
  • Dockerfile.dev for development: full toolchain + cargo-watch.
  • Mandatory volumes for cargo-registry and cargo-target — Rust compile times are heavy.
  • Rocket.toml for configuration. default/debug/release profiles. Override via ROCKET_* env vars.
  • rocket_db_pools for database integration with sqlx/diesel. #[derive(Database)] + #[database("name")].
  • AppError with Responder: an enum implementing rocket::Responder, plus From traits for auto-conversion.
  • Bind to 0.0.0.0 via Rocket.toml or the ROCKET_ADDRESS env var. Defaults to localhost.
  • Best practices: cache Cargo, dummy builds, cargo-watch, Rocket.toml, rocket_db_pools, AppError, request guards, catchers.
  • Use Rocket for services prioritizing DX. Avoid it for services needing tower middleware compatibility (use Axum).
  • Alternatives: Axum for idiomatic Tokio + tower, Actix Web for the highest performance + library ecosystem.

← Previous: Actix Web   Next: Express.js →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact