DocsRust

Rust

Deploy a Rust app on Ruust: Axum or Actix, detected from Cargo.toml, compiled in release mode and served on HTTPS.

Ruust hands your repo to Nixpacks, whose Rust provider recognises the app from a Cargo.toml file at the repo root. It reads your crate and toolchain, runs a release build with cargo build --release, and hatches the resulting binary as an Egg. This works the same way whether your server is built on Axum or Actix Web: both read the port from the environment and listen on a socket, which is all Ruust needs.

Deploy it

  1. Commit Cargo.toml (and Cargo.lock) at the repo root and push to a Git repo.
  2. In the dashboard, choose New Egg and connect the repo.
  3. Pick a size and a region (London or Virginia), then lay the Egg.
  4. Ruust runs cargo build --release and hatches the compiled binary.

The port

Ruust sets a PORT environment variable. Your server must read it and bind to 0.0.0.0, not localhost. Binding to the wrong address is the top reason an Egg builds but never hatches: the process is up, but nothing outside the container can reach it. Read PORT with std::env::var and construct the bind address from it.

rust
use std::env;
use axum::{routing::get, Router};
use tokio::net::TcpListener;

#[tokio::main]
async fn main() {
    let app = Router::new().route("/", get(|| async { "ok" }));

    let port: u16 = env::var("PORT")
        .unwrap_or_else(|_| "3000".to_string())
        .parse()
        .expect("PORT must be a number");

    let listener = TcpListener::bind(("0.0.0.0", port)).await.unwrap();
    axum::serve(listener, app).await.unwrap();
}
Axum: read PORT and bind to 0.0.0.0.

Actix Web

Actix follows the same rule: pass a (host, port) tuple to bind, with the host set to 0.0.0.0. Read PORT from the environment exactly as above.

rust
use std::env;
use actix_web::{web, App, HttpServer, Responder};

async fn index() -> impl Responder {
    "ok"
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let port: u16 = env::var("PORT")
        .unwrap_or_else(|_| "3000".to_string())
        .parse()
        .expect("PORT must be a number");

    HttpServer::new(|| App::new().route("/", web::get().to(index)))
        .bind(("0.0.0.0", port))?
        .run()
        .await
}
Actix Web: bind to 0.0.0.0 on PORT.

The start command

Nixpacks runs the release binary for you. To set the start command yourself, add a Procfile at the repo root with a web: line: on the Nixpacks path that line is read by Nixpacks and baked into the image as the start command (the Ruust workload spec carries no start-command field for the run path, and the Procfile is ignored when you supply your own Dockerfile). The binary lives at target/release/<name>, where the name matches the [[bin]] (or package) name in your Cargo.toml.

text
web: ./target/release/app
Procfile at the repo root pins the start command.

Environment variables

Set env vars on the Egg. They are available at both build and run time, encrypted at rest, and never printed in logs. Read them with std::env::var at run time. Rust ships no client bundle, so there is no public prefix to worry about: everything stays server-side.

Builds are slower

Rust compiles ahead of time, so a release build takes noticeably longer than an interpreted stack. That cost is paid at build time only: once the Egg hatches, the binary starts fast and runs lean.

dockerfile
FROM rust:1.79 AS build
WORKDIR /src
COPY Cargo.toml Cargo.lock ./
COPY src ./src
RUN cargo build --release

FROM gcr.io/distroless/cc-debian12
COPY --from=build /src/target/release/app /app
ENV PORT=3000
EXPOSE 3000
ENTRYPOINT ["/app"]
Multi-stage build with a distroless final stage.