Building and Running a Rust Application with Docker on Mac (Apple Silicon)
Tags: rust, docker, webdev, devops
Published: 2024-03-13 15:43:23

This guide will walk you through the process of building and running a Rust application using Docker. The provided Dockerfile is designed to build a Rust application for the ARM64 architecture and run it in a lightweight Debian container.

## Dockerfile 
```
# syntax=docker/dockerfile:1

################################################################################
# Create a stage for building the application.

ARG RUST_VERSION=1.69.0
ARG APP_NAME=my_app_name
FROM --platform=linux/arm64 rust:${RUST_VERSION}-slim-bullseye AS build
ARG APP_NAME
WORKDIR /app
RUN apt-get update && apt-get upgrade -y
RUN apt-get install -y pkg-config openssl libssl-dev ca-certificates
RUN \
  apt-get install ca-certificates && \
  apt-get clean
# Build the application.
# Leverage a cache mount to /usr/local/cargo/registry/
# for downloaded dependencies and a cache mount to /app/target/ for 
# compiled dependencies which will speed up subsequent builds.
# Leverage a bind mount to the src directory to avoid having to copy the
# source code into the container. Once built, copy the executable to an
# output directory before the cache mounted /app/target is unmounted.
RUN --mount=type=bind,source=src,target=src \
    --mount=type=bind,source=Cargo.toml,target=Cargo.toml \
    --mount=type=bind,source=Cargo.lock,target=Cargo.lock \
    --mount=type=cache,target=/app/target/ \
    --mount=type=cache,target=/usr/local/cargo/registry/ \
    <<EOF
set -e
rustup target add aarch64-unknown-linux-gnu
cargo build --locked --release --target aarch64-unknown-linux-gnu
cp ./target/aarch64-unknown-linux-gnu/release/$APP_NAME /bin/server
EOF

################################################################################
# Create a new stage for running the application that contains the minimal
# runtime dependencies for the application. This often uses a different base
# image from the build stage where the necessary files are copied from the build
# stage.
#
# The example below uses the debian bullseye image as the foundation for running the app.
# By specifying the "bullseye-slim" tag, it will also use whatever happens to be the
# most recent version of that tag when you build your Dockerfile. If
# reproducibility is important, consider using a digest
# (e.g., debian@sha256:ac707220fbd7b67fc19b112cee8170b41a9e97f703f588b2cdbbcdcecdd8af57).
FROM --platform=linux/arm64 debian:bullseye-slim AS final

# Create a non-privileged user that the app will run under.
# See https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#user
ARG UID=10001
RUN adduser \
    --disabled-password \
    --gecos "" \
    --home "/nonexistent" \
    --shell "/sbin/nologin" \
    --no-create-home \
    --uid "${UID}" \
    appuser
USER appuser

# Copy the executable from the "build" stage.
COPY --from=build /bin/server /bin/

# Expose the port that the application listens on.
EXPOSE 1000

# What the container should run when it is started.
CMD ["/bin/server"]
```


### Understanding the Dockerfile
The Dockerfile consists of two stages: a build stage and a final stage.

### Build Stage
The build stage is responsible for compiling the Rust application. It uses the official Rust Docker image as a base and installs the necessary dependencies. The Dockerfile then leverages several Docker features to optimize the build process:

1. **Bind Mounts**: The source code is mounted into the container using a bind mount, avoiding the need to copy the code into the container for each build.

2. **Cache Mounts**: The Cargo registry and compiled dependencies are cached using cache mounts, speeding up subsequent builds by reusing downloaded and compiled artifacts.

The build stage compiles the Rust application using `cargo build --release` and copies the resulting binary to `/bin/server`.

### Final Stage
The final stage creates a minimal runtime environment for the application using the `debian:bullseye-slim` image as a base. It performs the following steps:

1. **Create a Non-Privileged User**: A non-privileged user named appuser is created to run the application, following best practices for container security.

2. **Copy the Binary**: The compiled binary from the build stage is copied into the final image at /bin/server.

3. **Expose Port:** The container exposes port 1000, which is the port the application listens on.

4. **Default Command**: The container is configured to run the /bin/server binary when started.


### Building the Docker Image
To build the Docker image, run the following command in the directory containing the Dockerfile:
`docker build --platform linux/arm64 -t my_app_name . `

### Running the Docker Container
To run the Docker container, use the following command:

`docker run -d --name my_app_name -p 1000:1000 --restart on-failure --memory 1g --memory-swap 1g my_app_name`