Packages
Build custom Docker images from Dockerfile and use them as typed formulas in testcontainer.
Current section
Files
Jump to
Current section
Files
src/testcontainer_dockerfile.erl
-module(testcontainer_dockerfile).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/testcontainer_dockerfile.gleam").
-export([new/1, with_context/2, with_build_arg/3, with_timeout/2, container/1, with_expose_port/2, with_expose_ports/2, with_env/3, with_envs/2, with_secret_env/3, with_label/3, with_wait/2, with_extra_wait/2, with_command/2, with_entrypoint/2, with_name/2, with_network/2, with_bind_mount/3, with_readonly_bind/3, with_tmpfs/2, with_privileged/1, formula/1, image_id/1]).
-export_type([dockerfile_config/0, docker_image/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Build custom Docker images from a Dockerfile and use them as typed\n"
" formulas with [`testcontainer`](https://hex.pm/packages/testcontainer).\n"
"\n"
" `testcontainer` is a container runner: it does not build images.\n"
" This package fills the gap by shelling out to `docker build` and\n"
" wrapping the resulting image in a `Formula`.\n"
).
-opaque dockerfile_config() :: {dockerfile_config,
binary(),
binary(),
list({binary(), binary()}),
list(fun((testcontainer@container:container_spec()) -> testcontainer@container:container_spec())),
integer()}.
-opaque docker_image() :: {docker_image,
testcontainer@container:container(),
binary()}.
-file("src/testcontainer_dockerfile.gleam", 52).
?DOC(
" Start a new Dockerfile build configuration.\n"
"\n"
" `dockerfile_path` should point to a readable Dockerfile, e.g.\n"
" `\"./Dockerfile\"` or `\"docker/api.Dockerfile\"`. The path is trimmed\n"
" before being passed to `docker build -f`.\n"
).
-spec new(binary()) -> dockerfile_config().
new(Dockerfile_path) ->
{dockerfile_config, Dockerfile_path, <<"."/utf8>>, [], [], 600000}.
-file("src/testcontainer_dockerfile.gleam", 64).
?DOC(
" Set the build context (sent to the Docker daemon as the build root).\n"
" Defaults to the current directory (`.`).\n"
).
-spec with_context(dockerfile_config(), binary()) -> dockerfile_config().
with_context(Cfg, Context_path) ->
{dockerfile_config,
erlang:element(2, Cfg),
Context_path,
erlang:element(4, Cfg),
erlang:element(5, Cfg),
erlang:element(6, Cfg)}.
-file("src/testcontainer_dockerfile.gleam", 78).
?DOC(
" Add a `--build-arg` flag passed to `docker build`. Use for `ARG`\n"
" directives inside the Dockerfile.\n"
"\n"
" Note: build args are visible in the host's process list (`ps aux`)\n"
" during the build window. Do **not** pass real secrets via build args;\n"
" use [`with_secret_env`](#with_secret_env) for runtime secrets, or\n"
" Docker BuildKit secret mounts for build-time secrets.\n"
).
-spec with_build_arg(dockerfile_config(), binary(), binary()) -> dockerfile_config().
with_build_arg(Cfg, Key, Value) ->
Args = lists:append(erlang:element(4, Cfg), [{Key, Value}]),
{dockerfile_config,
erlang:element(2, Cfg),
erlang:element(3, Cfg),
Args,
erlang:element(5, Cfg),
erlang:element(6, Cfg)}.
-file("src/testcontainer_dockerfile.gleam", 89).
?DOC(
" Override the default 10-minute build timeout. Clamped to the inclusive\n"
" range `[1000, max_timeout_ms]`.\n"
).
-spec with_timeout(dockerfile_config(), integer()) -> dockerfile_config().
with_timeout(Cfg, Ms) ->
Clamped = case {Ms < 1000, Ms > 3600000} of
{true, _} ->
1000;
{false, true} ->
3600000;
{false, false} ->
Ms
end,
{dockerfile_config,
erlang:element(2, Cfg),
erlang:element(3, Cfg),
erlang:element(4, Cfg),
erlang:element(5, Cfg),
Clamped}.
-file("src/testcontainer_dockerfile.gleam", 287).
?DOC(
" Get the running `Container` so you can use `container.host_port`,\n"
" `container.host`, `container.mapped_url`, etc.\n"
).
-spec container(docker_image()) -> testcontainer@container:container().
container(Img) ->
erlang:element(2, Img).
-file("src/testcontainer_dockerfile.gleam", 464).
-spec add_transform(
dockerfile_config(),
fun((testcontainer@container:container_spec()) -> testcontainer@container:container_spec())
) -> dockerfile_config().
add_transform(Cfg, T) ->
{dockerfile_config,
erlang:element(2, Cfg),
erlang:element(3, Cfg),
erlang:element(4, Cfg),
lists:append(erlang:element(5, Cfg), [T]),
erlang:element(6, Cfg)}.
-file("src/testcontainer_dockerfile.gleam", 103).
?DOC(" Map a port that should be exposed when the container runs.\n").
-spec with_expose_port(dockerfile_config(), testcontainer@port:port_()) -> dockerfile_config().
with_expose_port(Cfg, P) ->
add_transform(
Cfg,
fun(Spec) -> testcontainer@container:expose_port(Spec, P) end
).
-file("src/testcontainer_dockerfile.gleam", 111).
?DOC(" Map several exposed ports at once.\n").
-spec with_expose_ports(dockerfile_config(), list(testcontainer@port:port_())) -> dockerfile_config().
with_expose_ports(Cfg, Ports) ->
add_transform(
Cfg,
fun(Spec) -> testcontainer@container:expose_ports(Spec, Ports) end
).
-file("src/testcontainer_dockerfile.gleam", 120).
?DOC(
" Add an environment variable injected at container start. Overrides\n"
" any `ENV` baked into the image.\n"
).
-spec with_env(dockerfile_config(), binary(), binary()) -> dockerfile_config().
with_env(Cfg, Key, Value) ->
add_transform(
Cfg,
fun(Spec) -> testcontainer@container:with_env(Spec, Key, Value) end
).
-file("src/testcontainer_dockerfile.gleam", 129).
?DOC(" Add many env vars at once.\n").
-spec with_envs(dockerfile_config(), list({binary(), binary()})) -> dockerfile_config().
with_envs(Cfg, Pairs) ->
add_transform(
Cfg,
fun(Spec) -> testcontainer@container:with_envs(Spec, Pairs) end
).
-file("src/testcontainer_dockerfile.gleam", 138).
?DOC(
" Add an env var whose value is wrapped in `cowl.Secret` and never\n"
" appears in `string.inspect` output.\n"
).
-spec with_secret_env(dockerfile_config(), binary(), cowl:secret(binary())) -> dockerfile_config().
with_secret_env(Cfg, Key, Secret) ->
add_transform(
Cfg,
fun(Spec) ->
testcontainer@container:with_secret_env(Spec, Key, Secret)
end
).
-file("src/testcontainer_dockerfile.gleam", 147).
?DOC(" Add a Docker label to the running container.\n").
-spec with_label(dockerfile_config(), binary(), binary()) -> dockerfile_config().
with_label(Cfg, Key, Value) ->
add_transform(
Cfg,
fun(Spec) -> testcontainer@container:with_label(Spec, Key, Value) end
).
-file("src/testcontainer_dockerfile.gleam", 157).
?DOC(
" Set the wait strategy used after the container starts. Replaces any\n"
" previous wait strategy.\n"
).
-spec with_wait(dockerfile_config(), testcontainer@wait:wait_strategy()) -> dockerfile_config().
with_wait(Cfg, Strategy) ->
add_transform(
Cfg,
fun(Spec) -> testcontainer@container:wait_for(Spec, Strategy) end
).
-file("src/testcontainer_dockerfile.gleam", 167).
?DOC(
" Combine an additional wait strategy with whatever is already configured\n"
" using `wait.all_of`. Useful to layer e.g. `wait.health_check()` on top\n"
" of a base port wait.\n"
).
-spec with_extra_wait(dockerfile_config(), testcontainer@wait:wait_strategy()) -> dockerfile_config().
with_extra_wait(Cfg, Extra) ->
add_transform(
Cfg,
fun(Spec) ->
Current = testcontainer@container:wait_strategy(Spec),
testcontainer@container:wait_for(
Spec,
testcontainer@wait:all_of([Current, Extra])
)
end
).
-file("src/testcontainer_dockerfile.gleam", 178).
?DOC(" Override the image's CMD when running the container.\n").
-spec with_command(dockerfile_config(), list(binary())) -> dockerfile_config().
with_command(Cfg, Cmd) ->
add_transform(
Cfg,
fun(Spec) -> testcontainer@container:with_command(Spec, Cmd) end
).
-file("src/testcontainer_dockerfile.gleam", 186).
?DOC(" Override the image's ENTRYPOINT.\n").
-spec with_entrypoint(dockerfile_config(), list(binary())) -> dockerfile_config().
with_entrypoint(Cfg, Ep) ->
add_transform(
Cfg,
fun(Spec) -> testcontainer@container:with_entrypoint(Spec, Ep) end
).
-file("src/testcontainer_dockerfile.gleam", 194).
?DOC(" Set a fixed container name.\n").
-spec with_name(dockerfile_config(), binary()) -> dockerfile_config().
with_name(Cfg, Name) ->
add_transform(
Cfg,
fun(Spec) -> testcontainer@container:with_name(Spec, Name) end
).
-file("src/testcontainer_dockerfile.gleam", 199).
?DOC(" Attach the container to a Docker network by name.\n").
-spec with_network(dockerfile_config(), binary()) -> dockerfile_config().
with_network(Cfg, Network) ->
add_transform(
Cfg,
fun(Spec) -> testcontainer@container:on_network(Spec, Network) end
).
-file("src/testcontainer_dockerfile.gleam", 207).
?DOC(" Bind-mount a host path read-write into the container.\n").
-spec with_bind_mount(dockerfile_config(), binary(), binary()) -> dockerfile_config().
with_bind_mount(Cfg, Host_path, Container_path) ->
add_transform(
Cfg,
fun(Spec) ->
testcontainer@container:with_bind_mount(
Spec,
Host_path,
Container_path
)
end
).
-file("src/testcontainer_dockerfile.gleam", 218).
?DOC(" Bind-mount a host path read-only into the container.\n").
-spec with_readonly_bind(dockerfile_config(), binary(), binary()) -> dockerfile_config().
with_readonly_bind(Cfg, Host_path, Container_path) ->
add_transform(
Cfg,
fun(Spec) ->
testcontainer@container:with_readonly_bind(
Spec,
Host_path,
Container_path
)
end
).
-file("src/testcontainer_dockerfile.gleam", 229).
?DOC(" Mount an in-memory tmpfs at the given container path.\n").
-spec with_tmpfs(dockerfile_config(), binary()) -> dockerfile_config().
with_tmpfs(Cfg, Container_path) ->
add_transform(
Cfg,
fun(Spec) ->
testcontainer@container:with_tmpfs(Spec, Container_path)
end
).
-file("src/testcontainer_dockerfile.gleam", 240).
?DOC(
" Run the container as privileged. **Use with extreme caution**: a\n"
" privileged container has full access to host devices and can escape\n"
" most isolation. Naming alone (`with_privileged`) is the security\n"
" signal — there is no runtime warning.\n"
).
-spec with_privileged(dockerfile_config()) -> dockerfile_config().
with_privileged(Cfg) ->
add_transform(Cfg, fun testcontainer@container:with_privileged/1).
-file("src/testcontainer_dockerfile.gleam", 474).
-spec apply_transforms(
testcontainer@container:container_spec(),
list(fun((testcontainer@container:container_spec()) -> testcontainer@container:container_spec()))
) -> testcontainer@container:container_spec().
apply_transforms(Spec, Transforms) ->
gleam@list:fold(Transforms, Spec, fun(S, T) -> T(S) end).
-file("src/testcontainer_dockerfile.gleam", 489).
-spec build_dockerfile(dockerfile_config()) -> {ok, binary()} |
{error, testcontainer_dockerfile@error:error()}.
build_dockerfile(Cfg) ->
case testcontainer_dockerfile_ffi:run_docker_build(
erlang:element(2, Cfg),
erlang:element(3, Cfg),
erlang:element(4, Cfg),
erlang:element(6, Cfg)
) of
{ok, Image_id} ->
{ok, Image_id};
{error, <<"DOCKER_NOT_FOUND"/utf8>>} ->
{error, docker_not_found};
{error, Reason} ->
{error, {build_failed, erlang:element(2, Cfg), Reason}}
end.
-file("src/testcontainer_dockerfile.gleam", 373).
-spec has_unsafe_char(binary()) -> boolean().
has_unsafe_char(S) ->
(gleam_stdlib:contains_string(S, <<"\x{0000}"/utf8>>) orelse gleam_stdlib:contains_string(
S,
<<"\n"/utf8>>
))
orelse gleam_stdlib:contains_string(S, <<"\r"/utf8>>).
-file("src/testcontainer_dockerfile.gleam", 453).
-spec is_digit(binary()) -> boolean().
is_digit(C) ->
case C of
<<"0"/utf8>> ->
true;
<<"1"/utf8>> ->
true;
<<"2"/utf8>> ->
true;
<<"3"/utf8>> ->
true;
<<"4"/utf8>> ->
true;
<<"5"/utf8>> ->
true;
<<"6"/utf8>> ->
true;
<<"7"/utf8>> ->
true;
<<"8"/utf8>> ->
true;
<<"9"/utf8>> ->
true;
_ ->
false
end.
-file("src/testcontainer_dockerfile.gleam", 395).
-spec is_alpha(binary()) -> boolean().
is_alpha(C) ->
case C of
<<"a"/utf8>> ->
true;
<<"b"/utf8>> ->
true;
<<"c"/utf8>> ->
true;
<<"d"/utf8>> ->
true;
<<"e"/utf8>> ->
true;
<<"f"/utf8>> ->
true;
<<"g"/utf8>> ->
true;
<<"h"/utf8>> ->
true;
<<"i"/utf8>> ->
true;
<<"j"/utf8>> ->
true;
<<"k"/utf8>> ->
true;
<<"l"/utf8>> ->
true;
<<"m"/utf8>> ->
true;
<<"n"/utf8>> ->
true;
<<"o"/utf8>> ->
true;
<<"p"/utf8>> ->
true;
<<"q"/utf8>> ->
true;
<<"r"/utf8>> ->
true;
<<"s"/utf8>> ->
true;
<<"t"/utf8>> ->
true;
<<"u"/utf8>> ->
true;
<<"v"/utf8>> ->
true;
<<"w"/utf8>> ->
true;
<<"x"/utf8>> ->
true;
<<"y"/utf8>> ->
true;
<<"z"/utf8>> ->
true;
<<"A"/utf8>> ->
true;
<<"B"/utf8>> ->
true;
<<"C"/utf8>> ->
true;
<<"D"/utf8>> ->
true;
<<"E"/utf8>> ->
true;
<<"F"/utf8>> ->
true;
<<"G"/utf8>> ->
true;
<<"H"/utf8>> ->
true;
<<"I"/utf8>> ->
true;
<<"J"/utf8>> ->
true;
<<"K"/utf8>> ->
true;
<<"L"/utf8>> ->
true;
<<"M"/utf8>> ->
true;
<<"N"/utf8>> ->
true;
<<"O"/utf8>> ->
true;
<<"P"/utf8>> ->
true;
<<"Q"/utf8>> ->
true;
<<"R"/utf8>> ->
true;
<<"S"/utf8>> ->
true;
<<"T"/utf8>> ->
true;
<<"U"/utf8>> ->
true;
<<"V"/utf8>> ->
true;
<<"W"/utf8>> ->
true;
<<"X"/utf8>> ->
true;
<<"Y"/utf8>> ->
true;
<<"Z"/utf8>> ->
true;
_ ->
false
end.
-file("src/testcontainer_dockerfile.gleam", 391).
-spec is_valid_arg_char(binary()) -> boolean().
is_valid_arg_char(C) ->
(is_alpha(C) orelse is_digit(C)) orelse (C =:= <<"_"/utf8>>).
-file("src/testcontainer_dockerfile.gleam", 387).
-spec is_valid_first_char(binary()) -> boolean().
is_valid_first_char(C) ->
is_alpha(C) orelse (C =:= <<"_"/utf8>>).
-file("src/testcontainer_dockerfile.gleam", 379).
-spec is_valid_build_arg_key(binary()) -> boolean().
is_valid_build_arg_key(Key) ->
case gleam@string:to_graphemes(Key) of
[] ->
false;
[First | Rest] ->
is_valid_first_char(First) andalso gleam@list:all(
Rest,
fun is_valid_arg_char/1
)
end.
-file("src/testcontainer_dockerfile.gleam", 344).
-spec validate_build_args(list({binary(), binary()})) -> {ok, nil} |
{error, testcontainer_dockerfile@error:error()}.
validate_build_args(Args) ->
case Args of
[] ->
{ok, nil};
[{Key, Value} | Rest] ->
case is_valid_build_arg_key(Key) of
false ->
{error,
{build_failed,
<<""/utf8>>,
<<<<"invalid build-arg key: "/utf8, Key/binary>>/binary,
" (must match [A-Za-z_][A-Za-z0-9_]*)"/utf8>>}};
true ->
case has_unsafe_char(Value) of
true ->
{error,
{build_failed,
<<""/utf8>>,
<<<<"build-arg value for "/utf8,
Key/binary>>/binary,
" contains NUL or CR/LF characters"/utf8>>}};
false ->
validate_build_args(Rest)
end
end
end.
-file("src/testcontainer_dockerfile.gleam", 336).
-spec validate_dockerfile_is_file(binary()) -> {ok, nil} |
{error, testcontainer_dockerfile@error:error()}.
validate_dockerfile_is_file(Path) ->
case fio:is_file(Path) of
{ok, true} ->
{ok, nil};
{ok, false} ->
{error, {dockerfile_not_found, Path}};
{error, _} ->
{error, {dockerfile_not_found, Path}}
end.
-file("src/testcontainer_dockerfile.gleam", 313).
-spec validate_path_string(binary(), binary()) -> {ok, nil} |
{error, testcontainer_dockerfile@error:error()}.
validate_path_string(Path, Kind) ->
case Path of
<<""/utf8>> ->
case Kind of
<<"dockerfile"/utf8>> ->
{error, {dockerfile_not_found, <<""/utf8>>}};
_ ->
{error,
{build_failed, Path, <<"context path is empty"/utf8>>}}
end;
_ ->
case has_unsafe_char(Path) of
true ->
{error,
{build_failed,
Path,
<<Kind/binary,
" path contains NUL or CR/LF characters"/utf8>>}};
false ->
{ok, nil}
end
end.
-file("src/testcontainer_dockerfile.gleam", 295).
-spec validate_and_normalize(dockerfile_config()) -> {ok, dockerfile_config()} |
{error, testcontainer_dockerfile@error:error()}.
validate_and_normalize(Cfg) ->
Trimmed_dockerfile = gleam@string:trim(erlang:element(2, Cfg)),
Trimmed_context = gleam@string:trim(erlang:element(3, Cfg)),
gleam@result:'try'(
validate_path_string(Trimmed_dockerfile, <<"dockerfile"/utf8>>),
fun(_) ->
gleam@result:'try'(
validate_path_string(Trimmed_context, <<"context"/utf8>>),
fun(_) ->
gleam@result:'try'(
validate_dockerfile_is_file(Trimmed_dockerfile),
fun(_) ->
gleam@result:'try'(
validate_build_args(erlang:element(4, Cfg)),
fun(_) ->
{ok,
{dockerfile_config,
Trimmed_dockerfile,
Trimmed_context,
erlang:element(4, Cfg),
erlang:element(5, Cfg),
erlang:element(6, Cfg)}}
end
)
end
)
end
)
end
).
-file("src/testcontainer_dockerfile.gleam", 262).
?DOC(
" Run `docker build` and return a `Formula` that wraps the resulting\n"
" image. The build happens eagerly when this function is called.\n"
"\n"
" On success, the returned `Formula` can be passed straight to\n"
" `testcontainer.with_formula` (see the README for an end-to-end example).\n"
"\n"
" Failure modes (`error.Error` variants):\n"
" - `DockerNotFound` — the `docker` CLI is not on PATH\n"
" - `DockerfileNotFound(path)` — the configured Dockerfile path is\n"
" empty, contains invalid characters, points to a directory, or does\n"
" not exist on disk\n"
" - `BuildFailed(path, reason)` — `docker build` returned non-zero\n"
" or invalid build-arg keys were rejected. `reason` carries full\n"
" stdout+stderr for diagnostics\n"
).
-spec formula(dockerfile_config()) -> {ok,
testcontainer@formula:formula(docker_image())} |
{error, testcontainer_dockerfile@error:error()}.
formula(Cfg) ->
gleam@result:'try'(
validate_and_normalize(Cfg),
fun(Trimmed_cfg) ->
gleam@result:'try'(
build_dockerfile(Trimmed_cfg),
fun(Image_id) ->
Spec = apply_transforms(
testcontainer@container:new(Image_id),
erlang:element(5, Trimmed_cfg)
),
{ok,
testcontainer@formula:new(
Spec,
fun(Running) ->
{ok, {docker_image, Running, Image_id}}
end
)}
end
)
end
).
-file("src/testcontainer_dockerfile.gleam", 281).
?DOC(" Get the Docker image ID (e.g. `sha256:abcd...`) of a built image.\n").
-spec image_id(docker_image()) -> binary().
image_id(Img) ->
erlang:element(3, Img).