Julia package for managing docker images and containers, with an aim to make testing with external resources simple and easy.
using Pkg
Pkg.add("Harbor")using Harbor
# pull an image ("name", "name:tag", and "name@sha256:..." all work)
Harbor.pull("alpine")
# list images
Harbor.images()
# run a (long-lived) container
container = Harbor.run!("alpine"; command=["sleep", "600"])
# exec in a container (throws a DockerError with exit code + stderr on failure)
output = Harbor.exec(container, ["sh", "-c", "echo -n hi"])
# fetch its logs
Harbor.logs(container)
# list containers (observed containers are never auto-removed by Harbor)
Harbor.ps()
# stop / start / restart / kill a container
Harbor.stop!(container)
Harbor.start!(container)
Harbor.restart!(container)
Harbor.kill!(container)
# remove a container
Harbor.remove!(container; force=true)
# lifecycle-managed container block: the container is force-removed
# synchronously when the block exits (even on error)
Harbor.with_container("alpine") do container
# ...
endDocker treats rmi name@digest as removal of the underlying image and all local
tags that point to it. Harbor.remove keeps this behavior for digest-pinned
images.
run! and exec pass environment values through a mode-0600 temporary
--env-file. Values do not appear in the Docker command line or alter the
Docker CLI process environment. Container display redacts them. Docker's
line-based env-file format does not support NUL, CR, or LF in names or values.
Map container ports to host ports with ports; use host port 0 to get an
OS-assigned ephemeral port (recommended for CI, where fixed ports collide):
Harbor.with_container("nginx"; ports=Dict(80 => 0)) do container
port = Harbor.host_port(container, 80) # the assigned host port
# connect to 127.0.0.1:port ...
endWhen ports is given, no wait_strategy is specified, and the run is
detached, run! waits until the lowest mapped container port is genuinely
ready before returning (a connection must survive docker's userland proxy,
not merely be accepted by it).
Control readiness via wait_strategy (with wait_timeout/wait_interval):
# wait for a mapped container port to accept connections
Harbor.run!("nginx"; ports=Dict(80 => 0), wait_strategy=(port=80,))
# wait for a log line (String or Regex)
Harbor.run!("postgres:16"; environment=Dict("POSTGRES_PASSWORD" => "pw"),
wait_strategy=(pattern=r"database system is ready",))
# wait for an HTTP response status
Harbor.run!("nginx"; ports=Dict(80 => 8080),
wait_strategy=(url="http://127.0.0.1:8080/", expected_status=200))
# wait for the image's HEALTHCHECK to report healthy
Harbor.run!("myimage"; wait_strategy=(healthy=true,))
# custom check
Harbor.run!("myimage"; wait_strategy=c -> Harbor.is_running(c))If the strategy is not satisfied within wait_timeout seconds, Harbor removes
the container and throws WaitTimeoutError with its logs. If the container
exits first, Harbor removes it and throws ContainerExitedError with its logs.
Containers started by Harbor are labeled org.juliaservices.harbor=true and
force-removed by a GC finalizer as a safety net. For deterministic cleanup use
with_container. A cleanup failure after a successful block is thrown. If the
block already failed, Harbor preserves that error and logs the cleanup failure.
To reap containers leaked by crashed processes:
Harbor.prune() # force-removes all Harbor-started containers on the host