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:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh- Windows: Download and run
rustup-init.exefrom rustup.rs. Verify installation withrustc --versionin PowerShell. - macOS: Run the curl command above or use
brew install rustup-init && rustup-init - Linux: Run the curl command above. Add
$HOME/.cargo/binto your PATH.
Once Rust is installed, create a new project:
cargo new my-axum-app && cd my-axum-appIf 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:
[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:
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:
cargo runVisit 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:
# 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
- Go to console.outplane.com
- Sign in with your GitHub account
- 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
mainor 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:
DATABASE_URL=postgres://user:password@host:5432/database
RUST_LOG=infoStep 3: Deploy
Click Deploy Application and watch the build process:
- Queued → Waiting for resources
- Building → Compiling your Rust application in release mode
- Deploying → Starting your Axum server
- 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:
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:
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:
RUST_LOG=info,my_axum_app=debug,tower_http=debugError Handling with thiserror and anyhow
Use thiserror for library errors and anyhow for application errors. Return structured error responses from your handlers:
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:
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:
- Go to Databases in the sidebar
- Click Create Database
- Select PostgreSQL version and region
- Copy the connection URL
Add the connection URL as an environment variable:
DATABASE_URL=postgres://user:password@host:5432/databaseThen use it in your Axum application with sqlx for async, compile-time checked queries:
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:
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:
[dependencies]
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres"] }Custom Domain Setup
Replace the default .outplane.app URL with your own domain:
- Navigate to Domains
- Click Map Domain
- Enter your domain (e.g.,
api.yourdomain.com) - 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:
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:
[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:
- Connect your GitHub repository
- Configure Dockerfile build method, port (8080), and environment variables
- 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.