Mastodon How to deploy Rust on Heroku (with Docker)

How to deploy Rust on Heroku (with Docker)

Due to its unrivaled reliability and performance, Rust is more and more appreciated by companies for web development. And when we talk about web development, Heroku is never far away.

So here is the easiest way to deploy a Rust app on Heroku (whether it be a worker or a webapp) πŸš€

Our application

We will deploy a simple web server using warp.

Cargo.toml

[package]
name = "myapp"
version = "0.1.0"
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
tokio = { version = "1", features = ["full"] }
warp = "0.3"

main.rs

use warp::Filter;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Heroku injects the port as an environment variable
    let port = std::env::var("PORT")
        .ok()
        .map(|val| val.parse::<u16>())
        .unwrap_or(Ok(8080))?;

    // GET /hello
    let hello = warp::path("hello").and(warp::get()).map(|| "World");

    // GET /world
    let world = warp::path("world").and(warp::get()).map(|| "Hello");

    // fallback
    let fallback = warp::any().map(|| "Not found");

    let routes = hello.or(world).or(fallback);

    println!("Starting server on port: {}", port);
    let (_addr, server) =
        warp::serve(routes).bind_with_graceful_shutdown(([0, 0, 0, 0], port), async {
            tokio::signal::ctrl_c()
                .await
                .expect("http_server: Failed to listen for CRTL+c");
            println!("Shutting down HTTP server");
        });

    server.await;

    Ok(())
}

Please note how simple it is to enable graceful shutdown :)

A small Docker image

The simplest way to deploy Rust on Heroku is to use Docker. Why? With Docker you fully control your CI pipeline (more on that later), and it can be deployed anywhere if Heroku is finally not your style.

As we saw previously, building minimal Docker images to deploy Rust brings up a lot of benefits: it’s not only good for security (reduced attack surface) but also to improve deployment times, reduce costs (less bandwidth and storage), and reduce the risk of dependency conflicts.

Here is our small, two-stages Dockerfile.

Dockerfile

####################################################################################################
## Builder
####################################################################################################
FROM rust:latest AS builder

RUN rustup target add x86_64-unknown-linux-musl
RUN apt update && apt install -y musl-tools musl-dev
RUN update-ca-certificates

# Create appuser
ENV USER=myapp
ENV UID=10001

RUN adduser \
    --disabled-password \
    --gecos "" \
    --home "/nonexistent" \
    --shell "/sbin/nologin" \
    --no-create-home \
    --uid "${UID}" \
    "${USER}"


WORKDIR /myapp

COPY ./ .

RUN cargo build --target x86_64-unknown-linux-musl --release

####################################################################################################
## Final image
####################################################################################################
FROM alpine:latest

# Import from builder.
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group

WORKDIR /myapp

# Copy our build
COPY --from=builder /myapp/target/x86_64-unknown-linux-musl/release/myapp ./

# Use an unprivileged user.
USER myapp:myapp

CMD ["/myapp/myapp"]

Why FROM alpine and not FROM scratch?

Because Heroku do weird things to inject the environment variables, so it’s easier to use an image with a shell and Docker’s CMD instruction that to hack around ENTRYPOINT.

If you try to use FROM scratch without an ENTRYPOINT, Heroku won’t be able to start your container because it will look for a shell that is not present.

CI/CD

And, last but not least, we need to set up a CI/CD pipeline to automagically test, build and deploy our app. CI pipelines are a commodity nowadays, so there is no need for Heroku’s integrated solution, which is, in my opinion, not the best one.

Here we use GitHub Actions, but it’s roughly the same with any Devops platform.

First, we need to set up the following secrets:

HEROKU_API_KEY=101010...
HEROKU_APP=example

.github/workflows/ci.yml

name: CI

on:
  push:
    branches:
      - main

jobs:

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - name: Update local toolchain
        run: |
          rustup update
          rustup component add clippy
          rustup install nightly          

      - name: Toolchain info
        run: |
          cargo --version --verbose
          rustc --version
          cargo clippy --version          

      - name: Test
        run: |
          cargo check
          cargo clippy -- -D warnings
          cargo test --all          


      - name: Build Docker image
        run: docker build -t registry.heroku.com/${{ secrets.HEROKU_APP }}/web:latest .

      - name: Docker image info
        run: docker images

      - name: Login to container registry
        env:
          HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }}
        run: heroku container:login

      - name: Push Docker image
        # see here for the details of the release phase https://devcenter.heroku.com/articles/container-registry-and-runtime#release-phase
        run: docker push registry.heroku.com/${{ secrets.HEROKU_APP }}/web

      - name: Release
        env:
          HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }}
        run: heroku container:release -a ${{ secrets.HEROKU_APP }} web

Conclusion

Deploying Rust apps on Heroku is easy thanks to Docker. If Heroku is too expensive or not flexible enough for your use case, switching to another platform such as Render or Digital Ocean App platform is just a few clicks away.

The code is on GitHub

As usual, you can find the code on GitHub: github.com/skerkour/kerkour.com (please don’t forget to star the repo πŸ™)

1 email / week to learn how to (ab)use technology for fun & profit: Programming, Hacking & Entrepreneurship.
I hate spam even more than you do. I'll never share your email, and you can unsubscribe at any time.

Tags: rust, programming, tutorial, devops

Want to learn Rust and offensive security? Take a look at my book Black Hat Rust. All early-access supporters get a special discount and awesome bonuses: https://academy.kerkour.com/black-hat-rust?coupon=BLOG.
Warning: this offer is limited in time!

Related posts