Actix-Web is one of the fastest web frameworks in any language, consistently topping benchmarks. Combined with Rust's memory safety and zero-cost abstractions, it's ideal for high-performance APIs and web services. This guide covers building an Actix-Web application, deploying it on a VPS, and configuring production infrastructure.
Project Setup
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
# Create new project
cargo new myapp && cd myapp
# Add dependencies to Cargo.toml
cat >> Cargo.toml HttpResponse {
HttpResponse::Ok().json(serde_json::json!({"status": "healthy"}))
}
async fn get_users(data: web::Data) -> HttpResponse {
match sqlx::query_as!(User, "SELECT id, name, email FROM users ORDER BY id")
.fetch_all(&data.db)
.await
{
Ok(users) => HttpResponse::Ok().json(users),
Err(e) => {
log::error!("Database error: {}", e);
HttpResponse::InternalServerError().json(
serde_json::json!({"error": "Internal server error"})
)
}
}
}
async fn create_user(
data: web::Data,
body: web::Json,
) -> HttpResponse {
match sqlx::query_as!(
User,
"INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, email",
body.name, body.email
)
.fetch_one(&data.db)
.await
{
Ok(user) => HttpResponse::Created().json(user),
Err(e) => {
log::error!("Failed to create user: {}", e);
HttpResponse::BadRequest().json(
serde_json::json!({"error": e.to_string()})
)
}
}
}
#[actix_web::main]
async fn main() -> std::io::Result {
dotenvy::dotenv().ok();
env_logger::init();
let database_url = env::var("DATABASE_URL")
.expect("DATABASE_URL must be set");
let pool = PgPoolOptions::new()
.max_connections(20)
.connect(&database_url)
.await
.expect("Failed to create pool");
let bind_addr = env::var("BIND_ADDR").unwrap_or_else(|_| "127.0.0.1:8080".to_string());
log::info!("Starting server on {}", bind_addr);
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(AppState { db: pool.clone() }))
.wrap(middleware::Logger::default())
.wrap(middleware::Compress::default())
.route("/health", web::get().to(health))
.route("/api/users", web::get().to(get_users))
.route("/api/users", web::post().to(create_user))
})
.bind(&bind_addr)?
.workers(num_cpus::get())
.run()
.await
}
Building for Production
# Release build with optimizations
cargo build --release
# Binary is at target/release/myapp
ls -lh target/release/myapp
# Typically 5-15MB — single static binary, no runtime needed
# Strip debug symbols for smaller binary
strip target/release/myapp
Cross-Compilation
# Build on your local machine, deploy binary to VPS
# Install cross-compilation target
rustup target add x86_64-unknown-linux-musl
# Build statically linked binary
cargo build --release --target x86_64-unknown-linux-musl
# Copy to VPS
scp target/x86_64-unknown-linux-musl/release/myapp user@vps:/opt/myapp/
Systemd Service
# /etc/systemd/system/myapp.service
[Unit]
Description=My Actix-Web Application
After=network.target postgresql.service
[Service]
Type=simple
User=appuser
Group=appuser
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/myapp
EnvironmentFile=/opt/myapp/.env
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/myapp/data
# Resource limits
LimitNOFILE=65535
MemoryMax=512M
[Install]
WantedBy=multi-user.target
# Create app user and deploy
sudo useradd -r -s /bin/false appuser
sudo mkdir -p /opt/myapp
sudo cp target/release/myapp /opt/myapp/
# Environment file
sudo tee /opt/myapp/.env