Container Management

Complete guide to creating, configuring, and managing containers with FluentDocker v3.

Step by Step

Kernel Setup

Before building any containers, create a kernel. Multiple kernels per application are supported. The kernel manages driver instances and provides access to container runtimes.

using FluentDocker.Kernel;
using FluentDocker.Builders;

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

All subsequent examples assume this kernel variable is available.

Container Lifecycle

Create and Start

In v3, Build() both creates and starts containers automatically. The result is a BuildResults object containing all built services.

using var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseContainer(c => c
        .UseImage("nginx:alpine"))
    .Build();

var container = results.Containers.First();
// Container is already running at this point

Stop and Start Cycle

Since containers auto-start during Build(), use the container service to stop and restart as needed.

using var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseContainer(c => c
        .UseImage("nginx:alpine"))
    .Build();

var container = results.Containers.First();

// Stop the running container
await container.StopAsync();

// Start again
await container.StartAsync();

Container States

var state = container.State;
// ServiceRunningState.Running, Stopped, Paused, etc.

if (container.State == ServiceRunningState.Running)
{
    Console.WriteLine("Container is running");
}

Pause and Resume

await container.PauseAsync();
await container.StartAsync();  // StartAsync() also resumes from Pause

Port Exposure

Explicit Port Mapping

// Map host port 8080 to container port 80
using var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseContainer(c => c
        .UseImage("nginx:alpine")
        .ExposePort(8080, 80))
    .Build();

// Access at http://localhost:8080

Random Port Assignment

// Let Docker assign a random host port
using var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseContainer(c => c
        .UseImage("nginx:alpine")
        .ExposePort("80"))
    .Build();

var container = results.Containers.First();

// Get the assigned port
var endpoint = container.ToHostExposedEndpoint("80/tcp");
Console.WriteLine($"Port: {endpoint.Port}");

Multiple Ports

using var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseContainer(c => c
        .UseImage("myapp:latest")
        .ExposePort(8080, 80)
        .ExposePort(8443, 443)
        .ExposePort(9090, 9090))
    .Build();

var container = results.Containers.First();
var httpEndpoint = container.ToHostExposedEndpoint("80/tcp");
var httpsEndpoint = container.ToHostExposedEndpoint("443/tcp");
var metricsEndpoint = container.ToHostExposedEndpoint("9090/tcp");

Environment Variables

Each call to WithEnvironment() sets one variable. Two overloads are available: WithEnvironment("KEY=VALUE") and WithEnvironment("KEY", "VALUE").

using var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseContainer(c => c
        .UseImage("postgres:15-alpine")
        .WithEnvironment("POSTGRES_PASSWORD=secret")
        .WithEnvironment("POSTGRES_USER", "myuser")
        .WithEnvironment("POSTGRES_DB", "mydb")
        .WithEnvironment("PGDATA=/var/lib/postgresql/data/pgdata"))
    .Build();

Wait Strategies

Wait for Port

using var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseContainer(c => c
        .UseImage("postgres:15-alpine")
        .WithEnvironment("POSTGRES_PASSWORD=secret")
        .ExposePort("5432")
        .WaitForPort("5432/tcp", 30000))
    .Build();

Wait for Process

using var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseContainer(c => c
        .UseImage("postgres:15-alpine")
        .WithEnvironment("POSTGRES_PASSWORD=secret")
        .WaitForProcess("postgres", 30000))
    .Build();

Wait for Log Message

using var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseContainer(c => c
        .UseImage("postgres:15-alpine")
        .WithEnvironment("POSTGRES_PASSWORD=secret")
        .WaitForLogMessage("database system is ready", 30000))
    .Build();

Wait for Healthy

Waits for the container’s Docker HEALTHCHECK to report healthy.

using var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseContainer(c => c
        .UseImage("myapp:latest")
        .WaitForHealthy(60000))
    .Build();

Wait for HTTP

using var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseContainer(c => c
        .UseImage("myapp:latest")
        .ExposePort("8080")
        .WaitForHttp("8080/tcp", "/health", 30000))
    .Build();

Custom Wait Function

The .Wait() lambda receives the container service and an iteration counter. Return values:

  • Negative (e.g. -1): success, stop waiting
  • Zero (0): not ready, retry immediately
  • Positive (e.g. 500): not ready, wait that many milliseconds before retry
using var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseContainer(c => c
        .UseImage("myapp:latest")
        .ExposePort("8080")
        .Wait((service, iteration) =>
        {
            try
            {
                var ep = service.ToHostExposedEndpoint("8080/tcp");
                var response = $"http://localhost:{ep.Port}/health".Wget()
                    .GetAwaiter().GetResult();
                return response.Contains("ok") ? -1 : 500;
            }
            catch
            {
                return 500;
            }
        }))
    .Build();

File Operations

Copy to Container on Start

Files are copied after the container starts (lifecycle hook).

using var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseContainer(c => c
        .UseImage("nginx:alpine")
        .CopyToOnStart("/local/nginx.conf", "/etc/nginx/nginx.conf")
        .CopyToOnStart("/local/html/", "/usr/share/nginx/html/"))
    .Build();

Copy from Container on Dispose

Files are copied from the container before it is removed.

