Back to Blog
Tutorial

How to Deploy a Rust Axum Application in 1 Minute

Daniel Brooks6 min read
How to Deploy a Rust Axum Application in 1 Minute

Rust is the most admired programming language for the eighth year running. It delivers memory safety without a garbage collector, zero-cost abstractions, and performance on par with C and C++. Axum is Rust's most popular async web framework, built on top of the Tokio runtime and the Tower middleware ecosystem.

Deploying a Rust application to production typically involves compiling release binaries, configuring multi-stage Docker builds, and setting up infrastructure. With Out Plane, you can deploy your Rust Axum application in under a minute. This guide shows you exactly how.

What You'll Need

Before starting, make sure you have:

  • Rust (latest stable) installed via rustup
  • Cargo (included with Rust)
  • A GitHub account
  • An Axum application in a GitHub repository

Don't have Rust installed? Install it from rustup.rs:

bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
  • Windows: Download and run rustup-init.exe from rustup.rs. Verify installation with rustc --version in PowerShell.
  • macOS: Run the curl command above or use brew install rustup-init && rustup-init
  • Linux: Run the curl command above. Add $HOME/.cargo/bin to your PATH.

Once Rust is installed, create a new project:

bash
cargo new my-axum-app && cd my-axum-app

If you don't have an Axum application yet, use our example below.

Quick Start: Sample Axum Application

Here's a minimal Axum application you can use. Create these files in your repository:

Update Cargo.toml:

toml
[package]
name = "my-axum-app"
version = "0.1.0"
edition = "2021"

