A coral octopus secures an Axum application module inside a VPS server as an HTTPS lock lights up.

How to Deploy a Rust Axum App to a VPS with Automatic HTTPS

Tako-kun·

Networking update. The server setup described here uses Tako’s legacy VPN integration. For new installations, see native WireGuard networks. For an existing Tailscale or NetBird host, follow the migration guide.

Your Axum router works locally. Getting it onto a VPS adds a second project: build a Linux binary, keep it running, issue a certificate, and work out whether the next version is healthy before sending users to it.

We’ll take a small Rust app through that path with Tako. The app uses the Rust SDK, a Dockerfile supplies the production build, and Tako handles public HTTPS and rolling releases. You keep your router. The certificate has somewhere else to live.

Build an Axum app with the Tako Rust SDK

Start with a Rust toolchain and create a binary project:

cargo new axum-on-tako --bin
cd axum-on-tako

Keep the package metadata Cargo created and add these dependencies to Cargo.toml:

[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["full"] }
tako = { version = "0.2.1", features = ["axum"] }

The tako crate’s axum feature provides the serving adapter. This example follows the Rust and Axum guide; the implementation is in the Rust SDK source.

Replace src/main.rs with:

use axum::{routing::get, Router};

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let app = Router::new()
        .route("/", get(|| async { "Hello from Axum on Tako" }))
        .route("/api/health", get(|| async { "ok" }));

    tako::axum::serve(app).await
}

The routes use Axum’s normal Router and get APIs. The serving call is the integration point: tako::axum::serve binds the SDK listener and wraps your router with authenticated internal status handling. Keep your existing handlers and middleware on that router.

Our /api/health endpoint is an application smoke test. Tako’s internal /status probe has its own host and token checks, supplied by the SDK. You don’t need to turn the public endpoint into an imitation of that protocol.

Generate the lockfile and check the app:

cargo generate-lockfile
cargo check
cargo test

Keep Cargo.lock with the application source: our production build will use --locked. We’ll exercise the HTTP routes through the image and Tako-managed local development below. If you’re adapting an existing workspace, substitute its binary name wherever this guide uses axum-on-tako.

The SDK also exposes runtime secrets through tako::secret("NAME"). We’ll leave this app secret-free so the first deploy only needs to prove routing and startup. When adding a database or API credentials, follow the Secrets guide rather than copying values into the Dockerfile.

Package the Rust binary in a container release

Create a Dockerfile in the project root:

FROM rust:1-bookworm AS build
WORKDIR /src
COPY . .
RUN cargo build --locked --release --bin axum-on-tako

FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \
    && rm -rf /var/lib/apt/lists/*
COPY --from=build /src/target/release/axum-on-tako /app/app
ENV HOST=0.0.0.0 PORT=3000
EXPOSE 3000
CMD ["/app/app"]

This follows Tako’s Rust Dockerfile example. The first stage compiles the application; the second copies the binary into the runtime image. Docker’s multi-stage build guide explains the same separation between build tools and shipped artifacts.

Both stages use Bookworm here to keep their native-library baseline aligned. Projects that link additional system libraries must provide compatible runtime packages too. Pin the tested base-image versions or digests for production; the broad tags above keep the example readable.

Add a .dockerignore:

.git
.tako
.env*
target

Tako first stages the upload context respecting .gitignore and its own exclusions. The image builder then applies .dockerignore. Ensure your Rust sources, manifests, lockfile, and Dockerfile survive both steps. A missing source file cannot be recovered by COPY . . after it has already been excluded from the upload.

If Docker is available locally, smoke-test the image:

docker build -t axum-on-tako .
docker run --rm -p 3000:3000 axum-on-tako

In another terminal, request http://localhost:3000/ and http://localhost:3000/api/health; expect the greeting and ok. This verifies image assembly and the HTTP listener; the full Tako bootstrap and deployment checks happen on the server.

You can use Podman for that local smoke test instead. A local image builder is optional for tako deploy: Tako uploads the source and builds the image on the target VPS with Podman. This gives the Rust compiler a Linux build environment without requiring a separately published application image.

Create tako.toml:

name = "axum-on-tako"
container = "Dockerfile"
dev = "cargo run"

[envs.production]
route = "axum.example.com"
servers = ["prod"]

The Dockerfile owns the production build and startup command. Container releases reject main, start, assets, [build], and [[build_stages]] in this configuration. There is no built-in Rust preset or runtime = "rust"; this tutorial uses the documented container release path.

The explicit dev command runs Rust locally. After installing the CLI in the next section, run tako dev from this configured project. Open the HTTPS URL it reports and request /api/health there too. Tako supplies the SDK bootstrap and listener settings for that process. Local development does not build or run the Dockerfile, so check the image separately when changing system packages or image assembly.

Deploy to the VPS and verify HTTPS

Install the Tako CLI on your workstation:

curl -fsSL https://tako.sh/install.sh | sh

Use a supported Linux VPS with glibc, systemd, and Tako’s required isolation controllers. Connect it and your workstation through Tailscale or NetBird, allowing SSH and private management traffic on TCP 9844. The server setup guide covers the requirements and access policies.

For an existing NetBird network, enroll the server like this:

tako servers add ubuntu@my-server.netbird.cloud --vpn netbird --name prod
tako servers list --json

Replace the hostname and administrator with your actual values. Interactive setup offers installation or repair if needed. If you’re using Tailscale, select --vpn tailscale and its server hostname instead. The inventory name prod must match the name in tako.toml.

Point axum.example.com at the VPS’s public address. Use your own domain, and allow public HTTP 80 and HTTPS 443. For this direct-to-VPS setup, the default Let’s Encrypt mode uses HTTP-01 validation. A route declaration does not create public DNS records; the TLS guide explains the certificate choices.

Deploy from the project directory:

tako deploy --env production

The server builds the image, starts its default command, and checks the SDK’s internal status response before routing public requests to healthy replacement instances. The path looks like this:

Diagram

Check the actual deployment:

tako status --json
tako logs --env production --days 1 --json
curl -i https://axum.example.com/
curl -i https://axum.example.com/api/health

Confirm the certificate, greeting, and ok response. A healthy rollout checks startup; exercising application endpoints checks the behavior users receive. Repeat the public requests after the next deploy, especially when changing routes, dependencies, or bootstrap behavior.

Keep these container boundaries in mind as the app grows:

ConcernThis deployment
HTTP listenerSDK binds 0.0.0.0:3000 inside the container.
Public HTTPSTako terminates TLS and routes traffic to healthy instances.
Private valuesSDK reads container bootstrap from TAKO_BOOTSTRAP_DATA.
Persistent filesTako mounts the app data directory at /tako/data by default and exposes it through TAKO_DATA_DIR; instances on one server share it.

The deployment guide covers rolling updates, and Operations covers logs and release inspection. Start with these two routes, verify one real deploy, then bring the rest of your Axum application along with the same SDK serving call and image build.