using var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseContainer(c => c
        .UseImage("myapp:latest")
        .CopyFromOnDispose("/app/logs/", "/local/artifacts/logs/")
        .CopyFromOnDispose("/app/coverage/", "/local/artifacts/coverage/"))
    .Build();

// Run tests...
// When disposed, logs and coverage are copied out

Export on Dispose

Export the entire container filesystem as a tar archive on dispose.

using var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseContainer(c => c
        .UseImage("myapp:latest")
        .ExportOnDispose("/local/artifacts/container.tar"))
    .Build();

With a condition and explode (extract tar contents):

using var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseContainer(c => c
        .UseImage("myapp:latest")
        .ExportOnDispose("/local/artifacts/", svc => svc.State == ServiceRunningState.Running, explode: true))
    .Build();

Execute Commands

On Running (Lifecycle Hook)

Execute a command automatically after the container starts.

using var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseContainer(c => c
        .UseImage("postgres:15-alpine")
        .WithEnvironment("POSTGRES_PASSWORD=secret")
        .WaitForPort("5432/tcp", 30000)
        .ExecuteOnRunning("psql", "-U", "postgres", "-c", "CREATE DATABASE mydb;"))
    .Build();

On Disposing (Lifecycle Hook)

Execute a command before the container is removed.

using var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseContainer(c => c
        .UseImage("myapp:latest")
        .ExecuteOnDisposing("sh", "-c", "echo 'shutting down' >> /app/log.txt"))
    .Build();

Ad-hoc Commands on a Running Container

var container = results.Containers.First();

var result = await container.ExecuteAsync("echo Hello World");
Console.WriteLine(result);  // "Hello World"

// Shell commands
var output = await container.ExecuteAsync("sh -c 'ls -la /app && cat /app/config.json'");

// Redis example
await container.ExecuteAsync("redis-cli SET mykey myvalue");
var value = await container.ExecuteAsync("redis-cli GET mykey");

Names, Labels, and Configuration

using var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseContainer(c => c
        .WithName("my-app-container")
        .UseImage("nginx:alpine")
        .WithLabel("app", "myapp")
        .WithLabel("version", "1.0.0"))
    .Build();

var container = results.Containers.First();
var config = container.GetConfiguration(fresh: true);
Console.WriteLine($"ID: {config.Id}, Name: {config.Name}, State: {config.State.Status}");

Resource Limits

using var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseContainer(c => c
        .UseImage("myapp:latest")
        .WithMemoryLimit(512 * 1024 * 1024)  // 512MB
        .WithCpuShares(1024))                 // CPU shares
    .Build();

Advanced Container Options

The following methods configure additional container properties inside the UseContainer(c => ...) lambda:

using var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseContainer(c => c
        .UseImage("myapp:latest")
        .WithPrivileged()                     // Full host access (use with caution)
        .WithWorkingDirectory("/app")         // Default working directory
        .WithCommand("sh", "-c", "sleep 3600") // Override CMD
        .WithHostname("app-host")
        .WithUser("appuser")
        .WithNetwork("my-network")            // Attach to named network
        .WithNetworkAlias("my-network", "app") // DNS alias on network
        .WithIPv4("10.18.0.22"))               // Static IP (requires custom subnet)
    .Build();

Container Existence Behavior

When a container with the same name already exists, control what happens:

// Reuse the existing container if one matches by name
.ReuseIfExists()

// Destroy the existing container and create a new one
.DestroyIfExists(force: true, removeVolumes: true)

// Always pull the latest image before creating
.ForcePullImage()

Cleanup and Dispose Behavior

By default, containers are stopped and removed when BuildResults is disposed. Use these methods inside the UseContainer(c => ...) lambda to customize:

.KeepContainer()            // Don't remove container on dispose (for debugging)
.KeepRunning()              // Don't stop container on dispose
.WithAutoRemove()           // Docker-level auto-remove on stop
.DeleteVolumeOnDispose()    // Remove anonymous volumes on dispose
.DeleteNamedVolumeOnDispose() // Remove named volumes on dispose

Container Logs

var container = results.Containers.First();

var logs = await container.GetLogsAsync();
foreach (var line in logs.Split('\n'))
{
    Console.WriteLine(line);
}

Volumes (Bind Mounts and Named Volumes)

// Bind mount
using var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseContainer(c => c
        .UseImage("nginx:alpine")
        .WithVolume("/local/html", "/usr/share/nginx/html"))
    .Build();

// Named volume
using var results2 = new Builder()
    .WithinDriver("docker", kernel)
    .UseContainer(c => c
        .UseImage("postgres:15-alpine")
        .WithEnvironment("POSTGRES_PASSWORD=secret")
        .WithVolume("pgdata", "/var/lib/postgresql/data"))
    .Build();

Multiple Containers

Build multiple containers in a single builder call.

using var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseContainer(c => c
        .WithName("db")
        .UseImage("postgres:15-alpine")
        .WithEnvironment("POSTGRES_PASSWORD=secret")
        .WaitForPort("5432/tcp", 30000))
    .UseContainer(c => c
        .WithName("app")
        .UseImage("myapp:latest")
        .WithEnvironment("DATABASE_HOST", "db")
        .WithNetwork("my-network")
        .ExposePort(8080, 80))
    .Build();

var db = results.GetContainer("db");
var app = results.GetContainer("app");

Next Steps