Updated Aug 27, 2026

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: 5
docker-compose up --build

depends_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-shortener

On 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 prepare

Key Takeaways

  • Multi-stage Docker builds: compile in rust:slim, copy binary to debian:slim — images under 10MB
  • The dependency pre-build trick (echo "fn main() {}") caches crate downloads across builds
  • docker-compose with healthcheck ensures 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 from Cargo.toml at 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.

📝 Ready to test your knowledge?

Answer the quiz below to mark this lesson complete.

Spot something off? Report an issue
© 2026 ByteLearn.dev. Free courses for developers. · Privacy