14. Deployment
📋 Jump to Takeaways🎁 A Rust binary is statically linked and tiny. The Docker image for your service can be under 10MB — smaller than most base images.
Multi-Stage Dockerfile
Rust compilation is slow. The trick is a two-stage build: compile in a full Rust image, copy only the binary into a minimal runtime image.
# Stage 1: build
FROM rust:1.80-slim AS builder
WORKDIR /app
# Cache dependencies separately from source
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release
RUN rm src/main.rs
# Build the real binary
COPY src ./src
COPY migrations ./migrations
RUN touch src/main.rs && cargo build --release
# Stage 2: runtime
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/url-shortener .
COPY migrations ./migrations
EXPOSE 3000
CMD ["./url-shortener"]The dependency caching trick (echo "fn main() {}") prevents re-downloading crates on every source change. Only when Cargo.toml or Cargo.lock changes do dependencies rebuild.
ca-certificates is needed for HTTPS requests to external services.
docker-compose for Local Development
# docker-compose.yml
services:
app:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://postgres:password@db:5432/urlshortener
PORT: 3000
LOG_LEVEL: debug
JWT_SECRET: local-dev-secret
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: urlshortener
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5docker-compose up --builddepends_on with condition: service_healthy waits for PostgreSQL to be ready before starting the app — prevents connection errors on first start.
Health Check Endpoint
Every deployment platform needs a health check. Return 200 when the service is ready, 503 when it's not:
use axum::{extract::State, http::StatusCode, Json};
use serde::Serialize;
#[derive(Serialize)]
struct HealthResponse {
status: &'static str,
version: &'static str,
}
pub async fn health(State(state): State<AppState>) -> (StatusCode, Json<HealthResponse>) {
// Check database connectivity
match sqlx::query("SELECT 1").execute(&state.pool).await {
Ok(_) => (
StatusCode::OK,
Json(HealthResponse { status: "ok", version: env!("CARGO_PKG_VERSION") }),
),
Err(_) => (
StatusCode::SERVICE_UNAVAILABLE,
Json(HealthResponse { status: "degraded", version: env!("CARGO_PKG_VERSION") }),
),
}
}env!("CARGO_PKG_VERSION") reads the version from Cargo.toml at compile time.
Environment Variables in Production
Never put secrets in the Docker image. Pass them at runtime:
docker run \
-e DATABASE_URL="postgres://..." \
-e JWT_SECRET="production-secret" \
-p 3000:3000 \
url-shortenerOn Kubernetes, use Secret objects. On Railway, Fly.io, or Render, set them in the dashboard.
Makefile
Makefile recipes must be indented with a hard tab, not spaces. Most editors look the same but Make treats them differently — spaces produce a missing separator error. Check your editor's tab/space setting before saving.
.PHONY: build run test docker-build docker-run
build:
cargo build --release
run:
cargo run
test:
cargo test
docker-build:
docker build -t url-shortener .
docker-run:
docker-compose up --build
migrate:
sqlx migrate run
prepare:
cargo sqlx prepareKey Takeaways
- Multi-stage Docker builds: compile in
rust:slim, copy binary todebian:slim— images under 10MB - The dependency pre-build trick (
echo "fn main() {}") caches crate downloads across builds docker-composewithhealthcheckensures PostgreSQL is ready before the app starts- Health endpoints check real dependencies (database) and return 503 on failure, not just 200
env!("CARGO_PKG_VERSION")embeds the version fromCargo.tomlat compile time- Never put secrets in Docker images — pass them as environment variables at runtime
🎁 You've built every piece. Now let's connect them all into a complete, running URL shortener service from project setup to first deployed request.