Docs menu

Rust on Primcloud

How Primcloud detects a Rust app, resolves the toolchain, compiles binaries with Cargo, caches between builds, builds a frontend alongside, and infers process types.

On this page

Primcloud detects a Rust app from Cargo.toml, installs the Rust toolchain your repo asks for, compiles every binary in the package or workspace with a release build, caches the toolchain, dependencies, and compiled artifacts between builds, and, if you don't ship a Procfile, generates process types from the compiled binaries. This page walks each phase and the config knobs that shape it.

Detection#

Primcloud runs the Rust build when the root of your repo contains a Cargo.toml file. That's the only trigger.

A package or workspace with no binary target (a library-only crate, say) still builds as a Rust app, and the build fails with a "no binary target" error.

One language per app. Primcloud checks languages in a fixed order and builds with the first that matches. Rust is checked ahead of Bun and Node.js, so a repo with a Cargo.toml and a package.json is a Rust app with a frontend; see Frontend assets. That includes a JavaScript project that keeps a native addon's Cargo.toml at its root: move the crate into a subdirectory to keep it a JavaScript app. Every other language is checked ahead of Rust: a repo that also carries a go.mod, a composer.json, a requirements.txt, or another language's manifest at the root builds as that language.

Install phase#

The build reads the toolchain channel from rust-toolchain or rust-toolchain.toml and installs rustc, cargo, and the standard library for it. With neither file, you get the current stable release. See Runtime versions for the accepted values.

Every toolchain download is verified against its published checksum before it's unpacked.

Compile phase#

The build runs a single command:

Terminal
cargo build --release --workspace --bins

--locked is added when your repo has a Cargo.lock, so the build fails rather than resolve different dependency versions from the ones you committed. Cargo downloads your dependencies as part of this step.

--workspace --bins compiles every binary target of every workspace member. A single package is its own workspace, so the same command covers both shapes, and a virtual workspace needs no root package.

Each compiled binary is installed on PATH under its target name. A binary whose required-features aren't enabled isn't built by Cargo; the build log lists it as skipped and it gets no process. If no binary is built at all, the build fails.

Your Cargo configuration is honored: [profile.release] settings in Cargo.toml, a .cargo/config.toml in the repo, and build.rs scripts all behave as they do locally. Variables you set in the Secrets panel are visible to Cargo during the build.

Cache behavior#

Primcloud persists a per-app build cache between deploys. Three things are reused across builds:

  • The Rust toolchain: reused as long as the resolved release matches the previous build.
  • Downloaded dependencies: Cargo's registry index, crate downloads, and git dependencies. Refreshed periodically so the cache stays bounded.
  • Compiled artifacts: reused as long as the Rust version and base image haven't changed, and refreshed periodically.

Only what changed is recompiled. A crate in your repo with a changed file is rebuilt, along with everything that depends on it. A workspace member or path dependency with no changed files is not, and neither are registry and git dependencies that are already compiled.

Frontend assets#

A Rust app with a package.json at the root gets its frontend built before cargo build runs. The lockfile you commit picks the tool:

Lockfile Frontend build
package-lock.json, yarn.lock, or pnpm-lock.yaml Node.js is installed, dependencies are installed with npm, Yarn, or pnpm, and your package.json build scripts run. See Node.js package managers.
bun.lock or bun.lockb Bun is installed, dependencies are installed with bun install --frozen-lockfile, and your package.json build scripts run with Bun. See Bun package managers.

The build scripts are heroku-prebuild, then heroku-build or else build, then heroku-postbuild, whichever of them package.json defines.

Files the frontend build writes into your repo tree (a dist/ directory, say) ship with the app beside your source. Your binary starts in the app root, so it can serve them by relative path. Because the frontend is built first, it's also on disk when Cargo runs, for crates that embed assets at compile time.

Without a Procfile, the Rust binary is the web process. A start script in package.json doesn't take over.

Commit the lockfile for the tool you use. A package.json with no npm, Yarn, or pnpm lockfile, and nothing that marks it as a Bun project, gets no dependencies installed and no build scripts run.

Release phase conventions#

Procfile takes precedence. If your repo has a Procfile at the root, Primcloud uses it verbatim to define process types, the same as every other language on Primcloud. When a Procfile is present, the build leaves process definitions entirely to your file. See Pod Formations for the format.

Auto-detected process types kick in when there's no Procfile. One process type is generated per compiled binary:

  • Each process is named after its binary.
  • The command for each process is the binary name: the process type doubles as the executable, since the compiled binaries are on PATH.
  • One process is the default web. If any binary is named web, that one wins. Otherwise the first binary Cargo lists is duplicated as a second entry named web, so the formation always has a web process to start.

Only web and worker are real pod types. web is the internet-facing process (one replica by default, never auto-removed); every other process name becomes a worker formation, created at zero replicas until you scale it up. See Pod Formations.

Port binding. Primcloud sets PORT on the web process (the constant 5000), and your web server must bind to it on 0.0.0.0. The build does not rewrite your launch command to inject the port, so your code has to read it:

let port = std::env::var("PORT").expect("$PORT must be set");
let listener = std::net::TcpListener::bind(format!("0.0.0.0:{port}")).expect("bind to $PORT");

Non-web formations get no PORT; a worker that doesn't serve HTTP doesn't need one. Any port other than 5000 is unreachable from outside the pod.

Runtime environment. No Rust-specific environment variables are set at runtime. Only the platform-provided ones (PORT, plus anything you configure in the Secrets panel) reach the running process. The Rust toolchain, Cargo's caches, and the build's intermediate artifacts aren't in the running app; your compiled binaries and your repo's files are.