# testcontainers-rs: integration testing against real containers

> testcontainers-rs is the official Rust library for running integration tests against real Docker containers, with a core crate that abstracts an image and a runner, a blocking API for synchronous tests and an async one for tokio, and a separate community crate of ready-made modules. It suits a Rust service that talks to a database, a queue or a cache and wants those dependencies to be the real thing during the test run.

**testcontainers/testcontainers-rs** — A library for integration-testing against docker containers from within Rust.

- Repository: https://github.com/testcontainers/testcontainers-rs
- Website: https://rust.testcontainers.org
- Stars: 1,138 · Forks: 203
- Language: Rust
- License: Apache-2.0
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/testcontainers-testcontainers-rs

## What the crate is

testcontainers-rs is the official Rust language fork of testcontainers.org, and the library provides an API for working with containers in a test environment.

The idea is the one the wider project has made common. A test that talks to a real Postgres, a real Redis and a real queue is worth more than a test that talks to a mock, because the mock encodes your assumptions about the thing you are testing against. The library makes that possible by starting a container, waiting until it is actually ready, handing you the mapped port, and cleaning it up when the test ends.

The Rust implementation is a library rather than a framework, which is the right shape for the language. There is no test runner plugin, no annotation processor and no build integration to configure. You depend on the crate, you ask for an image, and you use the returned handle.

The licence is unusual in a good way. It is licensed under either of the Apache License 2.0 or the MIT licence, at your option, and the workspace metadata carries the same choice for the whole project.

The current release is 0.28.0 from August 2026, with 0.27.3 in April and 0.27.2 in March, and the last push to the repository was on 2026-10-13. The documentation is published on a dedicated site, rust.testcontainers.org, and the crate documentation is on docs.rs.

## Three steps: depend, implement Image, pick a runner

The usage section describes the model in three steps, and the shape of them explains the whole design.

First, you depend on the testcontainers crate. Second, you implement testcontainers::core::Image for the Docker images you need. Third, you run it with any available runner from testcontainers::runners, with the note to use the blocking feature for the synchronous API.

The second step is where the design differs from the other Testcontainers implementations, and it is worth understanding why. In the language ports that use a runtime specific abstraction, a generic image is a struct with builder methods, and a specific image is a separate struct. Here, Image is a trait, so the generic case and the custom case are the same mechanism, and a specific image is a type that implements the trait rather than a special case in the API.

The practical effect is that there is no separate list of supported databases in the core crate. The crate provides a generic image that takes a name and a tag, and anything more specific is a type you write yourself.

The runners are the third piece, and having several of them is the point. A runner is the thing that knows how to start a container and hand you the result, and the difference between them is whether the call blocks or returns a future, which is a difference in the shape of your test function rather than in the shape of the container.

## The same test, blocking and async

The README gives the same Redis test twice, and the difference between the two versions is the whole ergonomics story of the crate.

The blocking version uses the synchronous runner and lives in a plain test function.

```rust
use testcontainers::{core::{IntoContainerPort, WaitFor}, runners::SyncRunner, GenericImage, ImageExt};

#[test]
fn test_redis() {
    let container = GenericImage::new("redis", "7.2.4")
        .with_exposed_port(6379.tcp())
        .with_wait_for(WaitFor::message_on_stdout("Ready to accept connections"))
        .with_network("bridge")
        .with_env_var("DEBUG", "1")
        .start()
        .expect("Failed to start Redis");
}
```

The async version swaps the runner and awaits the start, and the test function becomes a tokio test.

```rust
use testcontainers::{core::{IntoContainerPort, WaitFor}, runners::AsyncRunner, GenericImage, ImageExt};

#[tokio::test]
async fn test_redis() {
    let container = GenericImage::new("redis", "7.2.4")
        .with_exposed_port(6379.tcp())
        .with_wait_for(WaitFor::message_on_stdout("Ready to accept connections"))
        .with_network("bridge")
        .with_env_var("DEBUG", "1")
        .start()
        .await
        .expect("Failed to start Redis");
}
```

Note what did not change. The image, the exposed port, the wait condition, the network and the environment variable are identical, and the builder chain is the same method calls. Only the runner import and the await change. That is what the trait based design buys, and it is why a codebase with both kinds of tests does not need two APIs.

The port syntax is also worth a second look, because 6379.tcp() produces a typed port rather than a bare integer, and that is what lets the library know which container port to map and hand you a host port that is genuinely free.

## Waiting for readiness, and the options on the image

The wait condition is the part of a Testcontainers style library that decides whether your test is flaky.

A container process can be running for several seconds before the service inside it accepts connections. If the test connects too early, it fails, and the failure looks like a bug in your code. The wait condition removes that race, and the example uses a message on standard output, which is how most images signal readiness.

