Docker Image not being build with Bollard

I'm using the [Bollard]Bollard crate in Rust to build a Docker image from a custom Dockerfile and then create a container from that image. when I try to create the image, I get the following error:
Error during image build: Docker responded with status code 500: Cannot locate specified Dockerfile: Dockerfile
I've checked that:

  • The image name and tag are consistent (python_executor:latest) in both the build and container creation steps.
  • The Dockerfile is added to the tar archive with the correct name.
  • The [BuildImageOptions] uses the correct dockerfile field .

Despite this, the image is not being created

use bollard::Docker;
use bollard::container::{Config, CreateContainerOptions, StartContainerOptions};
use bollard::exec::{CreateExecOptions, StartExecResults};
use bollard::image::BuildImageOptions;
use bollard::models::{HostConfig, PortBinding};
use futures_util::stream::StreamExt;
use std::error::Error;
use std::fs::File;
use std::path::Path;
use tar::Builder;
use tokio::io::AsyncReadExt;

pub async fn handle_request(language: &str, code: &str) -> Result<String, Box<dyn Error>> {
    let docker = Docker::connect_with_local_defaults()?;

    // Select the appropriate Dockerfile
    let dockerfile_path = match language {
        "python" => "./docker/Dockerfile.python",
        "javascript" => "./docker/Dockerfile.javascript",
        "java" => "./docker/Dockerfile.java",
        _ => return Err(format!("Unsupported language: {}", language).into()),
    };

    // Build and run the container
    let container_name = build_and_run_container(&docker, dockerfile_path, language).await?;

    // Execute the code inside the container
    let result = execute_code_in_container(&docker, &container_name, code).await?;

    Ok(result)
}

pub async fn build_and_run_container(
    docker: &Docker,
    dockerfile_path: &str,
    language: &str,
) -> Result<String, Box<dyn Error>> {
    let image_name = format!("{}_executor:latest", language);
    // Create tar archive for build context
    let tar_path = "./docker/context.tar";
    let dockerfile_name = create_tar_archive(dockerfile_path, tar_path)?; // This should be a sync function that writes a tarball
    println!("Using dockerfile_name: '{}'", dockerfile_name);
    // Use a sync File, not tokio::fs::File, because bollard expects a blocking Read stream
    let mut file = tokio::fs::File::open(tar_path).await?;

    let mut contents = Vec::new();
    file.read(&mut contents).await?;
    // Build image options
    let build_options = BuildImageOptions {
        dockerfile: dockerfile_name,
        t: image_name.clone(),
        rm: true,
        ..Default::default()
    };

    // Start the image build stream
    let mut build_stream = docker.build_image(build_options, None, Some(contents.into()));

    // Print docker build output logs
    while let Some(build_output) = build_stream.next().await {
        match build_output {
            Ok(output) => {
                if let Some(stream) = output.stream {
                    print!("{}", stream);
                }
            }
            Err(e) => {
                eprintln!("Error during image build: {}", e);
                return Err(Box::new(e));
            }
        }
    }

    println!("Docker image '{}' built successfully!", image_name);

    // Create container config
    let container_name = format!("{}_executor_container", language);
    let config = Config {
        image: Some(image_name),
        host_config: Some(HostConfig {
            port_bindings: Some(
                [(
                    "5000/tcp".to_string(),
                    Some(vec![PortBinding {
                        host_ip: Some("0.0.0.0".to_string()),
                        host_port: Some("5000".to_string()),
                    }]),
                )]
                .iter()
                .cloned()
                .collect(),
            ),
            ..Default::default()
        }),
        ..Default::default()
    };
    // Create container
    docker
        .create_container(
            Some(CreateContainerOptions {
                name: &container_name,
                platform: None,
            }),
            config,
        )
        .await?;
    println!("Container '{}' created successfully.", container_name);

    // Start container
    docker
        .start_container(&container_name, None::<StartContainerOptions<String>>)
        .await?;
    println!("Container '{}' started successfully!", container_name);

    Ok(container_name)
}

async fn execute_code_in_container(
    docker: &Docker,
    container_name: &str,
    code: &str,
) -> Result<String, Box<dyn Error>> {
    let shell_command = format!("echo '{}' > script.py && python script.py", code);
    let exec_options = CreateExecOptions {
        cmd: Some(vec!["sh", "-c", &shell_command]),
        attach_stdout: Some(true),
        attach_stderr: Some(true),
        ..Default::default()
    };

    let exec = docker.create_exec(container_name, exec_options).await?;
    let output = docker.start_exec(&exec.id, None).await?;

    match output {
        StartExecResults::Attached { mut output, .. } => {
            let mut result = String::new();
            while let Some(Ok(log)) = output.next().await {
                match log {
                    bollard::container::LogOutput::StdOut { message } => {
                        result.push_str(&String::from_utf8_lossy(&message));
                    }
                    bollard::container::LogOutput::StdErr { message } => {
                        result.push_str(&String::from_utf8_lossy(&message));
                    }
                    _ => {}
                }
            }
            Ok(result)
        }
        _ => Err("Failed to execute code in container".into()),
    }
}

fn create_tar_archive(dockerfile_path: &str, tar_path: &str) -> Result<String, Box<dyn Error>> {
    let tar_file = File::create(tar_path)?;
    let mut tar_builder = Builder::new(tar_file);

    let _dockerfile_name = Path::new(dockerfile_path)
        .file_name()
        .ok_or("Invalid Dockerfile path")?
        .to_string_lossy()
        .to_string();
    tar_builder.append_path_with_name(dockerfile_path, "Dockerfile")?;
    tar_builder.finish()?;
    println!("Tar archive created at {}", tar_path);

    Ok("Dockerfile".to_string())
}

// service.rs
 let code = r#"print("Hello, World!")"#;
 let result = docker_manager::handle_request(language, code).await?;
    Ok(result)

Output received.

Server listening on [::1]:50051
Received request: ExecuteRequest { language: "python", code: "", stdin: "" }
Handling request for language: python
Tar archive created at ./docker/context.tar
Using dockerfile_name: 'Dockerfile'
Error during image build: Docker responded with status code 500: Cannot locate specified Dockerfile: Dockerfile
Error: Docker responded with status code 500: Cannot locate specified Dockerfile: Dockerfile


Above is my code folder structure

Read the error message again.

It looks to me like a certain file is missing, and from your screenshot, I have to agree with docker that it is indeed missing.

EDIT: reading is hard, and I have failed to read all of your post, sorry!

I'm not sure what Bollard is doing, so maybe something has gone wrong and docker doesn't know to look for the Dockerfile inside of the tar archive?

Was finally able to figure it out,

    let mut file = tokio::fs::File::open(tar_path).await?;

    let mut contents = Vec::new();
file.read_to_end(&mut contents).await?;

Problem was
file.read(&mut contents).await?;
only reads some bytes into the buffer. It doesn't guarantee reading the entire file. If the tarball is large, contents will only contain the first chunk, potentially truncating it before the actual Dockerfile content or even its full entry is included. Docker then receives an incomplete tar stream and cannot properly find/parse the Dockerfile.python entry.

2 Likes