Rust applications on Fly.io are surprisingly easy to deploy, but the real magic is how Fly.io’s networking and scaling abstract away the complexities of traditional infrastructure.

Here’s a simple Rust web server using actix-web:

use actix_web::{get, App, HttpResponse, HttpServer, Responder};

#[get("/")]
async fn hello() -> impl Responder {
    HttpResponse::Ok().body("Hello from Fly.io!")
s}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(hello)
    })
    .bind(("0.0.0.0", 8080))? // Bind to 0.0.0.0 to accept connections from anywhere
    .run()
    .await
}

To deploy this, you’ll need a fly.toml file:

app = "my-rust-app"
primary_region = "ord"

[build]
  builder = "rust:1.70" # Use a recent Rust builder image
  command = "cargo build --release --target x86_64-unknown-linux-musl" # Build for musl for static linking

[deploy]
  release_command = "" # No release command needed for this simple app

[services]
  ports = [
    { port = 80, protocol = "tcp" }, # Expose port 80 for HTTP traffic
  ]

[services.concurrency]
  max_per_minute = 1000 # Adjust based on expected load

[env]
  RUST_BACKTRACE = "1" # Enable backtraces for debugging

The fly.toml tells Fly.io how to build and run your app. The builder = "rust:1.70" specifies the Docker image to use for building, and command = "cargo build --release --target x86_64-unknown-linux-musl" ensures your Rust app is compiled into a statically linked binary for Linux. This is crucial because Fly.io containers are minimal and don’t have a full C library by default.

Once you have your Rust code and fly.toml in the same directory, you can deploy with:

flyctl deploy

Fly.io will build your Docker image, push it, and then deploy it to the specified primary_region (ord in this case). It automatically sets up a load balancer and assigns your app a public IP address. When you navigate to your app’s URL (e.g., my-rust-app.fly.dev), you’ll see "Hello from Fly.io!".

The [services.concurrency] setting in fly.toml is a key lever for managing load. By default, Fly.io might start with a single instance. If your app receives 1001 requests in a minute, and max_per_minute is set to 1000, Fly.io will automatically scale up another instance to handle the overflow. This automatic scaling is driven by request volume, not just CPU or memory usage, which is a powerful abstraction for web services.

The [services.ports] section is where you define how your application listens for traffic. We’re mapping external TCP port 80 to your application’s internal port. Since our Rust app binds to 0.0.0.0:8080, Fly.io’s proxy handles the translation from external port 80 to your app’s 8080. The 0.0.0.0 bind address is essential here; binding to 127.0.0.1 or localhost would prevent external access.

The RUST_BACKTRACE=1 environment variable is a small but vital detail for debugging. If your Rust application panics in production, this setting ensures that a full backtrace is printed to stderr, which Fly.io captures and makes accessible via flyctl logs. Without it, you’d only see a generic panic message.

The most surprising thing about Fly.io deployments, especially with compiled languages like Rust, is how little configuration is needed to get a highly available, globally distributed application running. You don’t think about VMs, load balancers, or even container orchestration. You think about your app and its ports.

The real power comes from understanding how Fly.io’s edge network and automatic scaling work together. When traffic hits your app’s domain, it’s routed to the nearest Fly.io edge location. If that location has an instance of your app running, it serves the request. If it doesn’t, or if the existing instances are overloaded, Fly.io’s control plane orchestrates the launch of new instances in the closest available region. This is all managed by the flyctl CLI and the fly.toml configuration.

A common point of confusion is how Fly.io handles health checks. By default, Fly.io sends TCP packets to the port you’ve exposed. If your application isn’t responding on that port, Fly.io will consider the instance unhealthy and restart it. For more sophisticated checks, you can configure HTTP health checks in fly.toml to ping a specific endpoint (e.g., /health) on your application. This allows your application to signal its readiness more granularly than just being alive.

The next step you’ll likely encounter is managing persistent storage for your application, which involves configuring volumes in fly.toml and using Fly.io’s volume APIs within your Rust code.

Want structured learning?

Take the full Fly-io course →