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 stepApply 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 dataCommit 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
- 1Provision a managed PostgreSQL (14.x or 16.x). PandaStack injects
DATABASE_URL; the binary reads it from the environment. - 2Connect the Git repo as a container app. The Dockerfile is auto-detected.
- 3The platform sets
PORT; the code reads it and binds0.0.0.0. - 4Run
sqlx migrate runonce as a deploy step. - 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=truein the build. - [ ] Multi-stage build,
ca-certificatesin runtime image. - [ ] Bind
0.0.0.0, readPORT. - [ ] Migrations run as a deploy step.
- [ ]
TraceLayer(or similar) for observability. - [ ]
--releasebuild.
Verifying
curl -s https://api.example.com/health
# {"status":"ok"}
curl -s https://api.example.com/api/postsHealthy 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).