Skip to content

v0.2.0

Latest

Choose a tag to compare

@github-actions github-actions released this 12 Jul 12:20
Immutable release. Only release title and notes can be modified.

This release ships the entire post-0.1.0 backlog — 59 commits: a new ecosystem-module layer on top of a core round-out across wait strategies, streaming transfers, exec, networks and volumes, compose, environment configuration, lifecycle, and a pre-release hardening audit.

Ecosystem modules (testcontainers::modules)

Seven prebuilt technology wrappers over GenericImage: Redis (redis:7.2), PostgreSQL (postgres:16-alpine), MySQL (mysql:8.4), MariaDB (mariadb:11), Kafka (apache/kafka:3.9.1, single-node KRaft), RabbitMQ (rabbitmq:3.13-management), MongoDB (mongo:7, single-node replica set — transactions and change streams work). Each is a copyable config builder modules::XxxImage whose start() returns a move-only started handle modules::XxxContainer — the naming mirrors the core GenericImageContainer split: the Image is the reusable recipe, the Container the running instance.

#include "testcontainers/modules.hpp"

const modules::PostgreSQLContainer pg = modules::PostgreSQLImage()
        .with_init_script("schema.sql", "CREATE TABLE t(id int);")
        .start();   // returns once every init script ran and TCP serves
