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=true is doing real work here. The build image has no database, so the query! 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:

NeedCrate(s)
Rate limitingtower_governor
Cachingmoka
Auth / JWTsjsonwebtoken
Cloud SDKs (S3, email, KMS)the official aws-sdk-* family
Paymentsasync-stripe
Metrics & tracingtracing, sentry
Background jobstokio::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:

  1. Why Rust — safety and predictable performance without a GC.
  2. Four core ideas — ownership, Result/Option + ?, traits, async.
  3. Project setup — Cargo, dependencies, workspaces.
  4. Axum — routers, handlers, extractors.
  5. SQLx — pools, parameterized queries, compile-time checking.
  6. Errors, auth, contracts — IntoResponse, middleware, Serde + ts-rs.
  7. 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.