README.md

August 2, 2026 · View on GitHub

 ███████╗████████╗██████╗ ██╗   ██╗██╗  ██╗███████╗
 ██╔════╝╚══██╔══╝██╔══██╗╚██╗ ██╔╝██║ ██╔╝██╔════╝
 ███████╗   ██║   ██████╔╝ ╚████╔╝ █████╔╝ █████╗
 ╚════██║   ██║   ██╔══██╗  ╚██╔╝  ██╔═██╗ ██╔══╝
 ███████║   ██║   ██║  ██║   ██║   ██║  ██╗███████╗
 ╚══════╝   ╚═╝   ╚═╝  ╚═╝   ╚═╝   ╚═╝  ╚═╝╚══════╝
                   [ d o c k e r ]

CI License: MIT stryke

[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

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 shared tokio multi-thread runtime drives every async call.
  • CLIENTSbollard::Docker cache keyed by DOCKER_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/httpTLS 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 statsInteractive TTY + stdin attach
Exec with captured stdout+stderrdocker-compose v2 file parser
Synchronous create/start/stopSwarm services / configs / secrets

[0xFF] License

MIT.