FluentDocker

repository·master·Indexed 23 days ago

https://github.com/mariotoffia/fluentdocker

A C# library providing a fluent API for managing Docker and Podman containers, Docker Compose, and Podman Kubernetes (kube play/down) within .NET applications and tests. It supports multiple drivers including Docker CLI, Podman CLI, and the Docker Engine API. Key features include container resource monitoring, static IPv4 assignment, custom network/volume management, inline Dockerfile building, and integration packages for Xunit, NUnit, and MsTest.

Tokens
78.7K
Snippets
172
Records
313
Agent score
80%

What's inside FluentDocker

  1. Understand the FluentDocker v3.0 Architecture

    master

    FluentDocker v3.0 uses a layered, pluggable driver architecture. This design allows for multiple container runtime implementations (like Docker CLI, Docker API, or Podman CLI) to coexist.

    The architecture consists of three layers:

    1. Fluent API (Builders): The high-level interface used to define deployments. Builders bind to a specific FluentDockerKernel instance.
    2. Services (Domain Objects): Objects that represent containers, networks, or volumes, which reference a specific kernel instance.
    3. FluentDocker Kernel: The central orchestrator that manages the DriverRegistry, DriverSelector, and DriverRouter. It is instantiable, meaning you can have multiple independent kernels (e.g., one for local Docker and one for remote Docker) running in the same application.
    4. Driver Layer: The actual implementation of the container runtime (e.g., dc-1 using Docker CLI, podman-1 using Podman CLI).
  2. Wait for healthy services in Docker Compose

    master

    FluentDocker v3 leverages Docker Compose V2's native --wait flag. When .WithWait() is used, Compose will wait for every service that has a healthcheck defined in the docker-compose.yml to report healthy before the .Build() method returns. You can specify a timeout in seconds using .WithWaitTimeout(seconds).

    using var results = new Builder()
        .WithinDriver("docker", kernel)
        .UseCompose(c => c
            .WithComposeFile("docker-compose.yml")
            .WithWait()
            .WithWaitTimeout(120)) // seconds
        .Build();
  3. Use Driver-Aware Builder Extensions

    master

    Builders implement IDriverScopedBuilder, which provides access to the kernel and driver ID inside configuration lambdas. This allows for driver-specific extensions (like .UsePod() for Podman) that will gracefully no-op if the current driver does not support them, preventing runtime errors when switching between Docker and Podman.

    // Podman-specific .UsePod() — no-op on Docker
    await new Builder()
        .WithinDriver("podman", kernel)
        .UseContainer(c => c
            .UseImage("redis:7-alpine")
            .UsePod("cache-pod")         // Only applies on Podman
            .ExposePort(6379, 6379))
        .BuildAsync();
  4. Use the Async Pattern and BuildAsync() in FluentDocker v3.0

    master

    In v3.0, all operations are asynchronous. The terminal method for any builder is BuildAsync(), which returns a Task<TResult>.

    Key changes from v2:

    • Use await .BuildAsync() instead of .Build()
    • Use await .StartAsync() instead of .Start()
    • Use await .StopAsync() instead of .Stop()
    • Use await .RemoveAsync() instead of .Remove()

    All async methods accept a CancellationToken to allow for timeouts or manual cancellation.

    // Kernel initialization
    var kernel = await FluentDockerKernel.Create()
        .WithDockerCli("docker", d => { })
        .BuildAsync();  // TERMINAL ASYNC
    
    // Container deployment
    var results = await new Builder()
        .WithinDriver("docker", kernel)
        .UseContainer(c => c.UseImage("nginx"))
        .BuildAsync();  // TERMINAL ASYNC
    
    // Service operations
    await results.All[0].StartAsync();
    await results.DisposeAllAsync();
    
    // With CancellationToken
    using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
    var deployment = await new Builder()
        .WithinDriver("docker", kernel)
        .UseContainer(c => c.UseImage("nginx"))
        .BuildAsync(cts.Token);
  5. Understand limitations of local act testing

    master

    When running workflows locally via act, be aware of the following limitations:

    1. Container-based tests are skipped: They do not work reliably in a Docker-in-Docker setup.
    2. SonarCloud scanning is skipped.
    3. NuGet package publishing is simulated: Commands will run, but packages will not actually be published.

    Recommendation: For a full test suite that includes container-based tests, run dotnet test directly on your development machine with Docker installed.

  6. NuGet packaging features

    master

    The project utilizes modern NuGet packaging with the following integrated features:

    • Package icon: The icon is sourced from icon/fluent-docker.png and included in the packages.
    • README files: Each project includes its README.md within the package.
    • Source Link: Source code is linked to GitHub repositories to facilitate debugging.
    • Versioning: Automatic versioning is handled via GitVersion.
  7. Use the v3 Lambda-Scoped Builder Pattern

    master

    v3 replaces the flat, chained builder API from v2 with lambda-scoped sub-builders nested inside WithinDriver(). Additionally, Build() now auto-starts resources, so an explicit .Start() call is typically no longer needed.

    // v3 pattern: WithinDriver + Lambda sub-builders
    var results = new Builder()
        .WithinDriver("docker", kernel)
        .UseContainer(c => c
            .UseImage("postgres:alpine")
            .WithEnvironment("POSTGRES_PASSWORD=secret")
            .ExposePort(5432, 5432))
        .Build();
    
    // Build() auto-starts; access resources via results
    var container = results.Containers[0];
  8. Manage Multiple Containers and Networks in a Single Builder

    master

    v3 allows you to declare networks, containers, and other services in a single builder chain with one terminal Build() call. This ensures all services are managed together. Use results.GetContainer("name") to retrieve specific containers by their assigned name. To ensure network cleanup, use .RemoveOnDispose() on the network configuration.

    using var results = new Builder()
        .WithinDriver("docker", kernel)
        .UseNetwork(n => n
            .WithName("backend-net")
            .RemoveOnDispose())
        .UseContainer(c => c
            .UseImage("redis:7-alpine")
            .WithName("cache")
            .WithNetwork("backend-net")
            .ExposePort("6379")
            .WaitForPort("6379/tcp", 30000))
        .UseContainer(c => c
            .UseImage("myapp:latest")
            .WithName("webapp")
            .WithNetwork("backend-net")
            .WithEnvironment("REDIS_HOST", "cache")
            .ExposePort("8080")
            .WaitForPort("8080/tcp", 30000))
        .Build();
    
    var network = results.Networks.First();
    var cache = results.GetContainer("cache");
    var webapp = results.GetContainer("webapp");
  9. Subscribe to Docker daemon events to trigger container startup

    master

    You can subscribe to events emitted by a Docker daemon to trigger specific actions, such as spinning up a container. When an event occurs on the daemon, FluentDocker can capture the event type (e.g., ContainerCreateEvent, NetworkConnectEvent, or ContainerStartEvent) and use it to drive logic like service orchestration or automated testing.

    Events:
    FluentDocker.Model.Events.ContainerCreateEvent
    FluentDocker.Model.Events.NetworkConnectEvent
    FluentDocker.Model.Events.ContainerStartEvent
  10. Use TemplateString for dynamic path interpolation

    master

    The TemplateString class allows for dynamic path construction using special tokens for system directories, random strings, and environment variables. This is useful for creating unique temporary directories for tests or mounting configuration files.

    Supported Variables

    VariableDescriptionExample
    ${TEMP}System temp directory/tmp
    ${TMP}Same as TEMP/tmp
    ${RND}Random filename via Path.GetRandomFileName()tmpk4xz0f.tmp
    ${PWD}Current working directory/home/user/project
    ${E_*}Environment variable (prefixed with E_)${E_HOME} -> /home/user

    Usage Examples

    Basic paths and random suffixes:

    // Temporary directory
    var tempPath = new TemplateString("${TEMP}/myapp");
    
    // With random suffix
    var uniquePath = new TemplateString("${TEMP}/test-${RND}");
    
    // Current directory
    var workPath = new TemplateString("${PWD}/config");

    Environment variables: To access an environment variable, use the ${E_VAR_NAME} syntax. If the variable is unset, the token remains unexpanded as a literal string.

    // Access any environment variable with E_ prefix
    var homePath = new TemplateString("${E_HOME}/myapp");
    
    // Custom environment variables
    Environment.SetEnvironmentVariable("MY_VAR", "custom-value");
    var customPath = new TemplateString("${E_MY_VAR}/data");

    Combining variables:

    var path = new TemplateString("${TEMP}/${E_USER}/session-${RND}");
    using FluentDocker.Model.Common;
    
    // Temporary directory
    var tempPath = new TemplateString("${TEMP}/myapp");
    // Expands to: /tmp/myapp (Linux) or C:\Users\...\AppData\Local\Temp\myapp (Windows)
    
    // With random suffix
    var uniquePath = new TemplateString("${TEMP}/test-${RND}");
    // Expands to: /tmp/test-tmpk4xz0f.tmp
    
    // Current directory
    var workPath = new TemplateString("${PWD}/config");
    // Expands to: /current/working/directory/config
  11. How driver extensibility works in FluentDocker

    master

    FluentDocker uses an extensibility model that allows drivers to expose custom interfaces and builder extensions without modifying the core kernel. This enables driver-specific features (like Podman pods or Docker Swarm modes) to integrate into the fluent API.

    The Workflow:

    1. Extension Methods (e.g., .UsePod("name")) are called on builders.
    2. These methods cast the builder to an IDriverScopedBuilder to access the current Kernel and DriverId.
    3. The builder uses TryDriver<T>() or RequireDriver<T>() to resolve the specific driver interface.
    4. The kernel resolves the interface via IDriverInterfaceResolver using a cascading strategy:
      • Check the Driver Pack IDriverInterfaceResolver
      • Check the Driver Pack SysCtl
      • Check the Driver IDriverInterfaceResolver
      • Fallback to a direct cast.
    ┌──────────────────────────────┐
                                     │   Extension Methods          │
                                     │   .UsePod("my-pod")          │
                                     │   .UseSwarmMode(replicas: 3) │
                                     └──────────┬───────────────────┘
                                                │ casts to
                                     ┌──────────▼───────────────────┐
                                     │   IDriverScopedBuilder       │
                                     │   .Kernel + .DriverId        │
                                     └──────────┬───────────────────┘
                                                │ calls
                                     ┌──────────▼───────────────────┐
                                     │   TryDriver<T>() /           │
                                     │   RequireDriver<T>()         │
                                     └──────────┬───────────────────┘
                                                │ delegates to
                            ┌───────────────────▼──────────────────┐
                            │  FluentDockerKernel.SysCtl(id, Type) │
                            └───────────────────┬──────────────────┘
                                                │ resolves via
                            ┌───────────────────▼──────────────────┐
                            │   IDriverInterfaceResolver           │
                            │   on DriverPack or Driver            │
                            └──────────────────────────────────────┘
  12. Migrate Builder API to the lambda + WithinDriver pattern

    master

    In FluentDocker v3.0.0, the builder API has changed. Instead of direct instantiation or configuration, you must use the WithinDriver pattern within a lambda context. This applies to:

    • Container Builder
    • Network Builder
    • Volume Builder

    For specific method-by-method mapping, refer to the Complete API Mapping documentation.