Category: Rust

  • [Troubleshooting] Error: could not find system library ‘openssl’ required by the ‘openssl-sys’ crate

    When using reqwest crate and building docker images with Dockerfile mentioned in this article, you will encounter openssl issues due to missing packages.

    Error: could not find system library 'openssl' required by the 'openssl-sys' crate

    It turns out that below line is the culprit:

    FROM docker.io/rust:1-slim-bookworm AS build
    
    #############################################

    rust:1-slim-bookworm misses lots of necessary package to run the application. It is suggested in official Rust docker image document that we should avoid using slim image.

    Simple but ugly solution

    Well, the simplest approach is using a minimal Dockerfile which will always work:

    FROM rust:bookworm
    
    COPY . .
    
    RUN cargo build --release
    
    EXPOSE 8080
    
    CMD ./target/release/your_package

    However, it will generate a pretty large image which can take gigabytes of storage.

    The better one

    We can still use multi stage build to generate an optimal image with a little tweak (using default rust image version rust:bookworm):

    FROM docker.io/rust:bookworm AS build
    
    ## cargo package name: customize here or provide via --build-arg
    ARG pkg=hello-world
    
    WORKDIR /build
    
    COPY . .
    
    RUN --mount=type=cache,target=/build/target \
        --mount=type=cache,target=/usr/local/cargo/registry \
        --mount=type=cache,target=/usr/local/cargo/git \
        set -eux; \
        cargo build --release; \
        objcopy --compress-debug-sections target/release/$pkg ./main
    
    ################################################################################
    
    FROM docker.io/debian:bookworm-slim
    
    WORKDIR /app
    
    ## copy the main binary
    ## add more files below if needed
    COPY --from=build /build/main ./
    
    EXPOSE 8080
    
    CMD ./main

    Now it builds fine, but you will get a runtime error when running reqwest:

    ./main: error while loading shared libraries: libssl.so.3: cannot open shared object file: No such file or directory

    Furthermore, after installing correct packages either vendored or automatic, you will still see error when sending https requests:

    hyper_util::client::legacy::Error(Connect, Ssl(Error { code: ErrorCode(1), cause: Some(Ssl(ErrorStack([Error { code: 167772294, library: "SSL routines", function: "tls_post_process_server_certificate", reason: "certificate verify failed", file: "ssl/statem/statem_clnt.c", line: 2092 }]))) }, X509VerifyResult { code: 20, error: "unable to get local issuer certificate" }))

    To solve these issues, I separately wrote an article for detailed explanation and solutions.

  • [Troubleshooting] libssl.so.3 and certificate Error Running Rust/Reqwest Under Debian Image

    If you see below errors while running Rust app using Docker, this article is right for you:

    error while loading shared libraries: libssl.so.3: cannot open shared object file: No such file or directory
    hyper_util::client::legacy::Error(Connect, Ssl(Error { code: ErrorCode(1), cause: Some(Ssl(ErrorStack([Error { code: 167772294, library: "SSL routines", function: "tls_post_process_server_certificate", reason: "certificate verify failed", file: "ssl/statem/statem_clnt.c", line: 2092 }]))) }, X509VerifyResult { code: 20, error: "unable to get local issuer certificate" }))

    Anatomy

    Reqwest document mentioned about requirements when running under Linux OS, where you need OpenSSL installed.

    To supply OpenSSL, Rust official document provided 2 ways:

    Vendored OpenSSL

    Add below dependency to Cargo.toml.

    [dependencies]
    openssl = { version = "0.10", features = ["vendored"] }

    Automatic OpenSSL

    Add below line to Dockerfile at stage of running app.

    RUN apt-get update && apt-get install -y pkg-config libssl-dev

    Either approach works fine and you can run the app successfully. However, when sending https requests, you will get below error:

    unable to get local issuer certificate

    A Debian image issue

    This thread gives a good idea of what happens here. Essentially, the debian official image does not have ca certification package installed. To solve the issue, simply install the package in Dockerfile:

    RUN apt-get update && apt-get install -y pkg-config libssl-dev && apt install -y ca-certificates
    # or below if using vendored OpenSSL
    # RUN apt-get update && apt install -y ca-certificates

    Final Dockerfile

    To build rust + actix-web + reqwest, below is what works for me:

    [package]
    name = "hello-world"
    version = "0.1.0"
    edition = "2021"
    
    [dependencies]
    actix-web = "4"
    reqwest = "0.12"
    serde = { version = "1.0", features = ["derive"] }
    openssl = { version = "0.10", features = ["vendored"] }
    FROM rust:bookworm AS build
    
    ## cargo package name: customize here or provide via --build-arg
    ARG pkg=hello-world
    
    WORKDIR /build
    
    COPY . .
    
    RUN --mount=type=cache,target=/build/target \
        --mount=type=cache,target=/usr/local/cargo/registry \
        --mount=type=cache,target=/usr/local/cargo/git \
        set -eux; \
        cargo build --release; \
        objcopy --compress-debug-sections target/release/$pkg ./main
    
    ################################################################################
    
    FROM docker.io/debian:bookworm-slim
    
    WORKDIR /app
    
    ## copy the main binary
    COPY --from=build /build/main ./
    
    EXPOSE 8080
    
    RUN apt-get update && apt install -y ca-certificates
    
    CMD ./main
  • Containerize Actix Web App Using Docker In A Cleaner Way

    Recently came across this task to try different Rust web frameworks and Actix Web is one of the most popular frameworks. Its documentation is clean and easy to bootstrap, however, when I was trying to deploy the app using Docker, I could not find a clean Dockerfile example that contains minimal dependencies.

    Luckily, when I was trying Rocket, it has a pretty clean documentation about containerize the app. After tweaking it a bit, it just worked for Actix Web app as well.

    Personally, I feel more confident to follow official documents for long term maintainability. Even tho the image size coming from the build is not as small as ~60MB like in this guide, but ~145MB image is also acceptable here consider other factors.

    Initiate Actix Web Sample App

    Create a new Rust app:

    cargo new hello-world
    cd hello-world

    Now add Actix Web dependency into Cargo.toml file:

    [dependencies]
    actix-web = "4"

    Then replace the contents of src/main.rs with the following:

    use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder};
    
    #[get("/")]
    async fn hello() -> impl Responder {
        HttpResponse::Ok().body("Hello world!")
    }
    
    #[post("/echo")]
    async fn echo(req_body: String) -> impl Responder {
        HttpResponse::Ok().body(req_body)
    }
    
    async fn manual_hello() -> impl Responder {
        HttpResponse::Ok().body("Hey there!")
    }
    
    #[actix_web::main]
    async fn main() -> std::io::Result<()> {
        HttpServer::new(|| {
            App::new()
                .service(hello)
                .service(echo)
                .route("/hey", web::get().to(manual_hello))
        })
        .bind(("0.0.0.0", 8080))?
        .run()
        .await
    }

    Compile and run the program:

    cargo run

    Test app using http://0.0.0.0:8080

    Add Dockerfile and build Docker image

    The original Dockerfile from Rocket document has some Rocket specific settings, simply removing those env variables would just work:

    FROM docker.io/rust:1-slim-bookworm AS build
    
    ## cargo package name: customize here or provide via --build-arg
    ARG pkg=hello-world
    
    WORKDIR /build
    
    COPY . .
    
    RUN --mount=type=cache,target=/build/target \
        --mount=type=cache,target=/usr/local/cargo/registry \
        --mount=type=cache,target=/usr/local/cargo/git \
        set -eux; \
        cargo build --release; \
        objcopy --compress-debug-sections target/release/$pkg ./main
    
    ################################################################################
    
    FROM docker.io/debian:bookworm-slim
    
    WORKDIR /app
    
    ## copy the main binary
    ## add more files below if needed
    COPY --from=build /build/main ./
    
    EXPOSE 8080
    
    CMD ./main

    Note that, pkg ARG need to be the same as package name in Cargo.toml.

    Now let’s build the image:

    docker build -t hello-world-image .

    Check the image size (~145MB):

    Finally, run the image:

    docker run -d -p 0.0.0.0:8080:8080 hello-world-image

    Test the app using http://0.0.0.0:8080

  • Develop and Deploy Rust Rocket App Using Docker Compose (Traefik/Docker Hub)

    This article will talk about how to use Docker for quick Rocket web app deployment. This architecture can be scaled with more complicated CI/CD framework and Kubernetes clusters for large scale application and zero downtime deployment.

    Prerequisite

    • Linux server
    • Local dev machine with Rust installed
    • Docker with Docker Compose
    • Follow this guide for setting up Traefik.
    • Domain name

    Getting started

    Update rust

    Install rustup by following the instructions on its website. Once rustup is installed, ensure the latest toolchain is installed by running the command:

    rustup default stable

    Initiate Rocket sample app

    cargo new hello-rocket --bin
    cd hello-rocket

    Now, add Rocket as a dependency in your Cargo.toml:

    [dependencies]
    rocket = "0.5.1"

    Modify src/main.rs so that it contains the code for the Rocket Hello, world! program, reproduced below:

    #[macro_use] extern crate rocket;
    
    #[get("/")]
    fn index() -> &'static str {
        "Hello, world!"
    }
    
    #[launch]
    fn rocket() -> _ {
        rocket::build().mount("/", routes![index])
    }

    Finally, we can run below command to test our first app:

    cargo run

    Build docker image and upload

    Add Dockerfile

    There are many tutorials out there but it is always a good practice to follow official documents: https://rocket.rs/guide/v0.5/deploying/#containerization

    Note that, in order to test Docker image in local, EXPOSE is needed:

    FROM docker.io/rust:1-slim-bookworm AS build
    
    ## cargo package name: customize here or provide via --build-arg
    ARG pkg=hello-rocket
    
    WORKDIR /build
    
    COPY . .
    
    RUN --mount=type=cache,target=/build/target \
        --mount=type=cache,target=/usr/local/cargo/registry \
        --mount=type=cache,target=/usr/local/cargo/git \
        set -eux; \
        cargo build --release; \
        objcopy --compress-debug-sections target/release/$pkg ./main
    
    ################################################################################
    
    FROM docker.io/debian:bookworm-slim
    
    WORKDIR /app
    
    ## copy the main binary
    COPY --from=build /build/main ./
    
    ## copy runtime assets which may or may not exist
    COPY --from=build /build/Rocket.tom[l] ./static
    COPY --from=build /build/stati[c] ./static
    COPY --from=build /build/template[s] ./templates
    
    ## ensure the container listens globally on port 8000
    ENV ROCKET_ADDRESS=0.0.0.0
    ENV ROCKET_PORT=8000
    
    ## uncomment below to test in local
    ## EXPOSE 8000
    
    CMD ./main

    Make sure the pkg set to same value as package name in Cargo.toml

    Build Docker image

    docker build -t your_username/my-rocket-image .

    Upload Docker image to Docker Hub

    First make sure you have Docker Hub account and then login to Docker:

    Docker login

    Then upload Docker image to Docker Hub:

    docker push your_username/my-rocket-image

    Deploy docker image to cloud instance with Docker Compose and Traefik

    Assume you already followed this guide and Traefik reverse proxy is up running in your server.

    Run uploaded Rocket app Docker image

    First let’s add a folder:

    mkdir ~/rocket-docker && cd ~/rocket-docker

    Then add docker-compose.yml:

    vi docker-compose.yml
    networks:
      traefik:
        external: true
     
    services:
      app:
        image: your_username/my-rocket-image:latest
        networks:
          - traefik
        labels:
          - "traefik.enable=true"
          - "traefik.http.routers.rocket-docker.rule=Host(`your-domain.com`)"
          - "traefik.http.routers.rocket-docker.service=rocket-docker"
          - "traefik.http.routers.rocket-docker.entrypoints=websecure"
          - "traefik.http.services.rocket-docker.loadbalancer.server.port=8000"
          - "traefik.http.services.rocket-docker.loadbalancer.passhostheader=true"
          - "traefik.http.routers.rocket-docker.tls=true"
          - "traefik.http.routers.rocket-docker.tls.certresolver=letsencrypt"
          - "traefik.http.routers.rocket-docker.middlewares=compresstraefik"
          - "traefik.http.middlewares.compresstraefik.compress=true"
          - "traefik.docker.network=traefik"
        restart: unless-stopped

    Make sure to update your-domain.com

    Run below to start service:

    docker compose up -d

    Verify by visiting www.your-domain.com see if everything works properly.

    Develop and update service with latest change

    Try change something in your local Next.js app, and run upload script again:

    docker build -t your_username/my-rocket-image . && docker push your_username/my-rocket-image

    Then go to your cloud instance and run below:

    docker pull your_username/my-rocket-image:latest && docker compose -f ~/rocket-docker/docker-compose.yml up -d

    Lastly, verify the change by visiting www.your-domain.com.