Dockerfile with axum returns 404

thebetterjort's edit

Edited 13 hours ago

  • [๐•] I have looked for existing issues (including closed) about this

Bug Report

I'm not able to access axum server from inside of a docker container.

Version

โ”œโ”€โ”€ axum v0.7.4
โ”‚ โ”œโ”€โ”€ axum-core v0.4.3

Platform

Linux f44bf27f15bb 6.6.12-linuxkit #1 SMP Fri Jan 19 08:53:17 UTC 2024 aarch64 GNU/Linux
or M2 Pro
uname -r
23.3.0

Crates

[dependencies]
axum = {version = "0.7.2", features = ["tracing"]}
dotenv = "0.15.0"
tokio = { version = "1.27.0", features = ["full"] }
tower-http = { version = "0.5.0", features = ["cors", "fs"] }
common = {path = "../common"}
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
askama = "0.12.1"
rand = "0.8.5"

[dev-dependencies]
anyhow = "1.0.48"
httpc-test = "0.1.1"

Description

main.rs

mod route;

use tracing::info;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

use axum::http::{
    header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE},
    HeaderValue, Method,
};
// use dotenv::dotenv;
use tower_http::cors::CorsLayer;

#[tokio::main]
async fn main() {
    // Load environment variables from a .env file.
    // dotenv().ok();

    let num = 10;
    println!(
        "Common lib loaded: {num} plus one is {}!",
        common::add_one(num)
    );

    tracing_subscriber::registry()
        .with(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| "web=debug".into()),
        )
        .with(tracing_subscriber::fmt::layer())
        .init();

    info!("hello, web server!");

    // Create a CORS layer to handle Cross-Origin Resource Sharing.
    let cors = CorsLayer::new()
        .allow_origin("http://0.0.0.0:3000".parse::<HeaderValue>().unwrap())
        .allow_methods([Method::GET, Method::POST, Method::PATCH, Method::DELETE])
        .allow_credentials(true)
        .allow_headers([AUTHORIZATION, ACCEPT, CONTENT_TYPE]);
    // Create the application router and attach the CORS layer.
    let app = route::create_index().layer(cors);

    // Bind the server to listen on the specified address and port.
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    println!("->> LISTENING on {:?}\n", listener.local_addr());
    // Start serving the application.
    axum::serve(listener, app).await.unwrap();
}

route.rs

use axum::Router;

use tower_http::services::ServeFile;

pub fn create_index() -> Router {
    Router::new().route_service("/", ServeFile::new("dist/index.html"))
}

Dockerfile

ARG PACKAGE=web
FROM rust:1.71.1 as builder
WORKDIR /app

COPY Cargo.toml .
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo remove common -v
RUN cargo build --release --bin web

COPY src src
RUN touch src/main.rs

RUN cargo add --git https://github.com/federal-courts-software-factory/open-case-filing-system.git common
RUN cargo build --release --bin web

RUN strip target/release/web

FROM gcr.io/distroless/cc-debian12 as release
WORKDIR /app
COPY --from=builder /app/target/release/web .


EXPOSE 3000

CMD ["./web"]

Docker cmd
docker run -p 0.0.0.0:3000:3000 ghcr.io/federal-courts-software-factory/open-case-filing-system/web@sha256:f9aca3996cf60553d26907d3e987ba0fd654011e1b18f3e99078045e99b13215

Docker output:
2024-02-08T05:52:32.006717Z INFO web: hello, web server! ->> LISTENING on Ok(0.0.0.0:3000)

Cargo run works as expected with the index.html being served as expected.

One last thing I have noticed is that the docker container cannot be killed, it hangs.

Using podman build -t docker-axum . and podman run --rm -p 3000:3000 docker-axum works for me, and the server responds with a 404 because I don't have that HTML file. You may want to log each request for easier debuggability.

$ curl localhost:3000 -v      
* Host localhost:3000 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:3000...
* Connected to localhost (::1) port 3000
> GET / HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/8.6.0
> Accept: */*
> 
< HTTP/1.1 404 Not Found
< access-control-allow-origin: http://0.0.0.0:3000
< access-control-allow-credentials: true
< vary: origin, access-control-request-method, access-control-request-headers
< content-length: 0
< date: Thu, 08 Feb 2024 20:36:34 GMT
< 
* Connection #0 to host localhost left intact

I cannot help you much with program termination, but these two links may be helpful.

https://matze.github.io/axum-notes/notes/basic/index.html

Can you replicate it with the following repo:

git clone https://github.com/federal-courts-software-factory/open-case-filing-system.git
cd web
podman build -t docker-axum -f Dockerfile.local

Behaves exactly the same. How are you testing the connection and what kind of error do you get?

Thank you!

I'm getting a 404. I've built the docker image in github actions, and using kaniko. I'm not able to access the index.html.

Just to be clear, you are still seeing a 404 error correct?

I've also deployed in GCP using cloud run the docker image and still unable to access index.html.

Wouldn't you have to COPY dist/index.html ./dist/index.html in your release image? Otherwise web won't have the file to read, it'll be thrown away in the builder.

Yes, the image only contains one file (usr/local/bin/app). You'd need to COPY the /dist folder into the image.

Still get a 404 error

ARG PACKAGE=web

FROM rust:1.71.0-slim as builder

ARG PACKAGE

WORKDIR /app
#RUN apt update && apt install lld clang -y
COPY . .
# cargo vendor and add the toml configuration to config.offline
# We can build if crates.io goes offline.
RUN cargo remove common -v
RUN cargo add --git https://github.com/federal-courts-software-factory/open-case-filing-system.git common

RUN cargo build --release --bin $PACKAGE

FROM debian:bullseye-slim as runtime

ARG PACKAGE
ENV APP=$PACKAGE

WORKDIR /app
# RUN apt-get update -y \
#     && apt-get install -y --no-install-recommends openssl ca-certificates \
#     # Clean up
#     && apt-get autoremove -y \
#     && apt-get clean -y \
#     && rm -rf /var/lib/apt/lists/*
COPY dist/index.html ./dist/index.html
COPY --from=builder /app/target/release/$PACKAGE $PACKAGE


ENTRYPOINT ["./web"]

Unfortunately I spoke too soon, this doesn't solve it. Looks like I had a cargo run instance in the background.

Previous dockerfile didn't work. This one does.

FROM lukemathwalker/cargo-chef:latest-rust-1.70.0 as chef
WORKDIR /app

FROM chef AS planner
COPY .  .
RUN cargo chef prepare --recipe-path recipe.json

FROM chef AS builder
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json
COPY . .
RUN cargo build --release

FROM gcr.io/distroless/cc-debian11
COPY --from=builder /app/target/release/web /
COPY --from=builder /app/web/dist /dist
CMD ["./web"]