Skip to content

Production-Grade Rust Deployments

It is standard practice in software development to use some sort of continuous deployment when issuing content to customers or clients (e.g. production).

There are development environment and there are production environments, and they serve different purposes. The purpose of a producetion environment is to run our software and make it available to users.

The reason this is required is because despite our best efforts, our development environment will always make assumptions about the functionality provided by the operating system (Linux vs. macOS), setup, and other software packages installed, among others. By building and running code in a production environment, restrictions are placed on the assumptions made about the underlying system, providing control over the program distributed to users.

While you can use virtual machines, virtual contianers (e.g. Docker) are a good option for small, agile teams.

With virtualization, most software hosting platforms (e.g. AWS, Heroku, Google Cloud, Digital Ocean), allow you to specify the production environment using Dockerfiles. These are recipes for spinning up a running instance of an environment (a container) for production.

Rust Dockerfile

To start a Dockerfile you need to declare an image, which is often an specific version of an operating system, sometimes with a language toolchain pre-configured (in this case, Rust).

The simplest working Dockerfile for a Rust project is shown below, this file should be saved in the top-level of your project with the name Dockerfile.

dockerfile
# We use the latest Rust stable release as base image
FROM rust:1.59.0

# Let's switch our working directory to `app` (equivalent to `cd app`)
# The `app` folder will be created for us by Docker in case it does not
# exist already.
WORKDIR /app
# Install the required system dependencies for our linking configuration
RUN apt update && apt install lld clang -y
# Copy all files from our working environment to our Docker image
COPY . .
# Let's build our binary!
# We'll use the release profile to make it faaaast
RUN cargo build --release
# When `docker run` is executed, launch the binary!
ENTRYPOINT ["./target/release/name_of_your_project"]
# We use the latest Rust stable release as base image
FROM rust:1.59.0

# Let's switch our working directory to `app` (equivalent to `cd app`)
# The `app` folder will be created for us by Docker in case it does not
# exist already.
WORKDIR /app
# Install the required system dependencies for our linking configuration
RUN apt update && apt install lld clang -y
# Copy all files from our working environment to our Docker image
COPY . .
# Let's build our binary!
# We'll use the release profile to make it faaaast
RUN cargo build --release
# When `docker run` is executed, launch the binary!
ENTRYPOINT ["./target/release/name_of_your_project"]

You can build the Docker image by running

sh
docker build --tag name_of_your_project --file Dockerfile .
docker build --tag name_of_your_project --file Dockerfile .

Note that the . at the end of the above command specifies the build context; that is, the files within which the Docker image has access to using commands like COPY or ADD. The build context can be specified as directories, URLs, and others.

Running an Image

If you give an image a tag, you can run it with

sh
docker run name_of_your_project
docker run name_of_your_project

This will execute the command(s) provided in the ENTRYPOINT line of your Dockerfile.

Networking with Docker

By default, a Docker image won't expose the ports of the nuderlying host machine. You can expose ports when running a docker image with the -p flag

sh
docker run -p 8080:80 name_of_your_project
docker run -p 8080:80 name_of_your_project

This will expose port 8080 on the host machine as port 80 in the container.

Optimizing the Docker Image

Optimizations:

  • Smallest possible image size
  • Caching for faster builds

Find the size of your image with

sh
docker images name_of_your_project
docker images name_of_your_project

and compare that to the base image you're using

sh
docker images rust:1.59.0
docker images rust:1.59.0
  1. Ignore files not required to build the docker image by adding them to .dockerignore. This file is populated in the same way as a .gitignore. The target folder is especially important to ignore as the build artifacts are typically quite large.
  2. Since Rust is statically linked, we don't have to keep the source code to run the executable. This allows us to break the Docker image build process into two steps: a builder stage and a runtime stage. This is demonstrated in the adapted Dockerfile below
dockerfile
# Builder stage
FROM rust:1.59.0 AS builder

WORKDIR /app
RUN apt update && apt install lld clang -y
COPY . .
RUN cargo build --release

# Runtime stage
FROM rust:1.59.0 AS runtime

WORKDIR /app
# Copy the compiled binary from the builder environment
# to our runtime environment
COPY --from=builder /app/target/release/your_project your_project
# We need the configuration file at runtime
COPY configuration configuration
ENV APP_ENVIRONMENT production
ENTRYPOINT ["./your_project"]
# Builder stage
FROM rust:1.59.0 AS builder

WORKDIR /app
RUN apt update && apt install lld clang -y
COPY . .
RUN cargo build --release

# Runtime stage
FROM rust:1.59.0 AS runtime

WORKDIR /app
# Copy the compiled binary from the builder environment
# to our runtime environment
COPY --from=builder /app/target/release/your_project your_project
# We need the configuration file at runtime
COPY configuration configuration
ENV APP_ENVIRONMENT production
ENTRYPOINT ["./your_project"]
  • It is often also possible to reduce image size by using a -slim version of the base image (in the case rust:1.59.0-slim)
  • Furthermore, if your breakdown your Rust project as above, then the builder stage is discarded once it's complete and thus doesn't contribute to the size of the resulting image. Since Rust packages are statically linked and everything required is contianed wthin the binary, the final image doesn't have to even be equipped with the Rust toolchain. Instead an image like debian:11-slim can be used to even further reduce the final image size.

Stopped at 3.8.2 Caching For Rust Docker Builds