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:
| Aspect | Rocket 0.5+ |
|---|---|
| Runtime | Async (Tokio) |
| Async syntax | Native async fn |
| Request guards | Built-in via the FromRequest trait |
| Responses | Built-in via the Responder trait |
| Database | rocket_db_pools or sqlx |
| State management | rocket::State<T> |
| Form handling | rocket::form::Form<T> |
| JSON | rocket::serde::json::Json<T> |
| Routing | Macro-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
FromRequesttrait, automatically invoked by Rocket.- Catchers for custom error pages:
#[catch(404)],#[catch(500)]. Return consistent JSON.- Type-safe form handling with the
FromFormderive 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-registryandcargo-target— Rust compile times are heavy.- Rocket.toml for configuration.
default/debug/releaseprofiles. Override viaROCKET_*env vars.- rocket_db_pools for database integration with
sqlx/diesel.#[derive(Database)]+#[database("name")].- AppError with Responder: an enum implementing
rocket::Responder, plusFromtraits for auto-conversion.- Bind to
0.0.0.0viaRocket.tomlor theROCKET_ADDRESSenv 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.