<p align="center">
<img src="https://raw.githubusercontent.com/lupodevelop/testcontainer_compose/main/assets/img/logo.png" alt="Pago, the paguro mascot, carrying a Docker container on his shell" width="220">
</p>
<h1 align="center">testcontainer_compose</h1>
<p align="center">
<a href="https://hex.pm/packages/testcontainer_compose"><img src="https://img.shields.io/hexpm/v/testcontainer_compose?color=ffaff3" alt="Hex Package"></a>
<a href="https://hexdocs.pm/testcontainer_compose/"><img src="https://img.shields.io/badge/hex-docs-ffaff3" alt="Hex Docs"></a>
<a href="https://github.com/lupodevelop/testcontainer_compose/actions/workflows/test.yml"><img src="https://github.com/lupodevelop/testcontainer_compose/actions/workflows/test.yml/badge.svg" alt="CI"></a>
<a href="https://github.com/lupodevelop/testcontainer_compose/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="License: MIT"></a>
<a href="https://gleam.run"><img src="https://img.shields.io/badge/made%20with-gleam-ffaff3?logo=gleam" alt="Made with Gleam"></a>
</p>
> Spin up `docker-compose.yml` stacks from your Gleam tests.
> Pago hands you typed services; the lifecycle (up + body + down) runs automatically.
Companion to [`testcontainer`](https://hex.pm/packages/testcontainer):
use it when you have an existing compose file and want it as the source
of truth at test time, with typed access to parsed services.
```gleam
let cfg =
testcontainer_compose.from_file("./docker-compose.yml")
|> testcontainer_compose.with_project_name("test_42")
|> testcontainer_compose.with_env_override("PG_PASS", "secret")
use stack <- testcontainer_compose.with_compose(cfg)
// Inspect typed service info
case testcontainer_compose.service_by_name(stack, "postgres") {
option.Some(svc) -> {
let _image = testcontainer_compose.service_image(svc)
let _ports = testcontainer_compose.service_ports(svc)
Ok(Nil)
}
option.None -> Ok(Nil)
}
```
`with_compose/2` runs `docker compose up -d --wait`, calls your body,
then always tears the stack down with `docker compose down --volumes
--remove-orphans`, even if the body returns an error. If the body succeeds
but teardown fails, the teardown error is surfaced so leaked stacks are never
silent.
## Why use it
- 🐚 **Native multi-container**: uses Docker's own dependency
resolution and health-wait
- 🔒 **Type-safe services**: parsed into `Service` records with
`name`, `image`, `ports`
- 🩺 **Reliable YAML handling**: delegates parsing to
`docker compose config --format json`, so anchors, includes and
`extends:` all work
- ⏱ **Hard-cleanup guarantee**: `down --volumes --remove-orphans` runs
even on body failure
- 🔐 **Secret-safe overrides**: `with_env_override` wraps values in
`cowl.Secret`
## Install
```sh
gleam add testcontainer_compose@1
```
## API
### Builder
- `from_file(path)` → `ComposeConfig`
- `with_project_name(cfg, name)` → unique project per test run
- `with_env_override(cfg, key, value)` → values wrapped in
`cowl.Secret` (no leak via `string.inspect`)
### Lifecycle
- `with_compose(cfg, body)` — up + body + down (always). Teardown failure
surfaced when body succeeds.
- `formula(cfg)` — returns a `StandaloneFormula(ComposeServices, Error)`.
Pass it to `testcontainer.with_standalone_formula/2` to get the same up +
body + down guarantee with a typed output:
```gleam
let assert Ok(f) =
testcontainer_compose.from_file("./docker-compose.yml")
|> testcontainer_compose.with_project_name("test_42")
|> testcontainer_compose.formula()
use stack <- testcontainer.with_standalone_formula(f)
```
### Service inspection
- `services(stack)` → `List(Service)`
- `service_by_name(stack, name)` → `Option(Service)`
- `service_name(svc)` / `service_image(svc)` / `service_ports(svc)`
### Parser (testable without Docker)
- `parse_services_json(path, json_str)`: feed it the output of
`docker compose config --format json` and validate parsing.
### Errors (`testcontainer_compose/error`)
```gleam
pub type Error {
ComposeFileNotFound(path: String)
InvalidYaml(path: String, reason: String)
ComposeFailed(path: String, reason: String)
}
```
`ComposeFailed.reason` carries the exit-code prefix (e.g.
`"docker daemon error: ..."`) plus full stderr/stdout, so diagnostics
never get silently swallowed.
## How it works
`testcontainer_compose` shells out to the Docker CLI for two reasons:
1. `docker compose config --format json` lets Docker handle YAML
parsing, anchors, env-var interpolation, includes, and `extends:`
inheritance. More reliable than re-implementing the spec.
2. `docker compose up/down` reuses Docker's own dependency resolution
and health-wait logic.
The Erlang FFI uses `erlang:open_port` with a 60-second timeout and
explicit exit-code mapping (125 = daemon error, 126 = no permissions,
127 = not found).
## When to use this vs alternatives
| Scenario | Tool |
| --- | --- |
| Pre-built public image, single container | `testcontainer` core, `container.new` |
| Multi-service Gleam-defined stack | `testcontainer.with_network` + multiple `with_container` |
| Existing `docker-compose.yml`, source of truth | **this package** |
| Compose file rarely changes | [Formula Builder](https://github.com/lupodevelop/testcontainer_formulas_builder) → codegen native |
| Custom image built inline | [`testcontainer_dockerfile`](https://hex.pm/packages/testcontainer_dockerfile) |
## Development
```sh
gleam run -m testcontainer_compose_dev # Dev runner (needs Docker)
gleam test # Unit tests (no Docker)
TESTCONTAINERS_INTEGRATION=true gleam test # Integration tests (Docker required)
```
## See also
- [`testcontainer`](https://github.com/lupodevelop/testcontainer): core container runner
- [`testcontainer_formulas`](https://github.com/lupodevelop/testcontainer_formulas): pre-built formulas for Postgres, Redis, MySQL, RabbitMQ, Mongo
- [`testcontainer_dockerfile`](https://github.com/lupodevelop/testcontainer_dockerfile): build custom images from a Dockerfile at test time
- [`testcontainer_formulas_builder`](https://github.com/lupodevelop/testcontainer_formulas_builder): visual builder + codegen, also accepts Dockerfile / compose uploads
Full API docs: <https://hexdocs.pm/testcontainer_compose>
## License
[MIT](LICENSE).