[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tower-http = { version = "0.6", features = ["cors", "trace"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

Replace src/main.rs:

rust
use axum::{routing::get, Json, Router};
use serde::Serialize;
use std::net::SocketAddr;
use tracing_subscriber::EnvFilter;

#[derive(Serialize)]
struct MessageResponse {
    message: String,
    status: String,
}

#[derive(Serialize)]
struct HealthResponse {
    status: String,
}

#[derive(Serialize)]
struct Item {
    id: u32,
    name: String,
}

async fn root() -> Json<MessageResponse> {
    Json(MessageResponse {
        message: "Hello from Axum!".to_string(),
        status: "running".to_string(),
    })
}

async fn health() -> Json<HealthResponse> {
    Json(HealthResponse {
        status: "healthy".to_string(),
    })
}

async fn list_items() -> Json<Vec<Item>> {
    let items = vec![
        Item { id: 1, name: "Item One".to_string() },
        Item { id: 2, name: "Item Two".to_string() },
        Item { id: 3, name: "Item Three".to_string() },
    ];
    Json(items)
}

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt()
        .with_env_filter(EnvFilter::from_default_env()
            .add_directive("my_axum_app=info".parse().unwrap()))
        .init();

    let app = Router::new()
        .route("/", get(root))
        .route("/health", get(health))
        .route("/api/items", get(list_items));

    let addr = SocketAddr::from(([0, 0, 0, 0], 8080));
    tracing::info!("Server listening on {}", addr);

    let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
    axum::serve(listener, app)
        .with_graceful_shutdown(shutdown_signal())
        .await
        .unwrap();
}

async fn shutdown_signal() {
    tokio::signal::ctrl_c()
        .await
        .expect("Failed to install CTRL+C signal handler");
    tracing::info!("Shutdown signal received, finishing in-flight requests...");
}

Test your application locally:

bash
cargo run

Visit http://localhost:8080 in your browser. You should see the JSON response.

Push this code to a GitHub repository, and you're ready to deploy.

Add a Dockerfile

Rust applications require a Dockerfile for deployment on Out Plane. Multi-stage builds keep the final image small and the binary optimized. This Dockerfile uses cargo-chef for Docker layer caching, which dramatically speeds up rebuilds when only your source code changes:

dockerfile
# Stage 1: Generate a recipe for dependencies
FROM rust:1.84-slim AS chef
RUN cargo install cargo-chef
WORKDIR /app

FROM chef AS planner
COPY . .
RUN cargo chef prepare --recipe-path recipe.json

# Stage 2: Build dependencies (cached layer)
FROM chef AS builder
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json

# Build the application
COPY . .
RUN cargo build --release

# Stage 3: Minimal runtime image
FROM debian:bookworm-slim AS runtime

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

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

EXPOSE 8080

CMD ["app"]

With cargo-chef, dependency changes are cached in a separate Docker layer. When you change only your Rust source code, Docker skips the dependency compilation step entirely, reducing build times from minutes to seconds.

Select Dockerfile as your build method in the next step.

Deploy in 3 Steps

Step 1: Connect Your Repository

  1. Go to console.outplane.com
  2. Sign in with your GitHub account
  3. Select your Axum repository from the list

Step 2: Configure Your Application

Configure the following settings in the create application form:

Build Method

Select Dockerfile as the build method. Rust applications require a Dockerfile for the compilation step.

Basic Settings

  • Port: Set to 8080
  • Branch: Select main or your preferred branch
  • Region: Choose the region closest to your users (default: Nuremberg)

Environment Variables (Optional)

If your application uses environment-specific configuration, add them here. For our simple example, no environment variables are required.

For more complex applications, you might add variables like database URLs or API keys using the Add button or Raw Edit for bulk entry:

text
DATABASE_URL=postgres://user:password@host:5432/database
RUST_LOG=info

Step 3: Deploy

Click Deploy Application and watch the build process:

  1. Queued → Waiting for resources
  2. Building → Compiling your Rust application in release mode
  3. Deploying → Starting your Axum server
  4. Ready → Your app is live

Once the status shows Ready, your application is live. You can find your application URL at the top of the deployment page. Click the URL to open your Axum app in a new tab. SSL is automatically configured.

Production Best Practices

Release Builds

Always compile with --release in your Dockerfile. Debug builds are 10-50x slower than release builds in Rust. The Dockerfile above already includes the --release flag.

Graceful Shutdown

Production Axum applications must handle shutdown signals. The with_graceful_shutdown method on axum::serve lets in-flight requests finish before the process exits:

rust
async fn shutdown_signal() {
    use tokio::signal;

    let ctrl_c = async {
        signal::ctrl_c()
            .await
            .expect("Failed to install CTRL+C handler");
    };

    let terminate = async {
        signal::unix::signal(signal::unix::SignalKind::terminate())
            .expect("Failed to install SIGTERM handler")
            .recv()
            .await;
    };

    tokio::select! {
        _ = ctrl_c => {},
        _ = terminate => {},
    }

    tracing::info!("Signal received, starting graceful shutdown");
}

This handles both SIGINT and SIGTERM, which Out Plane sends during redeployments.

Structured Logging with tracing

Use the tracing crate instead of println! for structured, leveled logging:

rust
use tracing::{info, warn, error};
use tracing_subscriber::EnvFilter;

fn init_logging() {
    tracing_subscriber::fmt()
        .with_env_filter(EnvFilter::from_default_env())
        .json()
        .init();
}

Set the RUST_LOG environment variable in Out Plane to control log levels:

text
RUST_LOG=info,my_axum_app=debug,tower_http=debug

Error Handling with thiserror and anyhow

Use thiserror for library errors and anyhow for application errors. Return structured error responses from your handlers:

rust
use axum::{http::StatusCode, response::IntoResponse, Json};
use serde_json::json;

enum AppError {
    NotFound(String),
    Internal(anyhow::Error),
}

impl IntoResponse for AppError {
    fn into_response(self) -> axum::response::Response {
        let (status, message) = match self {
            AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
            AppError::Internal(err) => {
                tracing::error!("Internal error: {:?}", err);
                (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error".to_string())
            }
        };

        (status, Json(json!({ "error": message }))).into_response()
    }
}

Tower Middleware

Axum is built on Tower, giving you access to a rich middleware ecosystem. Add request tracing and CORS in your router:

rust
use tower_http::cors::{Any, CorsLayer};
use tower_http::trace::TraceLayer;

let app = Router::new()
    .route("/", get(root))
    .route("/health", get(health))
    .layer(TraceLayer::new_for_http())
    .layer(
        CorsLayer::new()
            .allow_origin(Any)
            .allow_methods(Any)
            .allow_headers(Any),
    );

Tower middleware runs as composable layers, so you can add timeout, rate limiting, and compression without changing your handler logic.

Connecting a Database

Most Axum applications need a database. Out Plane provides managed PostgreSQL:

  1. Go to Databases in the sidebar
  2. Click Create Database
  3. Select PostgreSQL version and region
  4. Copy the connection URL

Add the connection URL as an environment variable:

text
DATABASE_URL=postgres://user:password@host:5432/database

Then use it in your Axum application with sqlx for async, compile-time checked queries:

rust
use sqlx::postgres::PgPoolOptions;

async fn init_database() -> sqlx::PgPool {
    let database_url = std::env::var("DATABASE_URL")
        .expect("DATABASE_URL must be set");

    PgPoolOptions::new()
        .max_connections(5)
        .connect(&database_url)
        .await
        .expect("Failed to create database pool")
}

Share the pool across handlers using Axum's state extraction:

rust
use axum::extract::State;

#[derive(Clone)]
struct AppState {
    db: sqlx::PgPool,
}

async fn list_users(
    State(state): State<AppState>,
) -> Json<Vec<User>> {
    let users = sqlx::query_as!(User, "SELECT id, name FROM users LIMIT 50")
        .fetch_all(&state.db)
        .await
        .unwrap();

    Json(users)
}

// In main()
let pool = init_database().await;
let state = AppState { db: pool };

let app = Router::new()
    .route("/api/users", get(list_users))
    .with_state(state);

Add sqlx to your Cargo.toml:

toml
[dependencies]
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres"] }

Custom Domain Setup

Replace the default .outplane.app URL with your own domain:

  1. Navigate to Domains
  2. Click Map Domain
  3. Enter your domain (e.g., api.yourdomain.com)
  4. Add the DNS records shown to your domain registrar

SSL certificates are automatically provisioned once DNS propagates.

Monitoring Your Axum Application

After deployment, monitor your Rust Axum application:

  • Logs: View real-time application logs including tracing output and request logs
  • Metrics: Track CPU, memory, and network usage
  • HTTP Logs: Analyze incoming requests, response times, and status codes

Access these from the sidebar in your application dashboard.

Rust applications typically show exceptionally low memory usage compared to garbage-collected languages. Monitor your baseline after deployment to right-size your instance.

Troubleshooting

Long Build Times

Use cargo-chef for Docker layer caching. Without it, every source code change triggers a full dependency recompilation. The Dockerfile in this guide already includes cargo-chef. If your first build takes several minutes, subsequent builds with only source code changes will be significantly faster.

Binary Not Found

Verify the binary name in your Dockerfile matches your package name. The binary name in COPY --from=builder must match the name field in your Cargo.toml. If your package is named my-axum-app, the binary is at target/release/my-axum-app (hyphens, not underscores).

Port Binding Issues

Check the port configuration. Make sure you set the port to 8080 in the application settings and your Axum application binds to 0.0.0.0:8080, not 127.0.0.1:8080. Binding to 127.0.0.1 rejects external connections inside the container:

rust
let addr = SocketAddr::from(([0, 0, 0, 0], 8080));

OpenSSL Linking Errors

Use rustls instead of OpenSSL. If your build fails with OpenSSL-related errors, switch to rustls for TLS. Most Rust crates support it as a feature flag:

toml
[dependencies]
reqwest = { version = "0.11", features = ["rustls-tls"], default-features = false }
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres"] }

This eliminates the need for OpenSSL system libraries in your Docker image.

High Memory Usage

Check for unbounded buffers and connection pools. Rust applications rarely leak memory, but unbounded channels, growing Vecs, or oversized connection pools can cause high usage. Set explicit limits on connection pools and buffer sizes. Monitor memory in the Metrics tab to identify trends.

Next Steps

Your Rust Axum application is now deployed and running in production. Here's what to explore next:

  • Scale your application: Adjust instance types and auto-scaling settings for higher traffic
  • Set up CI/CD: Enable automatic deployments on every git push
  • Add monitoring: Integrate with external monitoring tools via Out Plane's metrics export
  • Deploy microservices: Use Out Plane to deploy multiple Rust services with internal networking

Summary

Deploying a Rust Axum application to Out Plane takes three steps:

  1. Connect your GitHub repository
  2. Configure Dockerfile build method, port (8080), and environment variables
  3. Deploy and get your live URL with automatic HTTPS

No server configuration, no manual SSL setup, no infrastructure management. Just push your code and go live.

Ready to deploy your Rust Axum application? Get started with Out Plane and receive $20 in free credit. Pay only for what you use with per-second billing.


Tags

rust
axum
deployment
tutorial
api
performance

Start deploying in minutes

Connect your GitHub repository and deploy your first application today. $20 free credit. No credit card required.