Build custom Docker images from a
Dockerfileand use them as typed formulas withtestcontainer. Pago handles the build; you get the container.
testcontainer is a container runner: it does not build images.
This package fills the build-on-test gap by shelling out to
docker build and wrapping the resulting image in a Formula.
let cfg =
testcontainer_dockerfile.new("./Dockerfile")
|> testcontainer_dockerfile.with_context(".")
|> testcontainer_dockerfile.with_build_arg("BUILD_VERSION", "dev")
|> testcontainer_dockerfile.with_expose_port(port.tcp(3000))
|> testcontainer_dockerfile.with_env("LOG_LEVEL", "info")
|> testcontainer_dockerfile.with_wait(wait.health_check())
let assert Ok(formula) = testcontainer_dockerfile.formula(cfg)
use container <- testcontainer.with_formula(formula)
// the image is built once, the container is created from it,
// and torn down on scope exit.- 🛠 Build-on-test: real
docker builddriven from your test code, layers and all - 🔒 Type-safe pipeline:
with_*builders compose the resulting spec, applied after build - 🩺 Validation upfront: missing files, invalid build-arg keys, and unsafe characters caught before invoking Docker
- ⏱ Configurable timeout: 10-minute default, override via
with_timeout(ms) - 📦 Plays with the ecosystem: builds images that flow into
testcontainer.with_formulalike any other formula
gleam add testcontainer_dockerfilenew(path)— start with a Dockerfile pathwith_context(cfg, path)— build context (default.)with_build_arg(cfg, key, value)— passed as--build-argto docker buildwith_timeout(cfg, ms)— override the 10-minute build timeout
Mirror testcontainer/container builders, but on the built image:
with_expose_port(cfg, port)with_env(cfg, key, value)with_label(cfg, key, value)with_wait(cfg, strategy)with_command(cfg, cmd)/with_entrypoint(cfg, ep)with_name(cfg, name)/with_network(cfg, name)
formula(cfg) -> Result(Formula(DockerImage), error.Error)— runsdocker buildeagerly; pass the returnedFormulatotestcontainer.with_formula.
pub type Error {
DockerNotFound
DockerfileNotFound(path: String)
BuildFailed(path: String, reason: String)
}BuildFailed.reason includes full docker build output (stdout +
stderr merged), so you see exactly which RUN step broke.
docker build --quiet --no-cache -f <path> [--build-arg ...] <context>
is invoked via erlang:open_port with {spawn_executable, ...} (no
shell, no escape risk). With --quiet, Docker prints only the final
image ID on stdout — that's what formula/1 returns.
After build:
- The image ID becomes a regular
container.new(image_id)spec. - Any
with_expose_port,with_env,with_waitetc. accumulated onDockerfileConfigare applied to that spec via a chain of transform functions. - The configured spec is wrapped in a
Formula(DockerImage)that returns the image ID as the typed output.
| Workflow | Tool |
|---|---|
Pre-built public image (postgres:16, redis:7) |
testcontainer core, container.new |
| Custom image, pre-built in CI | testcontainer core with image tag |
| Custom image, built inline at test time | this package |
docker-compose.yml orchestrating multiple services |
testcontainer_compose |
gleam run -m testcontainer_dockerfile_dev # Dev runner (needs Docker)
gleam test # Unit tests (no Docker)
TESTCONTAINERS_INTEGRATION=true gleam test # Integration tests (Docker required)testcontainer— the core container runnertestcontainer_formulas— pre-built formulas for Postgres / Redis / MySQL / RabbitMQ / Mongotestcontainer_compose— spin updocker-compose.ymlstackstestcontainer_formulas_builder— visual builder + codegen, also accepts Dockerfile / compose uploads
Full API docs: https://hexdocs.pm/testcontainer_dockerfile
MIT.
