Back to Blog
Tutorial10 min read2026-07-05

How to Deploy a Rust Axum API

Axum is the ergonomic, Tokio-based web framework taking over Rust backends. Learn how to build a lean multi-stage container image and deploy an Axum API to production with a managed database.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

Axum has become the default choice for Rust web services — built by the Tokio team, it's fast, ergonomic, and composes beautifully with the Tower middleware ecosystem. Rust's reward for its compile-time strictness is a deployment story that's almost boring: a single static-ish binary, tiny memory footprint, and no runtime to install. This guide covers building a lean container image and deploying an Axum API with a managed PostgreSQL database.

Why Rust is a joy to deploy

A compiled Rust binary has no interpreter, no garbage collector, and minimal runtime dependencies. The result is a container that's small, starts fast, and sips memory — which pairs especially well with scale-to-zero environments where cold-start cost matters. The only real deployment trick is producing a small image, since the Rust build toolchain is large. Multi-stage Docker builds solve that.

Step 1: A minimal Axum app

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

#[derive(Serialize)]
struct Health { status: &'static str }

async fn health() -> Json<Health> {
    Json(Health { status: "ok" })
}

#[tokio::main]
async fn main() {
    let app = Router::new().route("/health", get(health));

    // bind to the platform-provided PORT, default 3000
    let port: u16 = std::env::var("PORT")
        .ok().and_then(|p| p.parse().ok()).unwrap_or(3000);
    let addr = SocketAddr::from(([0, 0, 0, 0], port));

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

The two production essentials: bind to 0.0.0.0 (not localhost) and read PORT from the environment.

Step 2: The multi-stage Dockerfile

This is where image size is won. Build in a full Rust image, then copy just the binary into a tiny runtime image:

# ---- build stage ----
FROM rust:1.82 AS builder
WORKDIR /app
# cache dependencies first
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs && cargo build --release && rm -rf src
COPY . .
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/*
WORKDIR /app
COPY --from=builder /app/target/release/my-api /usr/local/bin/my-api
EXPOSE 3000
CMD ["my-api"]

The dummy-main.rs trick caches your dependency compilation as a separate layer, so editing your source doesn't recompile every crate. That alone can turn a 10-minute rebuild into seconds.

Image strategyApprox sizeNotes
Single-stage rust:1.82~1.5 GB+Ships the whole toolchain
Multi-stage on debian:slim~80-120 MBRecommended baseline
Static musl on scratch/distroless~10-20 MBSmallest, needs musl target

For the absolute smallest image, build with the x86_64-unknown-linux-musl target and copy into scratch or distroless — but debian:slim is the pragmatic default that avoids musl/OpenSSL headaches.

Step 3: Add a database

Most APIs need persistence. sqlx is a great async, compile-time-checked choice. Read the connection string from the environment:

use sqlx::postgres::PgPoolOptions;

let pool = PgPoolOptions::new()
    .max_connections(5)
    .connect(&std::env::var("DATABASE_URL")?)
    .await?;

Using DATABASE_URL means the platform can inject it for you.

Step 4: Deploy on PandaStack

  1. 1Provision a managed PostgreSQL database (16.x).
  2. 2Create a container app from your repo. PandaStack builds your Dockerfile with rootless BuildKit — watch the live build logs as Cargo compiles.
  3. 3Link the databaseDATABASE_URL is auto-injected, exactly what sqlx reads. No manual connection-string handling.
  4. 4Attach a custom domain with automatic SSL.

That's it. Rust's small binary means fast deploys and low memory, so even the smaller compute tiers go a long way.

Step 5: Production polish

  • Migrations — run sqlx migrate run as a release/pre-deploy step, or use sqlx::migrate!() at startup for embedded migrations.
  • Tracing — add tower-http's TraceLayer and the tracing crate for structured logs that show up in your platform's live logs.
  • Graceful shutdown — use axum::serve(...).with_graceful_shutdown(...) so in-flight requests finish on redeploy.
  • Health checks — keep the /health endpoint cheap; platforms use it for readiness.

Build-time considerations

Rust's strength (thorough compilation) means longer build times than interpreted languages, especially on first build. Mitigations:

  • The dependency-caching layer trick above.
  • Keep your dependency tree lean; every crate adds compile time.
  • Use cargo build --release only for deploys; the optimized build is slower but produces the fast binary you want in production.

Honest caveats

Rust deployment is genuinely pleasant, but the build is the slow part — large dependency trees and release optimization mean your CI/deploy step takes longer than a Node or Python app. The caching strategy helps a lot, but plan for it. The runtime, however, is the payoff: tiny, fast, and memory-frugal. For latency-sensitive or high-concurrency APIs, that trade is well worth it.

Wrapping up

Deploying an Axum API is mostly about producing a lean image via a multi-stage build and binding correctly to PORT and 0.0.0.0. Add a managed Postgres with an auto-injected DATABASE_URL, layer in tracing and graceful shutdown, and you have a production Rust service that's fast and cheap to run.

PandaStack builds your Dockerfile, auto-wires the database, and serves it with automatic SSL — and Rust's small footprint is a great fit for the free tier. Deploy your Axum API at https://dashboard.pandastack.io.

References

  • Axum documentation: https://docs.rs/axum/latest/axum/
  • Tokio project: https://tokio.rs/
  • sqlx (async, compile-checked SQL): https://github.com/launchbadge/sqlx
  • Multi-stage Docker builds: https://docs.docker.com/build/building/multi-stage/
  • The Cargo book (release profiles): https://doc.rust-lang.org/cargo/reference/profiles.html

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also