// pg.connection_string() -> "postgresql://test:test@localhost:<port>/test"
  • Typed connection getters instead of client drivers: connection_string() (plus PostgreSQL's libpq conninfo() and exec_sql(), Kafka's bootstrap_servers() / internal_bootstrap_servers(), RabbitMQ's amqp_url(), MongoDB's mongosh(); MongoDB's DSN pins directConnection=true so every spec-compliant driver behaves the same). Getters are pure — host and mapped port resolve once at start().
  • Every module pins its image and encodes the readiness probe that actually works there: pg_isready forced onto TCP (the postgres image's first boot runs a temporary socket-only server that log and socket probes misread as ready), Kafka's two-phase KRaft boot (the broker must advertise the mapped host port, which does not exist until after start), RabbitMQ's log-wait-before-any-exec order (an early exec as root bricks the Erlang cookie), MongoDB's rs.initiate + PRIMARY poll from a started hook.
  • A uniform curated pass-through surface on all seven — with_env / with_label / with_network / with_network_alias / with_reuse / with_startup_timeout (a per-phase budget) / with_startup_attempts — plus two escape hatches: with_customizer(fn), applied after the module's own rendering so it wins, and to_generic().
  • Opt-in by include + link: umbrella header testcontainers/modules.hpp, link target testcontainers::modules (its own Conan component; core-only consumers are untouched). One core rider: GenericImage::with_image re-points a configured builder at another image.

Per-module contracts and known limits are documented in docs/feature-notes.md.

Core additions

Wait strategies, host resolution, DSNs

  • wait_for::successful_command({argv...}) / wait_for::successful_shell_command("script") — the seventh wait strategy polls a deadline-bounded exec until an attempt exits 0. Runs on every transport (named pipe and TLS included); non-zero exits and daemon 409s mean "not ready yet", a 404 propagates; the timeout error reports the last completed attempt (exit code + output snippet).
  • One host-address rule behind Container::host(), the HTTP/port wait probes, compose service hosts, and Ryuk registration: TESTCONTAINERS_HOST_OVERRIDE / host.override wins; tcp:///https:// daemons hand out their hostname; a local socket/pipe means "localhost" — unless the test process itself runs inside a Linux container, where the docker-bridge gateway from /proc/net/route is returned. This is the DinD / Testcontainers-Cloud enabler.
  • ConnectionString — a public, render-only DSN builder (scheme://[user[:password]@]host[:port][/database][?k=v&…]) with per-component percent-encoding and IPv6 bracketing; the building block behind the module getters.

Streaming

  • Transfers no longer buffer payloads whole: copy_to_container sends the tar as a chunked upload with host files read in 64 KiB blocks, and GenericBuildableImage::build() streams the context through POST /build the same way (build_image gained a BodyProducer overload). The tar writer moved to pax(restricted), lifting USTAR's 100-char path and 8 GiB file caps.
  • Directory build-context sources honor .dockerignore with docker-build (moby/patternmatcher) semantics.
  • Batched copy_to_container(id, vector<CopyToContainer>) ships a request's whole copy set in one PUT — the runner's create→copy→start path now uses it.
  • Copy-from grew a raw-bytes sink overload, copy_from_container_to(id, path, dest_dir) — a streaming download + extraction, wire to disk, tar-slip protected (the Windows interior-drive-letter escape included) — and container_path_stat(id, path), a cheap HEAD existence/size probe.
  • set_max_response_body(bytes) caps what the buffered paths will hold: a runaway body throws a DockerError naming the cap instead of allocating without bound.

Exec

  • Deadline-bounded streaming exec: exec(cmd, opts, consumer, deadline)ExecStreamResult — why delivery ended (FollowEnd) plus the exit code, present only when the command actually finished (closing the attach stream does not kill it).
  • Stdin now interleaves with the output read on a full-duplex pump (64 KiB chunks), so a command echoing a multi-megabyte stdin back can no longer backpressure the write into a timeout.
  • TTY sizing: ExecOptions::console_size (initial rows × columns at create, API 1.42+), ExecOptions::on_started (hands out the exec id at the first resize-valid moment), and resize_exec / Container::resize_tty.
  • All exec overloads now run one implementation with one end-of-stream contract: any read-side end (EOF, closed pipe, daemon reset) is the peer finishing — output is kept and exec-inspect settles the exit code (the buffered path used to throw on a reset).

Networks & volumes

  • IPAM multi-pool create: repeatable Network::Builder::with_ipam_pool (subnet, IP range, gateway, auxiliary addresses), echoed back typed in NetworkInspect.
  • Network::Builder::with_reuse — find-or-create with the container-reuse semantics (an exact with_name is required; a same-name network with a different config throws instead of creating an ambiguous duplicate).
  • DockerClient::list_networks / list_volumes / prune_volumes — filters pass through verbatim; API 1.42+ daemons prune only anonymous unused volumes unless the {"all","true"} filter is given.
  • Volume::populate now seeds volumes on Windows daemons too: it stages into the created helper's layer, then copies onto the volume from inside as ContainerAdministrator (a Windows daemon extracts archive uploads into the layer, never through mounts).

Compose

  • with_profile (repeatable) activates compose profiles — emitted on up and the teardown down, so profile-gated services are stopped gracefully instead of stranded.
  • with_scale(service, n) plus per-instance accessors: get_service_port / get_service_container_id take an optional instance number, service_instances lists the running ones, and with_exposed_service can probe a specific instance.
  • Per-service logs: get_service_logs (snapshot) and follow_service_logs (blocking, or deadline-bounded returning FollowEnd) — identical behavior under the Local, Containerised, and Auto client modes.
  • with_ambassador(service, port) reaches a service port the YAML never publishes: one socat relay joined to the project network, an ephemeral host port per target, resolved transparently by get_service_port.
  • Compose stacks are crash-safe now: start() registers the com.docker.compose.project filter with Ryuk before up, so even a crash mid-up is swept.

Environment configuration

  • Every env switch gained a ~/.testcontainers.properties key (docker.host, docker.tls.verify, hub.image.name.prefix, host.override, ryuk.disabled, testcontainers.reuse.enable, …) behind one shared parser, read once per process; the file format is shared with testcontainers-java.
  • The active docker context's TLS store (~/.docker/contexts/tls/…) is consumed: a tcp://-spelled context endpoint dials TLS like the docker CLI, SkipTLSVerify is honored, and tcp:// + DOCKER_TLS_VERIFY upgrades to https (default port 2375 → 2376).
  • Every internal utility image is configurable for corporate mirrors — ryuk.container.image, sshd.container.image, socat.container.image, compose.container.image (env or properties) — and TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX now reaches all of them, not just GenericImage.
  • Age-based pull policy: with_image_pull_policy(std::chrono::seconds) re-pulls when the local image's Created timestamp is older than the budget (java PullPolicy.ageBased parity; caveat: Created is the image build time — docker records no pull time).

Lifecycle & reaping

  • Built images carry the session labels (BuildOptions::labels, sent as the ?labels= query), so Ryuk removes them with the rest of the session's resources; base images and pulled layers are untouched.
  • Per-daemon reaper and host-port-forwarder registries keyed by endpoint URL: a second daemon used in the same process boots its own Ryuk and sshd sidecar; compose project filters always reach the environment daemon's reaper.
  • Reuse-hash freshness: copy-to sources now contribute their content identity to the hash (bytes verbatim; path + per-file size+mtime for host sources), so editing a fixture in place no longer adopts the stale reused container.
  • Container::stop(timeout_secs) forwards the daemon grace period (unset = the create-time StopTimeout, 0 = kill now, negative = wait indefinitely), and Container::start() restarts a stopped handle — a plain daemon start: waits and hooks do not re-run, ephemeral ports re-bind.
  • Network.hpp / Volume.hpp are now actually reachable from the testcontainers.hpp umbrella.

Typed HostConfig

  • Create side: with_cpu_limit(double) (docker run --cpus parity), with_cpuset_cpus, with_pids_limit, with_restart_policy (RestartPolicy factories), with_dns_server / with_dns_search / with_dns_option, with_sysctl, with_device. Unset fields emit nothing into the create body, so pre-existing reuse hashes are unaffected.
  • ContainerInspect::host_config echoes the typed knobs back (memory, shm size, nano-cpus, cpuset, pids limit, restart policy, DNS triple, sysctls, devices) — assert a landed limit without parsing inspect_raw().

Registry, network, and TLS robustness

  • pull_image retries a daemon 5xx (how transient registry trouble is relayed) — 3 tries with a 1s-then-2s backoff, tunable via DockerClient::set_pull_retry; 4xx, in-stream pull errors, and transport timeouts never retry.
  • Credential-helper lookups are cached per (helper, registry) for 5 minutes — including the "no credentials" answer, which under Docker Desktop's credsStore used to fork docker-credential-desktop on every single pull.
  • Fixed: the TLS transport configured its SSL context after creating the stream, so the client certificate was never presented and server verification under DOCKER_TLS_VERIFY silently stayed fail-open. Mutual TLS now works and is verified end to end in CI against a real --tlsverify docker:dind daemon (including a docker-context leg with no DOCKER_* connection env).

Pre-release hardening

  • Digest-pinned references (name@sha256:…) work end to end: create, exists/inspect, pull, and the utility-image overrides all split and re-join digest-aware.
  • Deadline arithmetic saturates (detail::saturated_add): an oversized startup timeout no longer overflows the clock rep — UB whose typical wrap failed healthy starts instantly, and only on stdlibs with a fine-grained clock (libstdc++'s nanosecond ticks; MSVC's 100 ns masked it).
  • url_encode no longer consults the C locale; the tar write path tolerates ARCHIVE_WARN like the read paths; sha256 gained the 55/56/64-byte padding-boundary vectors.

Behavior notes

Upgrading from 0.1.0: the public API is source-compatible — nothing shipped in 0.1.0 was renamed or removed. Behavior differences to know:

  • One-time reuse migration: reuse containers created by 0.1.0 with copy-to sources hash differently under the freshness-aware canonical and are not adopted — prune them externally (label sweep on org.testcontainers.reuse.hash). Copy-free reuse configs hash identically and are still adopted.
  • Env vs properties precedence is now java-parity (getEnvVarOrProperty): a SET, non-empty env var decides even when it decides "off". Previously a falsy TESTCONTAINERS_REUSE_ENABLE fell through to a file-enabled testcontainers.reuse.enable.
  • Lazy pull on create now runs only when the 404 body actually says "no such image" — a missing network no longer triggers a spurious re-pull with the error pinned on the wrong resource.
  • TLS verification is enforced: 0.1.0 never presented the client certificate and silently skipped server verification under DOCKER_TLS_VERIFY. A --tlsverify daemon that 0.1.0 could not reach now works; an untrusted server that 0.1.0 silently accepted is now rejected.
  • Built images are session-scoped: build() labels the image and Ryuk removes it after the process exits. To keep built images, disable the reaper (TESTCONTAINERS_RYUK_DISABLED) — the session label is then omitted, as before.
  • The reaper is fail-loud: a daemon where Ryuk cannot start fails that run instead of leaving it silently unwatched (TESTCONTAINERS_RYUK_DISABLED opts out).

Quality gates

~560 unit + ~210 integration tests (v0.1.0: ~300 / ~115), green against real daemons in both engine modes (Linux and Windows containers); the module suite runs live on the Linux CI job. New in CI this cycle: the tls-e2e job drives the integration suite against a mutual-TLS docker:dind daemon on every push. The warnings-as-errors (gcc/clang/MSVC), pinned clang-format + clang-tidy, ASan+UBSan, and CodeQL gates carry over from 0.1.0.

Full Changelog: v0.1.0...v0.2.0

Packaging

Source tarball: https://github.com/cppudge/testcontainers-cpp/archive/refs/tags/v0.2.0.tar.gz
sha256: fc76fbdd9b926d13ff9cd8c584fa74ff14048eb4a5226285206e97fb1fb48f54

ConanCenter conandata.yml entry for this version:

"0.2.0":
  url: "https://github.com/cppudge/testcontainers-cpp/archive/refs/tags/v0.2.0.tar.gz"
  sha256: "fc76fbdd9b926d13ff9cd8c584fa74ff14048eb4a5226285206e97fb1fb48f54"