Back to Blog
Tutorial10 min read2026-06-26

How to Deploy an Axum Rust API with a Database

Axum is the Tokio team's ergonomic Rust web framework built on Tower. This guide covers a layered Axum app with shared state, a sqlx PostgreSQL pool, multi-stage Docker builds, and production deployment.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

Axum is a web framework from the Tokio team, built on top of the Tower middleware ecosystem. It's known for clean, type-driven handlers and excellent composability via Tower layers. Like any Rust service, the deployment story centers on a multi-stage build producing a small, fast binary — but Axum's state management and middleware are worth covering specifically.

An Axum app with shared state

# Cargo.toml
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres"] }
serde_json = "1"
tower-http = { version = "0.6", features = ["trace"] }
// src/main.rs
use axum::{routing::get, Router, extract::State, Json};
use sqlx::postgres::{PgPool, PgPoolOptions};
use serde_json::{json, Value};
use std::net::SocketAddr;

#[derive(Clone)]
struct AppState { pool: PgPool }

async fn health() -> Json<Value> {
    Json(json!({ "status": "ok" }))
}

async fn posts(State(state): State<AppState>) -> Json<Value> {
    let rows = sqlx::query!("SELECT id, title FROM posts LIMIT 20")
        .fetch_all(&state.pool).await.unwrap();
    let titles: Vec<_> = rows.into_iter().map(|r| r.title).collect();
    Json(json!({ "titles": titles }))
}

#[tokio::main]
async fn main() {
    let db_url = std::env::var("DATABASE_URL").expect("DATABASE_URL not set");
    let pool = PgPoolOptions::new().max_connections(10)
        .connect(&db_url).await.expect("db connect failed");

    let app = Router::new()
        .route("/health", get(health))
        .route("/api/posts", get(posts))
        .with_state(AppState { pool });

    let port: u16 = std::env::var("PORT").ok()
        .and_then(|p| p.parse().ok()).unwrap_or(8080);
    let addr = SocketAddr::from(([0, 0, 0, 0], port));
    let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

Axum's State extractor injects the shared pool into handlers in a type-safe way. The binding details — 0.0.0.0 and PORT from the environment — are the same production essentials as any container app.

Tower middleware

Axum's superpower is the Tower ecosystem: tracing, timeouts, compression, rate limiting are all reusable layers. Add request tracing in one line:

use tower_http::trace::TraceLayer;
// ...
let app = Router::new()
    .route("/health", get(health))
    .layer(TraceLayer::new_for_http())
    .with_state(state);

This gives you structured request logs that show up in your platform's live logs.

Migrations with sqlx

Axum is unopinionated about the database layer; sqlx is the common pairing. Migrations:

sqlx migrate add create_posts
sqlx migrate run    # apply as a deploy step

Apply migrations once per deploy before serving. If you use compile-time-checked query!, commit the offline cache so builds don't need a live DB:

cargo sqlx prepare   # writes .sqlx/ offline data

Commit the .sqlx/ directory. This is the detail most first-time sqlx-in-CI deploys miss.

Multi-stage Docker build

FROM rust:1-slim AS build
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo 'fn main(){}' > src/main.rs && cargo build --release && rm -rf src
COPY . .
ENV SQLX_OFFLINE=true
RUN cargo build --release

FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=build /app/target/release/myapi /usr/local/bin/myapi
EXPOSE 8080
CMD ["myapi"]

SQLX_OFFLINE=true plus the committed .sqlx/ cache means the build compiles without connecting to a database — essential for clean CI builds. ca-certificates enables TLS to the database.

Deploying on PandaStack

  1. 1Provision a managed PostgreSQL (14.x or 16.x). PandaStack injects DATABASE_URL; the binary reads it from the environment.
  2. 2Connect the Git repo as a container app. The Dockerfile is auto-detected.
  3. 3The platform sets PORT; the code reads it and binds 0.0.0.0.
  4. 4Run sqlx migrate run once as a deploy step.
  5. 5Add a custom domain; SSL is automatic.

Axum binaries are small and start instantly, so they're an excellent match for scale-to-zero tiers where cold start equals "launch one binary." For latency-sensitive APIs, use always-on compute.

Production checklist

  • [ ] .sqlx/ offline cache committed; SQLX_OFFLINE=true in the build.
  • [ ] Multi-stage build, ca-certificates in runtime image.
  • [ ] Bind 0.0.0.0, read PORT.
  • [ ] Migrations run as a deploy step.
  • [ ] TraceLayer (or similar) for observability.
  • [ ] --release build.

Verifying

curl -s https://api.example.com/health
# {"status":"ok"}

curl -s https://api.example.com/api/posts

Healthy responses and real query results confirm the build and database wiring.

References

  • [Axum documentation (docs.rs)](https://docs.rs/axum/latest/axum/)
  • [Tower HTTP middleware](https://docs.rs/tower-http/latest/tower_http/)
  • [sqlx offline mode](https://github.com/launchbadge/sqlx/blob/main/sqlx-cli/README.md)
  • [Rust official Docker images](https://hub.docker.com/_/rust)

---

Axum's tiny, instant-start binary is ideal for scale-to-zero, and PandaStack auto-detects your Dockerfile while injecting DATABASE_URL from a managed PostgreSQL. Try it free at [dashboard.pandastack.io](https://dashboard.pandastack.io).

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also