The builder options in the example map to the things you actually need from a container. The exposed port says which port to publish, and the library maps it to a free host port. The wait condition says when the container is ready. The network puts the container on the bridge network, which matters when a test needs two containers to talk to each other. And the environment variable is passed into the container.

The generic image takes a name and a tag, so any image on a registry works, including a private one. The specific things, such as a container that needs a volume mounted, or one with a custom entrypoint, are where implementing the Image trait earns its place, and the README points at the trait as the second of the three steps for exactly that reason.

The cleanup is implicit, since the container handle is dropped when the test ends and the library removes the container, which is what makes a per test container safe to use in a suite where tests run in parallel.

## Ready-made images live in a separate crate

The easiest way to use the library, as the README puts it, is to depend on ready-to-use images, also known as modules. Those modules are available as a community-maintained crate, testcontainers-modules, with the repository linked as the community modules repository.

The split is deliberate and it is a good one. The core crate stays small, because everything in it has to be maintained by the project itself, while the module crate is where the community adds an image per database, per broker and per service, and where the churn belongs. A new database image is a contribution to the modules crate rather than a change to the core.

It also keeps the dependency graph clean. A project that only needs a Redis container for one test depends on the core crate and one module, not on every image the community has ever written.

The docs site is built with mkdocs, and the pinned requirements show the plugin set, with the material theme, a plugin for including markdown, one for including code and one for extra data. Those plugins are how the guide can embed the same example that appears in the readme, which is the same trick the Spring guides use, and it keeps the documentation from drifting away from the code.

There is a design principles document in the repository root, which is the kind of file that tells you what the maintainers will accept a change for and what they will not.

## The workspace, the toolchain and the limits

The project layout is a Cargo workspace with a single member, the testcontainers crate itself, and two directories excluded from the workspace, the test images that deliberately do not expose a port and the simple web server image. Those exclusions are test fixtures for the library's own behaviour, and keeping them out of the workspace stops a test image from being published or built as part of a normal release.

The workspace metadata sets the edition to 2021 and the minimum Rust version to 1.89, and the three names in the authors list are long standing contributors to the project. The release tooling is release-plz, and the changelog is a file in the root.

The docs are a Python project, with a Pipfile, a pinned runtime file and the requirements file for mkdocs, which is a reminder that a Rust project can still have a Python build step for its documentation.

The practical limits are worth being clear about. The library drives a real Docker daemon, so Docker has to be available wherever the tests run, and in a CI environment that means the runner has to support it. Every test that starts a container pays the start up cost, and that cost is measured in seconds rather than milliseconds, which is why this library is for integration tests and not for the unit test suite.

The alternative is the usual one, a mock or an in-process fake for the dependency, which is fast and has no requirements, and which is the right choice for logic. The case for real containers is the case where the behaviour you care about is the behaviour of the real service, and where a mock has quietly diverged from it.

And there is the middle path, a shared set of containers started once for the whole suite, which is faster than a container per test but leaks state between tests, and which is exactly the problem the per test container exists to solve.

## Conclusion

Adopt testcontainers-rs if your Rust integration tests need a real database, broker or cache, because the containers are created and torn down per test and the alternative, a shared fixture started once, leaks state between tests. Do not adopt it if your test suite has to be fast, since every container start costs seconds, and a unit test with a fake is the right tool for logic. Before you start, check that Docker is available in your CI environment, because the library drives a real daemon, and pick the blocking or the async runner to match how your tests are already written.

## FAQ

### What is testcontainers-rs?

It is the official Rust language fork of testcontainers.org, a library for integration-testing against Docker containers from within Rust. The core crate provides an API for working with containers in a test environment, and it is licensed under either Apache-2.0 or MIT at your option.

### How do I use testcontainers-rs with a specific image?

There are three steps: depend on the testcontainers crate, implement testcontainers::core::Image for the Docker images you need, and run it with a runner from testcontainers::runners, using the blocking feature for the synchronous API. The generic image takes a name and a tag, and the builder chain sets the exposed port, the wait condition, the network and the environment variables.

### What is the difference between the blocking and async APIs?

Only the runner and the await. The blocking version uses the SyncRunner in a plain test function, and the async version uses the AsyncRunner in a tokio test. The image, the port, the wait condition and the rest of the builder chain are identical between them.

### Where do the ready-to-use images for testcontainers-rs come from?

They are provided as modules in a separate community-maintained crate, testcontainers-modules, with its own repository. The core crate stays small that way, and new images are contributed to the modules crate rather than to the core.

## Sources

- [License: Apache-2.0](https://github.com/testcontainers/testcontainers-rs/blob/main/LICENSE)
- [Project website](https://rust.testcontainers.org)
- [README](https://github.com/testcontainers/testcontainers-rs/blob/main/README.md)
- [Releases](https://github.com/testcontainers/testcontainers-rs/releases)
- [testcontainers/testcontainers-rs on GitHub](https://github.com/testcontainers/testcontainers-rs)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/testcontainers-testcontainers-rs
