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/myappDocker
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-gnusystemd 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 -fCloud 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 systemdGoogle 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 512Heroku
# 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 }}:latestEnvironment 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 |