Back to Blog
Tutorial6 min read2026-07-13

How to Deploy Rocket (Rust) with PostgreSQL on PandaStack

Ship a Rocket 0.5 app to production: release builds, the three config gotchas, sqlx migrations, a caching Dockerfile, and a managed Postgres wired in.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

Rocket 0.5 runs on stable Rust, compiles to a single static-ish binary, and produces some of the smallest production images you'll ever ship. But its defaults are built for local development — it binds to localhost, refuses to launch without a secret key in release mode, and its database config format trips up almost everyone the first time. Here's the full path from cargo new to a live URL with a managed PostgreSQL database behind it.

Building Rocket for production

There's no separate "build for prod" ceremony in Rust — you just compile in release mode:

cargo build --release

The binary lands at target/release/. It embeds everything except system libraries, so your runtime image needs almost nothing.

One Rocket-specific detail worth knowing: Rocket picks its configuration profile based on how it was compiled. A debug build reads the [debug] profile from Rocket.toml, a release build reads [release]. This matters because some behavior changes with the profile — most importantly, secret key enforcement, which we'll get to.

The three config gotchas

Rocket reads configuration from Rocket.toml and from environment variables prefixed with ROCKET_. Environment variables win, which is exactly what you want on a platform. Three settings bite people in production:

1. Address. Rocket binds to 127.0.0.1 by default. Inside a container, that means the platform's router can't reach your app and every request times out. Set:

ROCKET_ADDRESS=0.0.0.0

2. Port. Rocket defaults to port 8000. That's fine — just make sure the port your platform routes to matches. You can override with ROCKET_PORT if needed.

3. Secret key. If the secrets feature is enabled (it's on by default, and private cookies use it), Rocket refuses to launch in a non-debug profile without a configured secret key. Your app will start fine locally in debug mode and then die instantly in production. Generate one:

openssl rand -base64 32

and set it as ROCKET_SECRET_KEY in your environment variables. Never commit it to Rocket.toml.

A minimal Rocket.toml that leaves secrets to the environment:

[release]
address = "0.0.0.0"
port = 8000

Wiring PostgreSQL with rocket_db_pools

The official database story for Rocket 0.5 is the rocket_db_pools crate, which manages an async connection pool and hands connections to your handlers as request guards. With sqlx and Postgres:

[dependencies]
rocket = "0.5"
rocket_db_pools = { version = "0.2", features = ["sqlx_postgres"] }

Declare the pool and attach it:

#[macro_use] extern crate rocket;
use rocket_db_pools::{sqlx, Database, Connection};

#[derive(Database)]
#[database("main")]
struct Db(sqlx::PgPool);

#[get("/")]
async fn index(mut db: Connection<Db>) -> String {
    let row: (i64,) = sqlx::query_as("SELECT count(*) FROM users")
        .fetch_one(&mut **db).await.unwrap();
    format!("{} users", row.0)
}

Here's the part that trips people up. rocket_db_pools expects its connection URL under the key databases.main.url, and setting that via an environment variable means writing TOML inside the variable:

ROCKET_DATABASES={main={url="postgres://..."}}

That works, but it's fragile. Since most managed platforms hand you a plain DATABASE_URL, the cleaner approach is to merge it into Rocket's figment at startup:

#[launch]
fn rocket() -> _ {
    let db_url = std::env::var("DATABASE_URL")
        .expect("DATABASE_URL must be set");
    let figment = rocket::Config::figment()
        .merge(("databases.main.url", db_url));
    rocket::custom(figment)
        .attach(Db::init())
        .mount("/", routes![index])
}

Now your app reads the standard DATABASE_URL and no one has to remember the nested-TOML-in-an-env-var trick. The expect is deliberate: fail loudly at boot, not on the first query.

Migrations with sqlx

Install the CLI and create your first migration:

cargo install sqlx-cli --no-default-features --features postgres
sqlx migrate add create_users

Edit the generated file in migrations/, then apply it with sqlx migrate run (it reads DATABASE_URL). For production I prefer embedding migrations in the binary and running them before Rocket starts serving, via an ignite fairing:

use rocket::fairing::{self, AdHoc};
use rocket::{Build, Rocket};

async fn run_migrations(rocket: Rocket<Build>) -> fairing::Result {
    match Db::fetch(&rocket) {
        Some(db) => match sqlx::migrate!("./migrations").run(&**db).await {
            Ok(_) => Ok(rocket),
            Err(e) => { eprintln!("migration failed: {e}"); Err(rocket) }
        },
        None => Err(rocket),
    }
}

Attach it with .attach(AdHoc::try_on_ignite("migrations", run_migrations)). sqlx tracks applied migrations in a _sqlx_migrations table and takes a lock while running, so a rolling deploy won't double-apply them. Migrations run before the server binds, so a failed migration means the old version keeps serving — which is the behavior you want.

A Dockerfile that doesn't recompile the world

Rust's compile times are the real tax here. A naive Dockerfile recompiles every dependency on every code change. The classic fix is a dummy-main layer that caches dependency builds:

FROM rust:1-bookworm AS build
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs \
    && cargo build --release && rm -rf src
COPY . .
RUN touch src/main.rs && cargo build --release

FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates \
    && rm -rf /var/lib/apt/lists/*
COPY --from=build /app/target/release/myapp /usr/local/bin/myapp
ENV ROCKET_ADDRESS=0.0.0.0 ROCKET_PORT=8000
EXPOSE 8000
USER 1000
CMD ["myapp"]

Replace myapp with your crate name from Cargo.toml. The ca-certificates package matters if your app makes outbound TLS calls (including TLS to your database). The final image is a slim Debian base plus one binary — typically well under 100 MB.

The touch src/main.rs on the second build forces cargo to recompile your crate (the dummy main.rs from the caching layer has a newer mtime than your real sources in some setups). It's a one-line insurance policy against the maddening "my changes aren't in the image" bug.

Deploying on PandaStack

With the Dockerfile in the repo, the deploy itself is short:

  1. 1Create a managed PostgreSQL instance (14.x or 16.x) from the dashboard.
  2. 2Connect your Git repo as a container app. PandaStack finds the Dockerfile and builds it with rootless BuildKit in an ephemeral Kubernetes Job — no host Docker daemon involved.
  3. 3Attach the database to the app. DATABASE_URL is injected automatically, so the figment merge above picks it up with zero copy-pasting of credentials.
  4. 4Add ROCKET_SECRET_KEY in the app's environment variables.
  5. 5Push. The build streams live logs, and every subsequent git push redeploys.

One honest note on build minutes: Rust release builds are slow compared to interpreted languages, and the free tier includes 300 build pipeline minutes a month. The dependency-caching Dockerfile above is what keeps a routine push from eating ten of them. If you ship often, Pro ($15/mo) raises that to 1000 minutes.

On the free tier your app scales to zero when idle and cold-starts on the next request — for a compiled Rocket binary the process start itself is nearly instant, so the wake-up cost is dominated by scheduling, not your app.

Checklist before you call it done

  • ROCKET_ADDRESS=0.0.0.0 set (env var or Rocket.toml)
  • ROCKET_SECRET_KEY set, generated with openssl rand, not committed
  • DATABASE_URL merged into figment at boot, failing loudly if absent
  • Migrations embedded and running on ignite, before the server binds
  • Dockerfile caching dependencies in a separate layer

Rocket's production defaults are strict on purpose — a launch-time failure over a missing secret key is annoying exactly once, then it's protecting you. If you want to see the whole loop from push to live URL, it takes a few minutes to try on https://pandastack.io.

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also