Deployment Approaches

Rust deployment strategies — static binaries, Docker, cross-compilation, systemd, cloud platforms, and CI/CD

Rust compiles to a single static binary with no runtime dependencies. This makes deployment simpler than most languages — there’s no interpreter, no VM, and usually no system libraries to install.

Static binary

The simplest deployment: compile and copy.

# Build a release binary
cargo build --release

# The binary is at:
ls -lh target/release/myapp

# Copy it anywhere and run
scp target/release/myapp server:/usr/local/bin/
ssh server 'myapp'

Optimized release builds

# Cargo.toml — optimize for small binary size
[profile.release]
opt-level = "z"     # optimize for size
lto = true           # link-time optimization
codegen-unput = 1    # slower build, better optimization
strip = true         # strip debug symbols
# Build and check size
cargo build --release
ls -lh target/release/myapp

# Further size reduction with UPX
upx --best target/release/myapp

Docker

Minimal multi-stage Dockerfile

# --- Build stage ---
FROM rust:1.78-alpine AS builder

RUN apk add --no-cache musl-dev

WORKDIR /app
COPY Cargo.toml Cargo.lock ./
# Cache dependencies
RUN mkdir src && echo "fn main() {}" > src/main.rs && cargo build --release && rm -rf src

COPY src src
COPY Cargo.toml .
RUN touch src/main.rs && cargo build --release

# --- Runtime stage ---
FROM alpine:3.19

RUN apk add --no-cache ca-certificates

COPY --from=builder /app/target/release/myapp /usr/local/bin/myapp

EXPOSE 8080
CMD ["myapp"]

Debian-based Dockerfile (if you need glibc)

# --- Build stage ---
FROM rust:1.78-slim AS builder

WORKDIR /app
COPY . .
RUN cargo build --release

# --- Runtime stage ---
FROM debian:bookworm-slim

RUN apt-get update && apt-get install -y ca-certificates libssl3 && rm -rf /var/lib/apt/lists/*

COPY --from=builder /app/target/release/myapp /usr/local/bin/myapp

EXPOSE 8080
CMD ["myapp"]

Docker Compose

version: "3.8"
services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/myapp
      - RUST_LOG=info
    depends_on:
      - db

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: myapp
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

Cross-compilation

Compile on your Mac/CI for a different target:

# Add a target
rustup target add x86_64-unknown-linux-musl
rustup target add aarch64-unknown-linux-musl
rustup target add x86_64-pc-windows-msvc

# Build for the target
cargo build --release --target x86_64-unknown-linux-musl

# Cross-compile with the `cross` tool (uses Docker, easier for complex targets)
cargo install cross
cross build --release --target x86_64-unknown-linux-gnu
cross build --release --target aarch64-unknown-linux-gnu
cross build --release --target x86_64-pc-windows-gnu

systemd service

# /etc/systemd/system/myapp.service
[Unit]
Description=My Rust Application
After=network.target

[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStart=/usr/local/bin/myapp
Restart=on-failure
RestartSec=5
Environment=RUST_LOG=info
Environment=DATABASE_URL=postgres://user:pass@localhost/myapp

# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/myapp/data

[Install]
WantedBy=multi-user.target
# Enable and start
sudo systemctl daemon-reload
sudo systemctl enable myapp
sudo systemctl start myapp
sudo systemctl status myapp

# View logs
journalctl -u myapp -f

Cloud platforms

AWS (EC2 or ECS)

# ECR push
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 123456789.dkr.ecr.us-east-1.amazonaws.com
docker build -t myapp .
docker tag myapp:latest 123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:latest
docker push 123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:latest

# ECS task definition points to the ECR image
# Or just scp the binary to EC2 and run under systemd

Google Cloud Run

# Build and push to Artifact Registry
gcloud builds submit --tag gcr.io/PROJECT_ID/myapp

# Deploy to Cloud Run
gcloud run deploy myapp \
  --image gcr.io/PROJECT_ID/myapp \
  --platform managed \
  --region us-east1 \
  --allow-unauthenticated \
  --set-env-vars "RUST_LOG=info"

Fly.io

# Initialize
fly launch

# Set secrets
fly secrets set DATABASE_URL=postgres://user:pass@host/db
fly secrets set SECRET_KEY=$(openssl rand -hex 32)

# Deploy
fly deploy

# Scale
fly scale count 2
fly scale memory 512

Heroku

# Use the Rust buildpack
heroku create myapp
heroku buildpacks:set emk/heroku-rust

# Or use Docker
heroku container:login
heroku container:push web -a myapp
heroku container:release web -a myapp

# Set config
heroku config:set RUST_LOG=info
heroku config:set DATABASE_URL=postgres://...

CI/CD

GitHub Actions

# .github/workflows/ci.yml
name: CI
on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: clippy, rustfmt
      - uses: Swatinem/rust-cache@v2
      - run: cargo fmt -- --check
      - run: cargo clippy -- -D warnings
      - run: cargo test
      - run: cargo build --release

  docker:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v5
        with:
          push: true
          tags: ghcr.io/${{ github.repository }}:latest

Environment configuration

// Use dotenv for local dev, env vars for production
use std::env;

fn get_config() -> Config {
    Config {
        host: env::var("HOST").unwrap_or_else(|_| "0.0.0.0".to_string()),
        port: env::var("PORT")
            .unwrap_or_else(|_| "8080".to_string())
            .parse()
            .expect("PORT must be a number"),
        database_url: env::var("DATABASE_URL")
            .expect("DATABASE_URL must be set"),
        log_level: env::var("RUST_LOG")
            .unwrap_or_else(|_| "info".to_string()),
    }
}

Health checks and graceful shutdown

use tokio::signal;
use tokio::sync::broadcast;

async fn main() {
    let (shutdown_tx, _) = broadcast::channel(1);

    // Spawn server with shutdown signal
    let server = run_server(shutdown_tx.subscribe());

    // Wait for Ctrl+C or SIGTERM
    signal::ctrl_c().await.expect("failed to listen for ctrl+c");

    // Signal shutdown
    let _ = shutdown_tx.send(());

    // Wait for server to finish
    server.await;
}

Deployment checklist

Item Details
Build cargo build --release with LTO and strip
Binary Single static binary, no runtime deps
Config Environment variables, not config files
Secrets DATABASE_URL, SECRET_KEY via env vars
Logging Use tracing crate, set RUST_LOG
Health Add /health endpoint
Shutdown Handle SIGTERM for graceful shutdown
Docker Multi-stage build, Alpine or scratch for minimal image
Monitoring tracing + metrics crates for observability