README.md
August 2, 2026 · View on GitHub
███████╗████████╗██████╗ ██╗ ██╗██╗ ██╗███████╗
██╔════╝╚══██╔══╝██╔══██╗╚██╗ ██╔╝██║ ██╔╝██╔════╝
███████╗ ██║ ██████╔╝ ╚████╔╝ █████╔╝ █████╗
╚════██║ ██║ ██╔══██╗ ╚██╔╝ ██╔═██╗ ██╔══╝
███████║ ██║ ██║ ██║ ██║ ██║ ██╗███████╗
╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝
[ d o c k e r ]
[DOCKER CLIENT FOR STRYKE // CONTAINERS + IMAGES + NETWORKS + VOLUMES + LOGS + EXEC + PRUNE]
"The docker daemon, scriptable from a one-liner."
Docker client for stryke. Containers, images, networks, volumes, logs, exec, prune against any reachable Docker daemon (Docker Desktop, Linux daemon, Podman with the docker-API socket, remote DOCKER_HOST). Opt-in package tier.
strykelang · MenkeTechnologiesMeta · stryke-k8s · stryke-kafka · stryke-demo
Read the Docs · Engineering Report
Table of Contents
- [0x00] Install
- [0x01] Quick start
- [0x02] API reference
- [0x03] FFI layer
- [0x04] Tests
- [0x05] Dev workflow
- [0x06] Layout
- [0x07] Roadmap
- [0xFF] License
[0x00] Install
From a release (no rustc on the consumer machine):
s pkg install -g github.com/MenkeTechnologies/stryke-docker
From a local checkout:
cd ~/projects/stryke-docker
cargo build --release
s pkg install -g .
Or:
make install
The cdylib is dlopened in-process on first use Docker. A shared tokio
runtime + persistent bollard::Docker client is held in OnceCell for
the life of the process — no fork-per-call, no fresh HTTP connection
each time.
[0x01] Quick start
use Docker
# Connection: $DOCKER_HOST or local socket — no setup.
p Docker::version()->{Version}
exit 1 unless Docker::ping()
# Pull, run, inspect, stop, rm.
Docker::pull "nginx:alpine"
val $r = Docker::run "nginx:alpine",
name => "web",
port => ["8080:80/tcp"],
label => ["app=web"],
detach => 1
p "started: $r->{id}"
# List with filters.
val @web = Docker::ps all => 1, filters => { label => ["app=web"] }
for val $c (@web) {
p " #{$c->{Id}} #{$c->{State}} #{join(',', @{$c->{Names}})}"
}
# Buffered logs.
p Docker::logs "web", tail => "50", timestamps => 1
# Exec — returns captured stdout+stderr.
p Docker::exec "web", ["sh", "-c", "nginx -v"]
# Networks + volumes.
Docker::network_create "appnet", driver => "bridge", subnet => "10.42.0.0/24"
Docker::volume_create "appdata"
p _->{Name} for Docker::volumes()
# Cleanup.
Docker::stop "web", time => 5
Docker::rm "web"
Docker::network_rm "appnet"
Docker::volume_rm "appdata"
# Reclaim space.
val $report = Docker::prune all => 1
p "freed: $report->{containers}{SpaceReclaimed} bytes"
Per-call connection overrides:
val %remote = (host => "tcp://docker.example.com:2376", timeout => 30)
Docker::ps %remote
[0x02] API reference
Daemon
Docker::ping %opts → 1 | ""
Docker::version %opts → \%info # Version, ApiVersion, Os, Arch, …
Docker::info %opts → \%info
Docker::pkg_version() → $version_string # the cdylib's CARGO_PKG_VERSION
Docker::events %opts → dies # streaming — deferred in v0.2.x cdylib
Containers
Docker::ps %opts → @{ \%container }
Docker::inspect $container, %opts → \%info
Docker::run $image, %opts → { id, warnings, started }
Docker::create $image, %opts → { id, warnings, started => false }
Docker::start $container, %opts → { id, started }
Docker::stop $container, %opts → { id, stopped } # opts: time
Docker::restart $container, %opts → { id, restarted }
Docker::kill $container, %opts → { id, killed } # opts: signal
Docker::rm $container, %opts → { id, removed } # opts: force, volumes
Docker::pause $container, %opts → 1 | 0
Docker::unpause $container, %opts → 1 | 0
Docker::rename $container, $name, %opts → 1 | 0
Docker::wait $container, %opts → $exit_status_code # blocks until exit
Docker::top $container, %opts → { Titles, Processes } # opts: ps_args
Docker::commit $container, %opts → $image_id # opts: repo, tag, comment, author, pause
%opts for run/create:
name, cmd \@argv, env \@KV, port \@spec, volume \@spec, label \@KV, network, workdir, user, hostname, restart, rm, tty
Logs + exec + stats
Docker::logs $container, %opts → $text
Docker::logs_follow $container, %opts → dies # streaming — deferred in v0.2.x cdylib
Docker::exec $container, \@cmd, %opts → $output # captured stdout+stderr
Docker::exec_inspect $id, %opts → \%info # exec exit code / running / pid
Docker::resize $container, $width, $height, %opts → 1 | 0 # resize the container TTY
Docker::resize_exec $id, $width, $height, %opts → 1 | 0 # resize an exec instance's TTY
Docker::stats $container, %opts → \%snapshot # one-shot stats (--no-stream)
Docker::diff $container, %opts → @{ {Path, Kind} } # fs changes vs image (0 mod,1 add,2 del)
Docker::history $image, %opts → @layers # image build history
Docker::df %opts → \%usage # docker system df (images/containers/volumes/cache)
Docker::port $container, %opts → \%ports # { "80/tcp" => [{HostIp, HostPort}] }
Docker::update $container, %opts → \%resp # live limits: memory, cpu_shares, cpu_quota, cpuset_cpus
Docker::image_inspect $image, %opts → \%info # config, layers, RootFS
Docker::volume_inspect $volume, %opts → \%info # driver, mountpoint, labels
Docker::network_inspect $network, %opts → \%info # driver, subnet, connected containers
Pure helpers (no daemon)
Docker::parse_image_ref($ref) → { registry, namespace, repository, tag, digest, path }
Docker::normalize_image_ref($ref) → { ref, registry, namespace, repository, tag, digest } # canonical form: nginx → docker.io/library/nginx:latest
Docker::build_image_ref(%opts) → $ref # parts → [registry/][namespace/]repo[:tag][@digest]; inverse of parse_image_ref
Docker::valid_container_name($n) → 1 | "" # /?[a-zA-Z0-9][a-zA-Z0-9_.-]+
Docker::valid_image_tag($tag) → { tag, valid, reason } # docker tag rules: ASCII alnum/_/./-, ≤128, no leading . or -
Docker::valid_digest($digest) → { digest, algorithm, hex, valid, reason } # OCI content digest algorithm:hex (sha256→64, sha512→128 lowercase hex)
Docker::valid_repository_name($name) → { name, valid, reason } # distribution/reference repo path grammar; lowercase only (rejects MyApp)
Docker::valid_image_ref($ref) → { ref, valid, reason } # validate a COMPLETE reference (registry/ns/repo:tag@digest); reason names the failing component
Docker::parse_port_spec($spec) → { host_ip, host_port, container_port, protocol }
Docker::parse_ulimit($spec) → { spec, name, soft, hard, unlimited } # docker run --ulimit type=soft[:hard]; single value → hard=soft; -1 = unlimited
Docker::build_port_spec(%opts) → $spec # parts → [ip:][host:]container[/proto]; inverse of parse_port_spec
Docker::parse_mount($spec) → { type, source, target, readonly, options } # -v src:dst[:opts]
Docker::build_mount(%opts) → $spec # { target, source?, options?, readonly? } → -v spec; inverse of parse_mount
Docker::parse_env($spec) → { spec, key, value, from_host } # -e KEY=VAL; split on first =; bare KEY → from_host
Docker::build_env($key, $value?) → $spec # KEY=VALUE, or bare KEY (host passthrough) when value omitted; inverse of parse_env
Docker::parse_restart_policy($spec) → { spec, policy, max_retries } # --restart no|always|unless-stopped|on-failure[:N]; only on-failure takes :N
Docker::build_restart_policy($policy, $max_retries?) → $spec # inverse; max_retries honoured only with on-failure
Docker::parse_memory($memory) → { memory, value, unit, bytes } # --memory/--shm-size byte size (go-units RAMInBytes; base 1024, k/m/g/t/p, optional i/b)
Docker::format_memory($bytes) → { bytes, value, unit, memory } # inverse: largest exact binary unit (536870912 → 512m); round-trips parse_memory
Docker::parse_platform($platform) → { platform, os, architecture, variant } # --platform os/arch[/variant] (linux/arm64/v8); variant optional
Docker::build_platform(\%parts) → { platform, os, architecture, variant } # { os, architecture, variant? } → --platform string; inverse of parse_platform
Docker::parse_label($spec) → { spec, key, value } # --label KEY=VAL; split on first =; bare KEY → undef value
Docker::build_label($key, $value?) → $spec # KEY=VALUE, or bare KEY when value omitted; inverse of parse_label
Docker::parse_device($spec) → { spec, host_path, container_path, permissions } # --device host[:container[:perms]]; container defaults to host, perms default rwm
Docker::build_device(%opts) → $spec # { host_path, container_path?, permissions? } → shortest --device spec; inverse of parse_device
Docker::parse_signal($signal) → { signal, name, number } # normalize a signal name/number to SIG-form (9 / KILL / sigkill → SIGKILL)
Docker::parse_duration($duration) → { duration, nanos, seconds } # Go time.ParseDuration (--health-interval, --stop-timeout, …): 1h30m, 1.5h, 300ms → nanoseconds
Docker::format_duration($nanos) → { nanos, duration } # inverse: nanoseconds → Go duration string (5400000000000 → 1h30m0s)
Docker::parse_cpus($cpus) → { cpus, nano_cpus } # --cpus → NanoCPUs (engine ×1e9; 1.5 → 1500000000)
Docker::parse_tmpfs($spec) → { spec, path, readonly, options } # --tmpfs path[:opts]; options split into { key, value }; ro sets readonly
Docker::build_tmpfs(%opts) → $spec # { path, options?, readonly? } → --tmpfs spec; inverse of parse_tmpfs
parse_mount classifies a -v short mount: a host-path source (/, ., ~)
is a bind, a bare name is a volume, and a lone container path is an
anonymous volume. The comma list after the second colon becomes options,
and an ro entry sets readonly.
Images
Docker::images %opts → @{ \%image }
Docker::pull $image, %opts → @events # drained event list
Docker::push $image, %opts → dies # deferred in v0.2.x cdylib
Docker::rmi $image, %opts → 1 | 0 # opts: force
Docker::tag $source, $target, %opts → 1 | 0
Docker::build $dir, %opts → dies # deferred in v0.2.x cdylib
Docker::search $term, %opts → @{ \%result } # Docker Hub search; opts: limit
Docker::inspect_registry $image, %opts → \%distribution # registry metadata, no pull
Networks + volumes
Docker::networks %opts → @{ \%network }
Docker::network_create $name, %opts → \%network # driver, subnet, gateway, label
Docker::network_rm $name, %opts → 1 | 0
Docker::network_connect $network, $container, %opts → 1 | 0 # opts: aliases \@names
Docker::network_disconnect $network, $container, %opts → 1 | 0 # opts: force
Docker::volumes %opts → @{ \%volume }
Docker::volume_create $name, %opts → \%volume # driver, label
Docker::volume_rm $name, %opts → { removed } # force
Prune
Docker::prune %opts → \%report
# opts: containers, images, volumes, networks, all
[0x03] FFI layer
Each Docker::* wrapper builds a JSON args dict and calls a sibling
docker__* symbol resolved out of libstryke_docker.{dylib,so}. The
cdylib is dlopened in-process on first use Docker (via stryke's
pkg::commands::try_load_ffi_for resolver hook). Its exports cover
containers (including network attach/detach, TTY resize, exec inspect),
images (including search and registry inspect), networks, volumes, exec,
logs, and prune, plus daemon-free helpers (docker__parse_image_ref,
docker__build_image_ref, docker__normalize_image_ref,
docker__valid_container_name, docker__parse_port_spec,
docker__build_port_spec, docker__parse_mount, docker__parse_label,
docker__parse_device, docker__parse_signal). The authoritative list is
[ffi].exports in stryke.toml.
Persistent state:
RUNTIME— one sharedtokiomulti-thread runtime drives every async call.CLIENTS—bollard::Dockercache keyed byDOCKER_HOST(socket path / tcp url). The v1 helper opened a fresh dockerd connection per fork; this reuses the same client + underlying HTTP pool across calls.
Snapshot vs. streaming: stats is a one-shot snapshot (--no-stream).
The remaining continuous-stream ops (events, logs --follow) and the
auth/tar-heavy build / push need a callback FFI / credential / tar-stream
design that the blocking StrToStr shape doesn't model yet; calling them
dies with a clear message.
[0x04] Tests
cargo test # compiles, no live calls
DOCKER_HOST=unix:///var/run/docker.sock s test t/ # live round-trip
Tests pull busybox:latest, run a sleep container with a unique
stryke-test-$$ name, run network/volume creates with unique names,
exec into it, and clean up at exit.
Local test daemon:
# macOS
open -a Docker
# Linux
sudo systemctl start docker
# Or any reachable daemon:
DOCKER_HOST=tcp://192.168.1.10:2375 s test t/
[0x05] Dev workflow
make # release build
make debug
make test
make install
make clean
[0x06] Layout
stryke-docker/
stryke.toml # stryke package manifest
Cargo.toml # cdylib crate manifest
Makefile
src/lib.rs # cdylib — docker__* extern "C" exports
lib/
Docker.stk # `use Docker`
t/
test_docker.stk # live round-trip (gated on a reachable daemon)
test_stryke_docker_surface.stk # wrapper-completeness pin
examples/
build.stk
discover.stk
health.stk
logs.stk
run.stk
.github/workflows/
ci.yml # cargo check/test/clippy + docs lint (no live daemon)
release.yml # cross-compile + GH release on tag push
[0x07] Roadmap
| Shipped (v0.2.x) | Later |
|---|---|
| Local socket + DOCKER_HOST tcp/http | TLS client certs (DOCKER_CERT_PATH / DOCKER_TLS_VERIFY) |
| Pull (drained event list) | Build / push / events / logs --follow (need callback FFI / tar / auth) |
| Pause/unpause/rename/wait/top/commit + one-shot stats | Interactive TTY + stdin attach |
| Exec with captured stdout+stderr | docker-compose v2 file parser |
| Synchronous create/start/stop | Swarm services / configs / secrets |
[0xFF] License
MIT.