Image Building

FluentDocker v3 provides a lambda-based API for building Docker images from Dockerfiles or inline definitions. All builder operations require a kernel and a driver scope.

Step by Step

Kernel Setup

Before using the builder, create a kernel. Multiple kernels per application are supported:

using FluentDocker.Kernel;
using FluentDocker.Builders;

// Create kernel (multiple kernels per app are supported)
var kernel = FluentDockerKernel.Create()
    .WithDockerCli("docker", d => d.AsDefault())
    .Build();

The kernel manages driver lifecycle. Many apps reuse one kernel across builder calls, but using multiple kernels in the same app is supported.

Build from Dockerfile

Basic Build

var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseImage("myapp:latest", img => img
        .FromFile("/path/to/Dockerfile"))
    .Build();

var image = results.All.OfType<IImageService>().First();
Console.WriteLine($"Image: {image.Name}");

Build from Dockerfile String

var dockerfileContent = @"
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN npm install
CMD [""node"", ""app.js""]
";

var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseImage("myapp:latest", img => img
        .FromString(dockerfileContent))
    .Build();

Inline Dockerfile

Simple Application

var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseImage("mynode:latest", img => img
        .From("node:18-alpine")
        .Run("npm install -g nodemon")
        .Add("app.js", "/app/app.js")
        .Add("package.json", "/app/package.json")
        .UseWorkDir("/app")
        .Run("npm install")
        .ExposePorts(3000)
        .Command("node", "app.js"))
    .Build();

Important: The UseImage(name, configure) lambda receives a DockerfileBuilder, not an IImageBuilder. The DockerfileBuilder provides Dockerfile instructions (.From(), .Run(), .Copy(), etc.). The IImageBuilder methods listed below are set at a different level – on the ImageBuilder that wraps the DockerfileBuilder. See the IImageBuilder Methods section for details.

IImageBuilder Methods

The ImageBuilder class (which implements IImageBuilder) provides build-level configuration that is separate from the Dockerfile instructions. These methods are not available inside the UseImage lambda directly. Instead, they can be accessed by calling .ToImage() on the DockerfileBuilder to return to the ImageBuilder:

Method Description
ReuseIfAlreadyExists() Skip the build if an image with the same name/tag already exists
AsImageName(string name) Set or override the image name
ImageTag(params string[] tags) Add additional tags to the built image
BuildArguments(params string[] args) Pass build arguments (format: "KEY=VALUE")
Label(params string[] labels) Add labels to the image metadata (format: "key=value")
NoCache() Disable the build cache
AlwaysPull() Always pull the base image, even if cached locally
RemoveIntermediate(bool force = false) Remove intermediate containers after a successful build
Platform(string platform) Set the target platform (e.g., "linux/amd64")
Target(string target) Set the target build stage in a multi-stage Dockerfile

With Environment Variables

var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseImage("myapi:latest", img => img
        .From("node:18-alpine")
        .Environment("NODE_ENV=production")
        .Environment("PORT=8080")
        .UseWorkDir("/app")
        .Copy("package*.json", "./")
        .Run("npm ci --only=production")
        .Copy(".", ".")
        .ExposePorts(8080)
        .Command("node", "server.js"))
    .Build();

Multi-Stage Build

var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseImage("myapp:latest", img => img
        // Build stage
        .From("node:18-alpine", "builder")
        .UseWorkDir("/app")
        .Copy("package*.json", "./")
        .Run("npm ci")
        .Copy(".", ".")
        .Run("npm run build")
        // Production stage
        .From("nginx:alpine")
        .Copy("/app/dist", "/usr/share/nginx/html", fromAlias: "builder")
        .ExposePorts(80))
    .Build();

Note: For multi-stage COPY –from, use the fromAlias parameter on the Copy method.

Dockerfile Instructions

FROM

.From("node:18-alpine")
.From("node:18-alpine", "builder")       // Named stage
.From("node:18-alpine", platform: "linux/amd64")  // With platform

RUN

.Run("apt-get update && apt-get install -y curl")
.Run("npm install")

COPY and ADD

.Copy("src/", "/app/src/")
.Copy("package.json", "/app/")
.Copy("src/", "/app/src/", chownUserAndGroup: "node:node")
.Copy("/app/dist", "/usr/share/nginx/html", fromAlias: "builder")  // COPY --from
.Add("https://example.com/file.tar.gz", "/app/")  // ADD can fetch URLs

WORKDIR

.UseWorkDir("/app")

ENV

.Environment("NODE_ENV=production")
.Environment("PORT=8080", "HOST=0.0.0.0")

EXPOSE

.ExposePorts(80)
.ExposePorts(80, 443, 8080)

CMD and ENTRYPOINT

.Command("node", "app.js")
.Entrypoint("docker-entrypoint.sh")

USER

.User("node")
.User("1000", "1000")  // UID, GID

VOLUME

.Volume("/data")
.Volume("/data", "/logs", "/config")

LABEL

.Label("version=1.0.0")
.Label("maintainer=dev@example.com")

ARG

.Arguments("VERSION", "1.0.0")
.Arguments("NODE_VERSION")

HEALTHCHECK

.WithHealthCheck("curl -f http://localhost/ || exit 1",
    interval: "30s",
    timeout: "10s",
    retries: 3)

SHELL

.Shell("/bin/bash", "-c")

.NET Application Examples

ASP.NET Core API

