Releases: cppudge/testcontainers-cpp
Release list
v0.2.0
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 GenericImage → Container 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 libpqconninfo()andexec_sql(), Kafka'sbootstrap_servers()/internal_bootstrap_servers(), RabbitMQ'samqp_url(), MongoDB'smongosh(); MongoDB's DSN pinsdirectConnection=trueso every spec-compliant driver behaves the same). Getters are pure — host and mapped port resolve once atstart(). - Every module pins its image and encodes the readiness probe that actually works there:
pg_isreadyforced 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'srs.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, andto_generic(). - Opt-in by include + link: umbrella header
testcontainers/modules.hpp, link targettestcontainers::modules(its own Conan component; core-only consumers are untouched). One core rider:GenericImage::with_imagere-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.overridewins;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/routeis 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_containersends the tar as a chunked upload with host files read in 64 KiB blocks, andGenericBuildableImage::build()streams the context throughPOST /buildthe same way (build_imagegained aBodyProduceroverload). The tar writer moved to pax(restricted), lifting USTAR's 100-char path and 8 GiB file caps. - Directory build-context sources honor
.dockerignorewith 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) — andcontainer_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 aDockerErrornaming 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), andresize_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 inNetworkInspect. Network::Builder::with_reuse— find-or-create with the container-reuse semantics (an exactwith_nameis 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::populatenow 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 onupand the teardowndown, so profile-gated services are stopped gracefully instead of stranded.with_scale(service, n)plus per-instance accessors:get_service_port/get_service_container_idtake an optional instance number,service_instanceslists the running ones, andwith_exposed_servicecan probe a specific instance.- Per-service logs:
get_service_logs(snapshot) andfollow_service_logs(blocking, or deadline-bounded returningFollowEnd) — 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 byget_service_port.- Compose stacks are crash-safe now:
start()registers thecom.docker.compose.projectfilter with Ryuk beforeup, so even a crash mid-upis swept.
Environment configuration
- Every env switch gained a
~/.testcontainers.propertieskey (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: atcp://-spelled context endpoint dials TLS like the docker CLI,SkipTLSVerifyis honored, andtcp://+DOCKER_TLS_VERIFYupgrades 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) — andTESTCONTAINERS_HUB_IMAGE_NAME_PREFIXnow reaches all of them, not justGenericImage. - Age-based pull policy:
with_image_pull_policy(std::chrono::seconds)re-pulls when the local image'sCreatedtimestamp is older than the budget (javaPullPolicy.ageBasedparity; caveat:Createdis 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-timeStopTimeout,0= kill now, negative = wait indefinitely), and `Conta...
v0.1.0
First tagged release. Native C++20 port of Testcontainers: spin up real Docker containers from your integration tests and tear them down automatically — a pure C++ client over the Docker Engine HTTP API. No docker CLI, no Rust bridge.
Highlights
GenericImage/GenericBuildableImage/Container/Network/Volume/DockerComposeContainer- Six wait strategies; exec (stdin / tty / streaming); copy to/from container; lifecycle hooks; container reuse
- Ryuk crash-safety reaper; registry auth including credential helpers; image build from Dockerfile
- Host-port exposure (
with_exposed_host_port: sshd sidecar + SSH tunnel back to the host) - Transports: unix socket (Linux/macOS), named pipe (Windows),
tcp:///https://(TLS) for remote daemons - First-class Windows containers: CI drives real Windows containers with mirror suites for build/volumes/networks/exec/copy/ports/waits/lifecycle
Quality gates
~300 unit + ~115 integration tests, green against real daemons in both engine modes (Linux and Windows containers). CI gates on -Wall -Wextra / /W4 as errors across gcc, clang, and MSVC, pinned clang-format + clang-tidy, an ASan+UBSan run of both suites, and CodeQL. A 100+-iteration stability soak of the integration suites on each engine passed with zero library failures.
Install
Conan 2 (in-repo recipe; ConanCenter submission is in progress):
conan create . --build=missing -s compiler.cppstd=20
CMake consumers: find_package(testcontainers CONFIG) and link testcontainers::testcontainers.
Requires C++20 (gcc ≥ 12, clang ≥ 15, MSVC ≥ 193, apple-clang ≥ 15). Dependencies: Boost (Beast/Asio, header-only usage), nlohmann_json, libarchive; optional OpenSSL (tls) and libssh2 (host_port_forwarding) — both features can be disabled to drop the dependencies entirely.
Known limits
Per-feature limits are documented in docs/feature-notes.md; the per-API engine-mode coverage matrix lives in docs/public-api-test-coverage.md. The main unverified path is end-to-end TLS against a real remote daemon. Shared builds are static-only on Windows (no symbol-export macros yet).
v0.1.0-alpha.1
Second alpha of the native C++ Testcontainers port. Highlights since v0.1.0-alpha.0:
Docker client
- Engine API version pinning — docker-CLI-style negotiation (
min(1.44, daemon's Api-Version)from one unversionedGET /_ping); every typed client call now hits/v1.NN/...paths, so a Docker 29+ daemon that drops old API versions keeps working. The rawrequest()escape hatch stays unversioned by design. - Streaming log wait —
wait_for::log(...)rides ONE deadline-boundedfollow_logsconnection (history + live) instead of snapshot polling every 200 ms; occurrence counting is incremental and chunk-boundary-safe. New publicfollow_logsoverload with a hard deadline (FollowEnd::{ConsumerStopped,StreamEnded,DeadlineExpired}). - Host port exposure —
with_exposed_host_portmakes services listening on the test host reachable from containers through ansshdsidecar + libssh2 remote-forward tunnel. - Exec now always hijacks with
Upgrade: tcp(CLI parity); named-pipe transport treats a peer CloseWrite as EOF.
Packaging (new)
conan create .produces a consumer-grade package:test_package/proves find_package + link + run; the recipe runs the unit suite unlesstools.build:skip_test.cmake --installships a workingtestcontainersConfig.cmake(find_package(testcontainers)→testcontainers::testcontainers) for non-Conan consumers.- The version lives in ONE place (
TC_VERSION_FULLin CMakeLists.txt);sharedis refused on Windows (no export macros yet).
Quality gates
- CI grew clang-format / clang-tidy (pinned) / ASan+UBSan / CodeQL gates, warnings-as-errors on project targets, and a
conan createjob on Linux, Windows and macOS — apple-clang compiles and runs the unit suite on every push now. - The Windows-engine integration mirror covers the docs/07 matrix; hang dumps are captured automatically.
Full log: v0.1.0-alpha.0...v0.1.0-alpha.1
v0.1.0-alpha.0 — first public alpha
First public alpha of testcontainers-cpp — a native C++20 port of Testcontainers: spin up real Docker containers from your integration tests and tear them down automatically. No docker CLI, no Rust bridge — a pure C++ client talking to the Docker Engine HTTP API directly (Boost.Beast/Asio).
Highlights
- Containers:
GenericImage→Containerlifecycle with typed config (env, ports, mounts, networks, labels, entrypoint, workdir/user, tty, host config patches), lifecycle hooks, startup retry, and container reuse. - Wait strategies: log, HTTP, health, port, exit, and composite waits with per-strategy deadlines.
- Exec: stdin (HTTP 101 upgrade + half-close), tty, streaming output, exit codes.
- Images: pull with registry auth (config.json incl. credential helpers), build from Dockerfile (
GenericBuildableImage), image substitution. - Networks & volumes: create/inspect/remove, IPAM basics, volume seeding.
- Copy: to/from container (tar over the API, no CLI).
- Docker Compose: local (host CLI), containerised (
docker:cliambassador), and auto client modes; per-service wait + TCP probes. - Ryuk: crash-safe reaping via the standard
testcontainers/ryuksidecar. - Transports: Windows named pipe (message-mode half-close), unix socket, TCP, TLS (client certs; end-to-end TLS against a remote daemon is the main unverified gap).
Platforms & CI
Green on Windows (named pipe, MSVC) and Linux (unix socket, gcc): ~300 unit tests + ~65 integration tests against a real daemon. CI runs the full integration suite on Linux and the unit suite on Windows for every push.
Status
Alpha: the core is feature-complete, but the API may still change. Known limits per subsystem are documented in docs/06-feature-notes.md; the actionable backlog lives in TODO.md.
Licensed under MIT or Apache-2.0, at your option.