ESC
Type to search...

Deployment

Deploying a Rapina application to production

Rapina compiles to a single static binary with no runtime dependencies, making deployment straightforward on any platform that runs Linux, macOS, or Windows.

Building for Release

Build your application with optimizations enabled:

cargo build --release

The binary is at target/release/<your-app-name>. This single file is all you need to deploy — no runtime, no interpreter, no dependency folder.

If your app uses a database, make sure the correct feature flag is enabled in your Cargo.toml:

[dependencies]
rapina = { version = "0.10.0", features = ["postgres"] }

Available database features: postgres, mysql, sqlite.


Environment Variables and Configuration

Rapina reads all configuration from environment variables. In production, set these directly in your hosting platform rather than relying on a .env file.

Application config

Use the #[derive(Config)] macro with #[env] and #[default] attributes:

#[derive(Config)]
struct AppConfig {
    #[env = "HOST"]
    #[default = "0.0.0.0"]
    host: String,

    #[env = "PORT"]
    #[default = "3000"]
    port: u16,
}

Note: Bind to 0.0.0.0 in production (not 127.0.0.1) so the server is reachable from outside the container or host.

Database config

VariableRequiredDefaultDescription
DATABASE_URLYesConnection string (e.g. postgres://user:pass@host/db)
DATABASE_MAX_CONNECTIONSNo10Connection pool maximum
DATABASE_MIN_CONNECTIONSNo1Connection pool minimum
DATABASE_CONNECT_TIMEOUTNo30Seconds before connection attempt fails
DATABASE_IDLE_TIMEOUTNo600Seconds before idle connections are closed
DATABASE_LOGGINGNofalse in releaseLog SQL queries

Auth config

VariableRequiredDefaultDescription
JWT_SECRETYesSecret key for signing JWTs
JWT_EXPIRATIONNo3600Token lifetime in seconds

Logging

Set the RUST_LOG environment variable to control log verbosity. This takes precedence over the programmatic .level() setting on TracingConfig.

RUST_LOG=info          # recommended for production
RUST_LOG=warn          # quieter — only warnings and errors
RUST_LOG=myapp=debug   # debug logs for your crate, info for everything else

Health Check Endpoint

Rapina ships a built-in health check at GET /__rapina/health. Enable it with:

Rapina::new()
    .with_health_check(true)

The endpoint returns 200 OK and is automatically public (no authentication required). Point your load balancer or orchestrator at /__rapina/health.


Graceful Shutdown

Rapina handles SIGINT (Ctrl-C) and SIGTERM automatically. When a signal is received:

  1. The server stops accepting new connections
  2. In-flight requests are given time to complete (default: 30 seconds)
  3. Shutdown hooks run sequentially
  4. The process exits

Configure the drain timeout

use std::time::Duration;

Rapina::new()
    .shutdown_timeout(Duration::from_secs(60))

Run cleanup on shutdown

Rapina::new()
    .on_shutdown(|| async {
        tracing::info!("Flushing metrics...");
        // cleanup logic here
    })

Hooks execute after connections drain (or after the timeout expires), in the order they were registered.


Docker Setup

Since there are no runtime dependencies, a multi-stage build produces a minimal image.

Dockerfile

# Build stage
FROM rust:1.85-bookworm AS builder
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
COPY src ./src

# If you have migrations:
# COPY migrations ./migrations

RUN cargo build --release

# Runtime stage
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*

COPY --from=builder /app/target/release/myapp /usr/local/bin/myapp

EXPOSE 3000
CMD ["myapp"]

.dockerignore

target/
.git/
.env
*.db

Build and run

docker build -t myapp .
docker run -p 3000:3000 \
  -e DATABASE_URL=postgres://user:pass@host/db \
  -e JWT_SECRET=your-production-secret \
  -e RUST_LOG=info \
  myapp

Tip: For even smaller images, use FROM scratch or FROM gcr.io/distroless/cc-debian12 as the runtime stage if your app doesn't need a shell or package manager. You may need to statically link with RUSTFLAGS='-C target-feature=+crt-static' and target x86_64-unknown-linux-gnu.


Running Behind a Reverse Proxy

In production, place your Rapina app behind a reverse proxy for TLS termination, static file serving, and load balancing.

Nginx

upstream rapina {
    server 127.0.0.1:3000;
}

server {
    listen 443 ssl;
    server_name api.example.com;

    ssl_certificate     /etc/ssl/certs/api.example.com.pem;
    ssl_certificate_key /etc/ssl/private/api.example.com.key;

    location / {
        proxy_pass http://rapina;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Caddy

api.example.com {
    reverse_proxy 127.0.0.1:3000
}

Caddy handles TLS certificates automatically via Let's Encrypt.

Important: IP extraction behind a proxy

Rapina's rate limiter reads client IPs from X-Forwarded-For (leftmost entry), then X-Real-IP, falling back to "unknown". Make sure your reverse proxy sets these headers so rate limiting and logging reflect the real client IP.


Deployment Targets

Railway

Railway detects Rust projects automatically via Cargo.toml.

  1. Push your code to a GitHub repository
  2. Create a new project on Railway and connect the repo
  3. Set environment variables in the Railway dashboard
  4. Bind to 0.0.0.0 and use the PORT environment variable Railway provides:
#[derive(Config)]
struct AppConfig {
    #[env = "HOST"]
    #[default = "0.0.0.0"]
    host: String,

    #[env = "PORT"]
    #[default = "3000"]
    port: u16,
}

Fly.io

Create a fly.toml:

app = "myapp"
primary_region = "iad"

[build]

[http_service]
  internal_port = 3000
  force_https = true

[checks]
  [checks.health]
    port = 3000
    type = "http"
    interval = "10s"
    timeout = "2s"
    path = "/__rapina/health"

Deploy with the Fly CLI:

fly launch
fly secrets set DATABASE_URL=postgres://...
fly secrets set JWT_SECRET=your-production-secret
fly deploy

AWS ECS

  1. Build and push your Docker image to ECR
  2. Create a task definition referencing the image
  3. Pass environment variables via the task definition or AWS Secrets Manager
  4. Configure an ALB target group with a health check on /__rapina/health
  5. Set the ECS service desired count for availability

Bare Metal / VPS

Copy the release binary to your server, set environment variables, and run it behind a reverse proxy:

scp target/release/myapp user@server:/opt/myapp/

Create a systemd service:

[Unit]
Description=My Rapina App
After=network.target

[Service]
Type=simple
ExecStart=/opt/myapp/myapp
Environment=DATABASE_URL=postgres://localhost/myapp
Environment=JWT_SECRET=your-production-secret
Environment=RUST_LOG=info
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
sudo systemctl enable myapp
sudo systemctl start myapp

Rapina handles SIGTERM from systemd gracefully, draining connections before exiting.


Production Checklist

Logging and tracing

Rapina::new()
    .with_tracing(TracingConfig::new().json().level(tracing::Level::INFO))
    .middleware(RequestLogMiddleware::new())
  • Use .json() for structured logs that integrate with log aggregators (Datadog, Loki, CloudWatch)
  • Add RequestLogMiddleware to log method, path, status, and duration for every request
  • Set RUST_LOG=info in production

Metrics

Enable Prometheus metrics for monitoring:

[dependencies]
rapina = { version = "0.10.0", features = ["metrics"] }
Rapina::new()
    .with_metrics(true)

This exposes GET /metrics with http_requests_total, http_request_duration_seconds, and http_requests_in_flight. Point Prometheus or your monitoring stack at this endpoint.

Rate limiting

Rapina::new()
    .with_rate_limit(RateLimitConfig::per_minute(60))

CORS

Lock down origins in production:

Rapina::new()
    .with_cors(CorsConfig::with_origins(vec![
        "https://app.example.com".to_string(),
    ]))

Request safeguards

use rapina::middleware::{TimeoutMiddleware, BodyLimitMiddleware, TraceIdMiddleware};
use std::time::Duration;

Rapina::new()
    .middleware(TraceIdMiddleware::new())
    .middleware(TimeoutMiddleware::new(Duration::from_secs(30)))
    .middleware(BodyLimitMiddleware::new(2 * 1024 * 1024)) // 2 MB

Full production example

use rapina::prelude::*;
use rapina::middleware::{
    CorsConfig, CompressionConfig, TraceIdMiddleware,
    TimeoutMiddleware, BodyLimitMiddleware, RequestLogMiddleware,
};
use std::time::Duration;

#[derive(Config)]
struct AppConfig {
    #[env = "HOST"]
    #[default = "0.0.0.0"]
    host: String,

    #[env = "PORT"]
    #[default = "3000"]
    port: u16,
}

#[tokio::main]
async fn main() -> std::io::Result<()> {
    load_dotenv();

    let config = AppConfig::from_env().expect("Missing config");
    let addr = format!("{}:{}", config.host, config.port);

    Rapina::new()
        .with_health_check(true)
        .with_tracing(TracingConfig::new().json().level(tracing::Level::INFO))
        .middleware(TraceIdMiddleware::new())
        .middleware(RequestLogMiddleware::new())
        .middleware(TimeoutMiddleware::new(Duration::from_secs(30)))
        .middleware(BodyLimitMiddleware::new(2 * 1024 * 1024))
        .with_cors(CorsConfig::with_origins(vec![
            "https://app.example.com".to_string(),
        ]))
        .with_rate_limit(RateLimitConfig::per_minute(60))
        .with_compression(CompressionConfig::default())
        .with_metrics(true)
        .shutdown_timeout(Duration::from_secs(60))
        .on_shutdown(|| async {
            tracing::info!("Application shutting down");
        })
        .state(config)
        .discover()
        .listen(&addr)
        .await
}