var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseImage("myapi:latest", img => img
        // Build stage
        .From("mcr.microsoft.com/dotnet/sdk:8.0", "build")
        .UseWorkDir("/src")
        .Copy("*.csproj", "./")
        .Run("dotnet restore")
        .Copy(".", ".")
        .Run("dotnet publish -c Release -o /app/publish")
        // Runtime stage
        .From("mcr.microsoft.com/dotnet/aspnet:8.0")
        .UseWorkDir("/app")
        .Copy("/app/publish", ".", fromAlias: "build")
        .Environment("ASPNETCORE_URLS=http://+:8080")
        .ExposePorts(8080)
        .Entrypoint("dotnet", "MyApi.dll"))
    .Build();

.NET Worker Service

var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseImage("myworker:latest", img => img
        .From("mcr.microsoft.com/dotnet/sdk:8.0", "build")
        .UseWorkDir("/src")
        .Copy(".", ".")
        .Run("dotnet publish -c Release -o /app")
        .From("mcr.microsoft.com/dotnet/runtime:8.0")
        .UseWorkDir("/app")
        .Copy("/app", ".", fromAlias: "build")
        .Entrypoint("dotnet", "MyWorker.dll"))
    .Build();

Python Application

var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseImage("myflask:latest", img => img
        .From("python:3.11-slim")
        .UseWorkDir("/app")
        .Copy("requirements.txt", ".")
        .Run("pip install --no-cache-dir -r requirements.txt")
        .Copy(".", ".")
        .Environment("FLASK_APP=app.py")
        .ExposePorts(5000)
        .Command("flask", "run", "--host=0.0.0.0"))
    .Build();

Go Application

var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseImage("mygo:latest", img => img
        // Build stage
        .From("golang:1.21-alpine", "builder")
        .UseWorkDir("/app")
        .Copy("go.mod", "./")
        .Copy("go.sum", "./")
        .Run("go mod download")
        .Copy(".", ".")
        .Run("CGO_ENABLED=0 go build -o main .")
        // Runtime stage
        .From("alpine:latest")
        .Run("apk --no-cache add ca-certificates")
        .UseWorkDir("/root/")
        .Copy("/app/main", ".", fromAlias: "builder")
        .ExposePorts(8080)
        .Command("./main"))
    .Build();

Build with Container

Build an image and immediately run it as a container in the same builder chain:

var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseImage("myapp:test", img => img
        .From("node:18-alpine")
        .UseWorkDir("/app")
        .Copy(".", ".")
        .Run("npm install")
        .ExposePorts(3000)
        .Command("npm", "start"))
    .UseContainer(c => c
        .UseImage("myapp:test")
        .ExposePort(3000, 3000)
        .WaitForPort("3000/tcp", 30000))
    .Build();

// Access the running container from results
var container = results.Containers.First();

Build Arguments in Dockerfile

var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseImage("myapp:latest", img => img
        .Arguments("VERSION", "1.0.0")
        .Arguments("BUILD_DATE")
        .From("node:18-alpine")
        .Label("version=${VERSION}")
        .Label("build-date=${BUILD_DATE}")
        .UseWorkDir("/app")
        .Copy(".", "."))
    .Build();

Accessing Build Results

The Build() and BuildAsync() methods return a BuildResults object:

var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseImage("myapp:latest", img => img.From("alpine:latest"))
    .UseContainer(c => c
        .UseImage("myapp:latest")
        .WithName("myapp"))
    .Build();

// All services
var allServices = results.All;

// Typed access
var images = results.OfType<IImageService>();
var containers = results.Containers;

// By name (requires .WithName("myapp") on the container builder)
var myContainer = results.GetContainer("myapp");

// Dispose all services when done
results.Dispose();

Note: BuildResults does not have an Images property. To access built images, use the generic OfType<T>() method:

var images = results.OfType<IImageService>();
// or equivalently:
var images = results.All.OfType<IImageService>().ToList();

The available typed convenience properties on BuildResults are: Containers, Networks, Volumes, and ComposeServices. For images, always use OfType<IImageService>().

Async Build

For async contexts (ASP.NET, UI applications), use BuildAsync to avoid deadlocks:

var results = await new Builder()
    .WithinDriver("docker", kernel)
    .UseImage("myapp:latest", img => img
        .From("alpine:latest")
        .Run("echo 'hello'"))
    .BuildAsync();

// Async disposal
await results.DisposeAllAsync();

Testing with Custom Images

public class CustomImageTest : IDisposable
{
    private readonly FluentDockerKernel _kernel;
    private readonly BuildResults _results;

    public CustomImageTest()
    {
        _kernel = FluentDockerKernel.Create()
            .WithDockerCli("docker", d => d.AsDefault())
            .Build();

        _results = new Builder()
            .WithinDriver("docker", _kernel)
            .UseImage("test-app:latest", img => img
                .From("node:18-alpine")
                .UseWorkDir("/app")
                .Copy("./test-fixtures/", "/app/")
                .Run("npm install")
                .ExposePorts(3000)
                .Command("npm", "test"))
            .UseContainer(c => c
                .UseImage("test-app:latest")
                .ExposePort(3000, 3000)
                .WaitForPort("3000/tcp", 30000))
            .Build();
    }

    [Fact]
    public async Task App_ReturnsHealthy()
    {
        var container = _results.Containers.First();
        var endpoint = container.ToHostExposedEndpoint("3000/tcp");
        var response = await $"http://localhost:{endpoint.Port}/health".Wget();
        Assert.Contains("healthy", response);
    }

    public void Dispose()
    {
        _results?.Dispose();
        _kernel?.Dispose();
    }
}

Next Steps