README.md

<p align="center">
  <img src="https://raw.githubusercontent.com/lupodevelop/testcontainer_dockerfile/main/assets/img/logo.png" alt="Pago, the paguro mascot, carrying a Docker container on his shell" width="220">
</p>

<h1 align="center">testcontainer_dockerfile</h1>

<p align="center">
  <a href="https://hex.pm/packages/testcontainer_dockerfile"><img src="https://img.shields.io/hexpm/v/testcontainer_dockerfile?color=ffaff3" alt="Hex Package"></a>
  <a href="https://hexdocs.pm/testcontainer_dockerfile/"><img src="https://img.shields.io/badge/hex-docs-ffaff3" alt="Hex Docs"></a>
  <a href="https://github.com/lupodevelop/testcontainer_dockerfile/actions/workflows/test.yml"><img src="https://github.com/lupodevelop/testcontainer_dockerfile/actions/workflows/test.yml/badge.svg" alt="CI"></a>
  <a href="https://github.com/lupodevelop/testcontainer_dockerfile/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>

> Build custom Docker images from a `Dockerfile` and use them as typed
> formulas with [`testcontainer`](https://hex.pm/packages/testcontainer).
> 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`.

```gleam
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.
```

## Why use it

- 🛠 **Build-on-test**: real `docker build` driven 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_formula` like any other formula

## Install

```sh
gleam add testcontainer_dockerfile@1
```

## API

### Build configuration

- `new(path)` — start with a Dockerfile path
- `with_context(cfg, path)` — build context (default `.`)
- `with_build_arg(cfg, key, value)` — passed as `--build-arg` to docker build
- `with_timeout(cfg, ms)` — override the 10-minute build timeout

### Container shape (applied AFTER build)

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)`

### Lifecycle

- `formula(cfg) -> Result(Formula(DockerImage), error.Error)` — runs
  `docker build` eagerly; pass the returned `Formula` to
  `testcontainer.with_formula`.

### Errors (`testcontainer_dockerfile/error`)

```gleam
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.

## How it works

`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:

1. The image ID becomes a regular `container.new(image_id)` spec.
2. Any `with_expose_port`, `with_env`, `with_wait` etc. accumulated on
   `DockerfileConfig` are applied to that spec via a chain of
   transform functions.
3. The configured spec is wrapped in a `Formula(DockerImage)` that
   returns the image ID as the typed output.

## When to use this vs alternatives

| 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`](https://hex.pm/packages/testcontainer_compose) |

## Development

```sh
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)
```

## See also

- [`testcontainer`](https://github.com/lupodevelop/testcontainer) — the
  core container runner
- [`testcontainer_formulas`](https://github.com/lupodevelop/testcontainer_formulas) —
  pre-built formulas for Postgres / Redis / MySQL / RabbitMQ / Mongo
- [`testcontainer_compose`](https://github.com/lupodevelop/testcontainer_compose) —
  spin up `docker-compose.yml` stacks
- [`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_dockerfile>

## License

[MIT](LICENSE).