Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Containers

The Container module is the heart of Testcontainers OCaml. It manages the complete lifecycle of Docker containers—from configuration through cleanup.

Container Request

Before starting a container, you build a request describing what you want:

open Testcontainers

let request =
  Container_request.create "nginx:alpine"
  |> Container_request.with_exposed_port (Port.tcp 80)
  |> Container_request.with_env "NGINX_HOST" "localhost"
  |> Container_request.with_wait_strategy
       (Wait_strategy.for_http ~port:(Port.tcp 80) "/")

Available Configuration

FunctionDescription
create imageCreate request with Docker image
with_exposed_port portExpose a port
with_exposed_ports portsExpose multiple ports
with_env key valueSet environment variable
with_envs pairsSet multiple env vars
with_cmd argsOverride CMD
with_entrypoint argsOverride ENTRYPOINT
with_working_dir dirSet working directory
with_user userRun as specific user
with_name nameSet container name
with_label key valueAdd label
with_labels pairsAdd multiple labels
with_mount mountAdd volume mount
with_privileged boolRun in privileged mode
with_wait_strategy strategySet readiness check
with_startup_timeout secondsMax wait time
with_auto_remove boolRemove on stop

Starting Containers

Manual Lifecycle

open Lwt.Syntax

let run_test () =
  let request = Container_request.create "redis:7-alpine"
    |> Container_request.with_exposed_port (Port.tcp 6379)
  in

  (* Start container *)
  let* container = Container.start request in

  (* Use container *)
  let* host = Container.host container in
  let* port = Container.mapped_port container (Port.tcp 6379) in
  Printf.printf "Redis at %s:%d\n" host port;

  (* Stop and remove *)
  let* () = Container.stop container in
  let* () = Container.terminate container in
  Lwt.return_unit

Use with_container for automatic cleanup:

let run_test () =
  let request = Container_request.create "redis:7-alpine"
    |> Container_request.with_exposed_port (Port.tcp 6379)
  in

  Container.with_container request (fun container ->
    let* host = Container.host container in
    let* port = Container.mapped_port container (Port.tcp 6379) in
    Printf.printf "Redis at %s:%d\n" host port;
    Lwt.return_unit
  )
  (* Container automatically stopped and removed here *)

Container Operations

Getting Connection Details

(* Host is always 127.0.0.1 for local Docker *)
let* host = Container.host container in

(* Get mapped port (Docker assigns random available port) *)
let* port = Container.mapped_port container (Port.tcp 5432) in

(* Get all mapped ports *)
let* ports = Container.mapped_ports container in

Executing Commands

Run commands inside the container:

let* (exit_code, output) = Container.exec container ["echo"; "hello"] in
Printf.printf "Exit: %d, Output: %s\n" exit_code output;

(* Run shell commands *)
let* (exit_code, output) = Container.exec container [
  "sh"; "-c"; "echo $HOME && whoami"
] in

Accessing Logs

(* Get all logs *)
let* logs = Container.logs container in
print_endline logs;

(* Stream logs with callback *)
let* () = Container.follow_logs
  ~on_log:(fun chunk -> Printf.printf "%s" chunk; Lwt.return_unit)
  container
in

(* Logs are useful for debugging test failures *)

Container Networking Info

