>_STRYKE-DOCKER
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). The heavy bollard / tokio code ships as a Rust cdylib that stryke dlopens in-process on first use Docker.
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 # produces target/release/libstryke_docker.{dylib,so}
s pkg install -g . # cdylib lands in ~/.stryke/store/docker@<version>/
# one-liner
make install
The cdylib is dlopened in-process on first use Docker — no helper-binary fork per call. A shared tokio runtime + persistent bollard::Docker client is held in OnceCell for the life of the process; any stryke script that declares use Docker resolves the package automatically. The package is versioned 0.19.0 (stryke.toml / Cargo.toml) and is published with publish = false — it installs from a GitHub release or a local checkout, not from crates.io. make install wraps the build + s pkg install -g . step.
Quick start: use Docker
| daemon version + ping | p Docker::version()->{Version}; exit 1 unless Docker::ping() |
| daemon info | val $i = Docker::info(); p $i->{Containers}, $i->{Images} |
| list all containers | val @all = Docker::ps(all => 1); p scalar(@all) |
| inspect one | p Docker::inspect("web")->{State}{Status} |
| create + start in one shot | val $r = Docker::run("nginx:alpine", name => "web") |
| buffered logs (last 50 lines) | p Docker::logs("web", tail => "50") |
| exec (captured stdout+stderr) | p Docker::exec("web", ["sh", "-c", "nginx -v"]) |
| stop (timeout secs) + remove | Docker::stop("web", timeout => 5); Docker::rm("web") |
| networks + volumes | Docker::network_create("appnet"); Docker::volume_create("appdata") |
| reclaim space | val $report = Docker::prune(all => 1) |
Every wrapper accepts a trailing host => "<socket-or-tcp-url>" to override the daemon for that one call (see Connection model below); with no override the package targets $DOCKER_HOST or the local socket. The full API surface is enumerated in Daemon & container API and Daemon-free helpers, and in the README "API reference" section.
Connection model
There is no client to construct and no login step. The cdylib resolves a daemon endpoint per call: an explicit host => "…" option wins, otherwise $DOCKER_HOST, otherwise the platform default socket. A bollard::Docker client is built once per distinct endpoint and cached (keyed by DOCKER_HOST), so repeated calls reuse the same connection + HTTP pool instead of reconnecting.
| local socket (default) | Docker::ps() |
| environment override | DOCKER_HOST=tcp://192.168.1.10:2375 s script.stk |
| per-call override | Docker::ps(host => "tcp://docker.example.com:2376") |
Works against Docker Desktop, a Linux daemon, Podman exposing the docker-API socket, or a remote DOCKER_HOST. TLS client certs (DOCKER_CERT_PATH / DOCKER_TLS_VERIFY) are on the roadmap, not shipped.
Daemon & container API
These wrappers each build a JSON args dict and call the matching docker__* symbol on the cdylib. A daemon must be reachable; on a daemon error the wrapper dies with Docker::<fn>: <message>. Connection opts (host) may trail any call.
| Daemon | version() · ping() · info() · df() · pkg_version() |
| List / inspect | ps() · inspect($c) · images() · networks() · volumes() · image_inspect($i) · network_inspect($n) · volume_inspect($v) |
| Lifecycle | create($img) · run($img) · start($c) · stop($c, timeout => N) · restart($c) · kill($c, signal => "…") · rm($c, force => 1, volumes => 1) · pause($c) · unpause($c) · rename($c, $new) · wait($c) |
| Introspect | top($c) · stats($c) · diff($c) · port($c) · update($c) · history($img) · commit($c) · exec_inspect($id) |
| Logs & exec | logs($c, tail => "N") · exec($c, \@cmd) · resize($c, $w, $h) · resize_exec($id, $w, $h) |
| Images | pull($img) · rmi($img, force => 1) · tag($src, $dst) · search($term) · inspect_registry($img) |
| Networks | network_create($n) · network_rm($n) · network_connect($n, $c) · network_disconnect($n, $c) |
| Volumes | volume_create($n) · volume_rm($n) |
| Prune | prune(containers => 1, images => 1, volumes => 1, networks => 1, all => 1) |
create/run forward name, cmd (arrayref) and env (arrayref of KEY=VALUE) to the daemon; run is create with start => 1. exec requires an arrayref command and returns captured stdout+stderr as a single string. The per-call result shapes (container dicts, inspect documents, prune report) are documented field-by-field in the README API reference.
Daemon-free helpers (pure, no socket)
A large share of the surface needs no daemon at all: parsers, validators and their inverse builders for Docker's CLI spec grammars. They run anywhere, are deterministic, and are what the test suite exercises without a live socket. Each returns a JSON dict.
| parse an image ref | Docker::parse_image_ref("ghcr.io/o/a:1.2") # => { registry, namespace, repository, tag, digest, path } |
| canonicalize a ref | Docker::normalize_image_ref("nginx") # => docker.io/library/nginx:latest |
| reassemble a ref | Docker::build_image_ref(repository => "nginx", tag => "alpine") |
| validate a name | Docker::valid_container_name("web") # => 1; rejects names < 2 chars |
| validate a tag / repo / digest / ref | Docker::valid_image_tag("1.0") · valid_repository_name("o/a") · valid_digest("sha256:…") · valid_image_ref("o/a:1@sha256:…") |
| ports | Docker::parse_port_spec("0.0.0.0:8080:80/tcp") # => { host_ip, host_port, container_port, protocol }; build_port_spec(…) is the inverse |
| mounts | Docker::parse_mount("/data:/srv:ro") # => { type, source, target, readonly, options }; build_mount(…) inverse |
| env / label | Docker::parse_env("K=V") · build_env("K","V") · parse_label("K=V") · build_label("K","V") |
| devices / tmpfs | Docker::parse_device("/dev/sda:/dev/xvda:rwm") · build_device(…) · parse_tmpfs("/run:ro") · build_tmpfs(…) |
| restart policy | Docker::parse_restart_policy("on-failure:3") # => { policy, max_retries }; build_restart_policy(…) inverse |
| memory | Docker::parse_memory("512m") # => { bytes => 536870912, … }; format_memory(536870912) => "512m" |
| duration | Docker::parse_duration("1h30m") # => nanoseconds; format_duration(5400000000000) => "1h30m0s" |
| cpus / signal / platform / ulimit | Docker::parse_cpus("1.5") # => 1500000000 NanoCPUs · parse_signal("9") => SIGKILL · parse_platform("linux/arm64/v8") · parse_ulimit("nofile=1024") |
Every parser has a builder inverse where the round-trip is defined (image ref, port, mount, env, label, device, restart policy, platform, memory, duration, tmpfs). The grammar rules each one enforces — tag ≤128 chars and no leading ./-, lowercase-only repository names, OCI algorithm:hex digests (sha256→64, sha512→128 lowercase hex), go-units base-1024 memory, Go time.ParseDuration durations — are spelled out per function in the README API reference.
Deferred operations
Four operations are present in the wrapper but die with a clear message rather than returning partial data — each needs an FFI shape the current blocking JSON-in/JSON-out boundary doesn't model yet:
Docker::logs_follow | streaming logs — needs a callback FFI; use Docker::logs for snapshot reads |
Docker::events | streaming daemon events — needs a callback FFI |
Docker::push | needs the registry-auth surface designed |
Docker::build | needs the tar + stream surface designed |
One-shot stats (--no-stream) ships; the continuous-stream variants do not. Calling a deferred op raises a die naming the op and the reason, so scripts fail loudly instead of silently mis-behaving.
Runnable examples
The examples/ directory ships .stk scripts that are eval-guarded so they exit cleanly when no daemon is reachable:
discover.stk | read-only tour: version, info, ps -a, image count |
health.stk | daemon snapshot + per-container status via inspect |
run.stk | pull, run, inspect, stop, rm lifecycle |
logs.stk | buffered logs read (the streaming follow path is deferred) |
build.stk | image-build sketch (the build op is deferred) |
# run any example s examples/discover.stk # point it at a remote daemon DOCKER_HOST=tcp://localhost:2375 s examples/discover.stk
Testing
# compile-only — no live daemon needed (exercises the pure helpers) cargo test # live round-trip against a reachable daemon DOCKER_HOST=unix:///var/run/docker.sock s test t/
The live tests under t/ pull busybox:latest, run a sleep container under a unique stryke-test-$$ name, create networks/volumes with unique names, exec into the container, and clean up at exit. t/test_stryke_docker_surface.stk is a wrapper-completeness pin so the .stk surface can't silently drop a function.
Why a package, not a builtin
bollard + tokio for full daemon-API coverage — opt-in so the stryke core binary stays under 40 MB.
The stryke side is a thin .stk wrapper that calls docker__* FFI symbols on the cdylib; the heavy code lives in libstryke_docker.{dylib,so}, dlopened in-process on first use Docker. Core stryke is never linked against this package's deps.
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). Dispatch is by symbol name, not by a verb field in the payload — the wrapper sends the call-specific keys directly (container, image, tail, …) plus an optional host override. The authoritative export list is [ffi].exports in stryke.toml.
Responses are JSON. A success returns the per-function result object directly; a failure returns {"error": "<msg>"} and the wrapper's _decode step raises die "Docker::<fn>: <msg>". A panic inside the handler is caught and surfaced as the same error shape rather than aborting the process.
# request (built by the .stk wrapper, dispatched by symbol name)
docker__stop({"container": "web", "timeout": 5})
# success — the per-fn result object, returned as-is
{"ok": true}
{"registry": "ghcr.io", "namespace": "o", "repository": "a", "tag": "1.2", ...}
# failure — wrapper die()s with the message
{"error": "No such container: web"}
Persistent state. A single shared tokio multi-thread runtime drives every async call, and a bollard::Docker client cache keyed by DOCKER_HOST reuses the same client + underlying HTTP pool across calls — no fork-per-call, no fresh connection each time. The returned CString is freed via the cdylib-exported stryke_free_cstring, wired automatically by stryke's cdylib loader.
Troubleshooting
Docker::ping returns false / version dies | No daemon at the resolved endpoint. Start Docker Desktop, sudo systemctl start docker, or set DOCKER_HOST to a reachable socket / tcp url. |
die "Docker::<fn>: <msg>" | The daemon returned an error (no such container, image not found, …). The message is the daemon's, passed through the FFI error envelope. |
a deferred op dies | logs_follow, events, push, build are not implemented in the 0.19.0 cdylib — see Deferred operations. |
Docker::exec: cmd must be arrayref | exec takes the command as an arrayref: Docker::exec("web", ["sh","-c","…"]). |
| helper validators want pure input | The parse_* / valid_* / build_* helpers never touch a socket — if they fail it is bad input, not a daemon problem. valid_* returns { valid, reason } rather than dying. |
Layout
stryke-docker/ ├── Cargo.toml # cdylib crate manifest (crate-type = ["cdylib"], publish = false) ├── src/ │ └── lib.rs # cdylib — docker__* extern "C" exports ├── lib/ # stryke .stk wrapper(s) — `use Docker` ├── stryke.toml # stryke package manifest ├── t/ # zunit-style tests ├── examples/ # runnable .stk examples ├── Makefile # `make install` builds + installs └── docs/ # this site (GitHub Pages)
Sibling packages
Part of the stryke package family. Browse the others via the MenkeTechnologiesMeta umbrella repo:
- stryke-arrow — Apache Arrow / Parquet / Feather
- stryke-aws — S3, DynamoDB, SQS, Lambda, STS
- stryke-azure — Blob, Queues, Cosmos DB, Key Vault, Entra
- stryke-clickhouse — ClickHouse
- stryke-demo — live demos for every connector
- stryke-duckdb — embedded DuckDB SQL engine
- stryke-email — email + campaign client
- stryke-fleet — parallel expect/PTY automation
- stryke-gcp — Cloud Storage, Pub/Sub, Secret Manager, BigQuery, Firestore
- stryke-grpc — reflection-based gRPC client
- stryke-gui — mouse / keyboard / screen / clipboard automation
- stryke-k8s — Kubernetes
- stryke-kafka — Apache Kafka
- stryke-mcpd — MCP servers without a runtime
- stryke-mongo — MongoDB
- stryke-mssql — Microsoft SQL Server
- stryke-mysql — MySQL / MariaDB
- stryke-neo4j — Neo4j graph (Bolt)
- stryke-office — Office docs / PDF / images
- stryke-parquet — Parquet file inspector
- stryke-polars — Polars + ndarray + linalg + FFT
- stryke-postgres — PostgreSQL
- stryke-redis — Redis / Valkey
- stryke-scrape — web scraping / crawling
- stryke-scylla — ScyllaDB / Cassandra
- stryke-search — Elasticsearch / OpenSearch
- stryke-selenium — browser automation (WebDriver)
- stryke-spark — Spark Connect (no JVM)
- stryke-utils — pure-stryke utility belt
- stryke-zmq — ZeroMQ messaging