Skip to content
Docs

Deploy Rust on Vercel with Docker

Build a Rust application with Axum and Docker, then deploy it to Vercel Functions. Learn how to configure environment variables, managed storage, and preview deployments for your project.

Anshuman BhardwajContent Engineer

Axum is a Rust web framework built on Tokio and Tower. Vercel provides a native Rust runtime in public beta, but a container is useful when you want to run an existing Axum HTTP server, control its Linux dependencies, or preserve an existing Docker build.

Cargo produces a native binary; a multi-stage build can compile the application once and omit the compiler and toolchain from the final image. Add a Dockerfile.vercel and set the service runtime to container. Vercel builds the image, stores it in the Vercel Container Registry, and serves it from a Function that autoscales with traffic and scales to zero when idle.

This guide deploys a minimal Axum API, binds it to Vercel's PORT, and explains how to use durable external storage instead of the container file system.

Copy link to headingPrerequisites

Copy link to headingHow it works

The project is one Axum application with the following parts:

  • src/main.rs defines / and /health, binds to 0.0.0.0 on $PORT, and drains on SIGTERM.
  • Cargo.toml declares Axum, Tokio, and Serde, and Cargo.lock pins their resolved versions.
  • Dockerfile.vercel compiles a release binary in one stage and copies it into a minimal final stage.
  • vercel.json declares a container service and rewrites incoming requests to it.
  • On deployment, Vercel builds the image and runs it as a Vercel Function.

Copy link to headingSteps

Copy link to heading1. Initialize the project

Create the project and add the dependencies with the features this guide uses:

Terminal
cargo new vercel-docker-rust
cd vercel-docker-rust
cargo add axum
cargo add serde --features derive
cargo add tokio --features macros,net,rt-multi-thread,signal

The resulting Cargo.toml should look like this. The release profile is optional, but keeps the shipped binary small:

Cargo.toml
[package]
name = "vercel-docker-rust"
version = "0.1.0"
edition = "2024"
[dependencies]
axum = "0.8"
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "signal"] }
[profile.release]
strip = true
lto = "thin"

Now, for the HTTP server, update the src/main.rs:

src/main.rs
use axum::{routing::get, Json, Router};
use serde::Serialize;
use std::{env, net::SocketAddr};
#[derive(Serialize)]
struct Message {
message: &'static str,
}
#[derive(Serialize)]
struct Health {
status: &'static str,
}
async fn hello() -> Json<Message> {
Json(Message { message: "Hello from Rust on Vercel" })
}
async fn health() -> Json<Health> {
Json(Health { status: "ok" })
}
#[tokio::main]
async fn main() {
let port = env::var("PORT").ok().and_then(|v| v.parse().ok()).unwrap_or(80);
let address = SocketAddr::from(([0, 0, 0, 0], port));
let app = Router::new()
.route("/", get(hello))
.route("/health", get(health));
let listener = tokio::net::TcpListener::bind(address).await.unwrap();
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
.await
.unwrap();
}
async fn shutdown_signal() {
let mut terminate =
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("install SIGTERM handler");
terminate.recv().await;
}

The server must bind to 0.0.0.0, not 127.0.0.1, and read PORT. The fallback to port 80 matches Vercel's default container port.

Copy link to heading2. Add Dockerfile.vercel

Create the Dockerfile.vercel file in the project root:

Dockerfile.vercel
FROM lukemathwalker/cargo-chef:latest-rust-1 AS chef
WORKDIR /app
FROM chef AS planner
COPY . .
RUN cargo chef prepare --recipe-path recipe.json
FROM chef AS build
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json
COPY . .
RUN cargo build --release
FROM gcr.io/distroless/cc-debian12:nonroot
COPY --from=build /app/target/release/vercel-docker-rust /usr/local/bin/server
ENV PORT=80
ENTRYPOINT ["/usr/local/bin/server"]

Compiling dependencies from the Cargo Chef recipe before copying the application code lets Docker reuse the dependency layer. The final stage contains the binary and the C libraries it links against, excludes Cargo and the compiler, and runs as the image's non-root user. The path in the COPY line must match the package name in Cargo.toml, and pinning an exact toolchain tag instead of latest-rust-1 makes builds reproducible.

Copy link to heading3. Add vercel.json

Create a vercel.json file to configure your project:

vercel.json
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"services": {
"api": {
"root": ".",
"entrypoint": "Dockerfile.vercel",
"runtime": "container"
}
},
"rewrites": [
{ "source": "/(.*)", "destination": { "service": "api" } }
]
}

The service selects the container runtime and points to the Dockerfile. The catch-all rewrite sends every public path to the Axum service.

Copy link to heading4. Run locally

Run the project through Vercel's local container environment:

Terminal
vercel dev -L

Use the URL printed by the CLI, usually http://localhost:3000:

Terminal
curl http://localhost:3000/
curl http://localhost:3000/health

Copy link to heading5. Deploy to Vercel

Authenticate and create a production deployment:

Terminal
vercel login
vercel deploy --prod

For Git-based deployments, push the directory to a repository, import it in Vercel, and keep both configuration files at the configured project root.

Copy link to headingExtend your application

You have deployed an Axum API as a native Rust binary. You can now add routes, middleware, authentication, and external data services.

Copy link to headingEnvironment variables

Add configuration in the project settings or with vercel env add NAME. Pull development values locally with vercel env pull. Read secrets at runtime with std::env rather than baking them in at compile time with env!, and do not commit them or include them in the image.

Copy link to headingDatabase and storage

The container runs on Vercel Functions, where the file system is not persistent. Here are some recommendations for persistent workloads:

Use caseRecommended service
Files and uploadsVercel Blob
Relational dataMarketplace Postgres integration
Sessions and cachingMarketplace Redis integration
Frequently read configurationVercel Global Config

Keep database pools small, build them once during startup, and share them through Axum state so a warm instance can reuse connections.

Copy link to headingIterate with preview deployments

A connected Git repository receives a unique Preview Deployment for each branch push or pull request. Run vercel deploy without --prod to create the same disposable environment from the CLI.

Copy link to headingScale automatically with Fluid compute

Vercel Services use Fluid compute by default. Instances can process concurrent requests and scale to zero when idle. Active CPU billing stops while code waits on I/O and when no requests are running; memory and invocation charges still apply.

Copy link to headingTroubleshooting

Copy link to headingThe binary starts locally but requests return 502

Cause: The listener is bound to 127.0.0.1 or a hardcoded port instead of the container interface and the port Vercel routes to.

Fix: Bind to [0, 0, 0, 0] and parse PORT, with 80 as the fallback. EXPOSE does not configure routing.

Copy link to headingThe final image reports a missing loader or library

Cause: The binary was linked against libraries that the runtime image does not provide, which commonly happens when build and runtime stages mix Alpine and Debian.

Fix: Keep the build and runtime images in the same family, or build a static MUSL binary and pair it with a matching static image. Run the exact final image locally before deploying.

Copy link to headingBuilds remain slow after small source changes

Cause: Copying the whole source tree before compiling dependencies invalidates the Cargo layer cache on every edit.

Fix: Keep the Cargo Chef planner and cargo chef cook stages ahead of the source build, and commit Cargo.lock so dependency resolution is reproducible.

Copy link to headingSpawned Tokio tasks stop before completion

Cause: Tasks detached with tokio::spawn assume the process outlives the response. Scale-in can send SIGTERM at any point after a request finishes.

Fix: Finish request work before responding, use Vercel Cron for schedules, and use Workflow for durable multi-step work. Keep request handlers idempotent.

Copy link to headingNext steps

More Vercel Services guides