(* Get container's IP address *)
let* ip = Container.container_ip container in
Printf.printf "Container IP: %s\n" ip;

(* Get all IPs (when connected to multiple networks) *)
let* ips = Container.container_ips container in
List.iter (fun (network, ip) ->
  Printf.printf "Network %s: %s\n" network ip
) ips;

(* Get network aliases *)
let* aliases = Container.network_aliases container in
List.iter (fun (network, alias) ->
  Printf.printf "Alias on %s: %s\n" network alias
) aliases;

(* Get gateway address *)
let* gateway = Container.gateway container in
Printf.printf "Gateway: %s\n" gateway;

Full Container Inspection

(* Get complete container info *)
let* info = Container.inspect container in
Printf.printf "Container ID: %s\n" info.id;
Printf.printf "Name: %s\n" info.name;
Printf.printf "Running: %b\n" info.state.running;
Printf.printf "IP Address: %s\n" info.network_settings.ip_address;

Checking State

(* Is container running? *)
let* running = Container.is_running container in

(* Get detailed state *)
let* state = Container.state container in
match state with
| `Running -> print_endline "Running"
| `Exited -> print_endline "Exited"
| `Created -> print_endline "Created but not started"
| `Paused -> print_endline "Paused"
| _ -> print_endline "Other state"

Container ID and Name

let id = Container.id container in    (* e.g., "a1b2c3d4e5f6" *)
let name = Container.name container in (* e.g., "/fervent_euler" *)

Port Mapping

Docker maps container ports to random available host ports:

Container Port 5432/tcp  →  Host Port 54321
Container Port 80/tcp    →  Host Port 32768

Port Types

(* TCP port (most common) *)
let pg_port = Port.tcp 5432

(* UDP port *)
let dns_port = Port.udp 53

(* Parse from string *)
let port = Port.of_string "8080/tcp"

Exposing Ports

(* Single port *)
Container_request.with_exposed_port (Port.tcp 80) request

(* Multiple ports *)
Container_request.with_exposed_ports [
  Port.tcp 80;
  Port.tcp 443;
  Port.tcp 8080;
] request

Environment Variables

let request =
  Container_request.create "postgres:16"
  |> Container_request.with_env "POSTGRES_PASSWORD" "secret"
  |> Container_request.with_env "POSTGRES_USER" "admin"
  |> Container_request.with_env "POSTGRES_DB" "myapp"

(* Or use with_envs for multiple *)
  |> Container_request.with_envs [
       ("POSTGRES_PASSWORD", "secret");
       ("POSTGRES_USER", "admin");
       ("POSTGRES_DB", "myapp");
     ]

Volume Mounts

Bind Mounts

Mount host directory into container:

let mount = Volume.bind
  ~host:"/path/on/host"
  ~container:"/path/in/container"
  ()

let request =
  Container_request.create "nginx:alpine"
  |> Container_request.with_mount mount

Read-Only Mounts

let mount = Volume.bind
  ~mode:Volume.ReadOnly
  ~host:"/configs"
  ~container:"/etc/nginx/conf.d"
  ()

Named Volumes

let mount = Volume.volume
  ~name:"my-data"
  ~container:"/data"
  ()

Tmpfs Mounts

In-memory filesystem:

let mount = Volume.tmpfs
  ~container:"/tmp"
  ~size:67108864  (* 64MB *)
  ()

Command and Entrypoint

(* Override CMD *)
let request =
  Container_request.create "alpine:latest"
  |> Container_request.with_cmd ["sleep"; "3600"]

(* Override ENTRYPOINT *)
let request =
  Container_request.create "alpine:latest"
  |> Container_request.with_entrypoint ["/bin/sh"; "-c"]
  |> Container_request.with_cmd ["echo hello && sleep 3600"]

Labels

Labels help identify and filter containers:

let request =
  Container_request.create "nginx:alpine"
  |> Container_request.with_label "project" "my-app"
  |> Container_request.with_label "environment" "test"
  |> Container_request.with_labels [
       ("version", "1.0");
       ("team", "backend");
     ]

Best Practices

Always Use with_container

(* Good: automatic cleanup *)
Container.with_container request (fun c -> ...)

(* Risky: manual cleanup might be skipped on error *)
let* c = Container.start request in
(* if error occurs here, container leaks *)
let* () = Container.terminate c in

Use Specific Image Tags

(* Good: reproducible *)
Container_request.create "postgres:16.1-alpine"

(* Risky: may change unexpectedly *)
Container_request.create "postgres:latest"

Set Reasonable Timeouts

Container_request.with_startup_timeout 120.0  (* 2 minutes *)

Use Alpine Images When Possible

(* Faster to pull, smaller footprint *)
"postgres:16-alpine"
"redis:7-alpine"
"nginx:alpine"