Step 7 — Build, Run & Deploy
You have a complete Tasks API: routed endpoints, real Postgres queries, typed errors, auth middleware, and a locked JSON contract. Let's ship it.
Debug vs release builds
cargo run # debug build — fast to compile, slower to run
cargo build --release # optimized build — slow to compile, fast to run
./target/release/tasks-api
Always deploy the release build. The optimizer makes a large difference for a server, and the binary is self-contained.
Observability: logs you can actually use
Before production, add structured logging with tracing. Unlike println!, it produces structured, level-filtered, span-aware output:
# Cargo.toml
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_env_filter("info,tasks_api=debug")
.init();
tracing::info!("starting tasks-api");
// ...
}
You already used tracing::error!(?e, ...) in the error handler in Step 6 — now it's captured. In production, tracing feeds log aggregators and error trackers (iKanban wires it into Sentry). The ?e syntax records the value as a structured field, not just text.
Configuration from the environment
Never hardcode secrets. Read them from the environment (a container platform injects them):
let database_url = std::env::var("DATABASE_URL")
.expect("DATABASE_URL must be set");
let port = std::env::var("PORT").unwrap_or_else(|_| "3000".to_string());
Containerizing: the multi-stage Dockerfile
The standard way to deploy a Rust service is a multi-stage Docker build: compile in a full toolchain image, then copy just the binary into a tiny runtime image.
# ---- build stage ----
FROM rust:1.86 AS builder
WORKDIR /app
COPY . .
ENV SQLX_OFFLINE=true # use the committed .sqlx/ cache — no DB at build time
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/*
COPY --from=builder /app/target/release/tasks-api /usr/local/bin/tasks-api
EXPOSE 3000
CMD ["tasks-api"]
The result is a small image containing a single static-ish binary — no interpreter, no node_modules, no JVM. This is one of Rust's operational joys: the deploy artifact is tiny and starts instantly.
SQLX_OFFLINE=trueis doing real work here. The build image has no database, so thequery!macros read the committed.sqlx/cache instead of connecting. This is why Step 5 insisted you commit that directory — without it, the Docker build fails.
A pre-deploy checklist
Before shipping a Rust backend, run the same gates a production team runs:
cargo fmt --check # formatting is consistent
cargo clippy -- -D warnings # zero lint warnings (treat warnings as errors)
cargo check # everything type-checks
cargo test # tests pass
Then, if you changed any query!/query_as! macro:
cargo sqlx prepare # regenerate the offline cache
git add .sqlx/ # commit it alongside your change
Batch your deploys. Compiling and deploying a Rust service takes minutes, not seconds. iKanban's workflow is explicit about grouping related backend changes into one push, because each push triggers a multi-minute build-and-deploy. Plan the change, make it complete, then ship once.
Where a real backend goes from here
You've built and shipped a working Rust API. The same four ideas from Step 2 scale all the way up. When you're ready for production concerns, the ecosystem has mature answers:
| Need | Crate(s) |
|---|---|
| Rate limiting | tower_governor |
| Caching | moka |
| Auth / JWTs | jsonwebtoken |
| Cloud SDKs (S3, email, KMS) | the official aws-sdk-* family |
| Payments | async-stripe |
| Metrics & tracing | tracing, sentry |
| Background jobs | tokio::spawn, task queues |
Every one of these is in iKanban's own Cargo.toml — the service you use is built from exactly the pieces this tutorial introduced.
You made it
You now understand, end to end, how a Rust backend is built:
- Why Rust — safety and predictable performance without a GC.
- Four core ideas — ownership,
Result/Option+?, traits, async. - Project setup — Cargo, dependencies, workspaces.
- Axum — routers, handlers, extractors.
- SQLx — pools, parameterized queries, compile-time checking.
- Errors, auth, contracts —
IntoResponse, middleware, Serde + ts-rs. - Shipping — release builds, tracing, Docker, deploy discipline.
The best next step is to open a real handler in a codebase and read it — you now have the vocabulary to follow every line. Happy building.