perry/container exposes the OCI primitives that operate on one
container at a time: create, start, run, stop, remove, exec, logs,
inspect, plus image management. For multi-service stacks, see
perry/compose — but you can mix the two modules in the
same program (a long-running compose stack plus one-off run() helpers
against it is a normal pattern).
Every async function returns a Promise. The runtime backend (docker,
podman, apple/container, …) is auto-detected on first use; see
Overview for the probe order
and override knobs.
run() creates and starts a container in one shot, returning a handle:
{{#include ../../examples/stdlib/container/snippets.ts:run-simple}}The full ContainerSpec accepts:
| Field | Type | Effect |
|---|---|---|
image |
string |
(required) Image reference, e.g. "alpine:3.19". |
name |
string |
Explicit container name. Defaults to {md5(image)[0..8]}-{random_hex8} when unset. |
cmd |
string[] |
Command-line override (overrides the image's CMD). |
entrypoint |
string[] |
Entrypoint override. |
env |
Record<string, string> |
Environment variables. |
ports |
string[] |
Port maps in "host:container" form, e.g. ["8080:80"]. |
volumes |
string[] |
Volume mounts in "host:container[:ro]" form, e.g. ["./data:/data:ro"]. |
network |
string |
Network name to attach to. |
rm |
boolean |
Auto-remove on exit (docker run --rm). |
labels |
Record<string, string> |
Container labels. |
read_only |
boolean |
Mount the root filesystem read-only. |
privileged |
boolean |
Run privileged. Use sparingly. |
user |
string |
UID, username, or "UID:GID". |
workdir |
string |
Working directory inside the container. |
cap_add |
string[] |
Linux capabilities to add (e.g. ["NET_BIND_SERVICE"]). |
cap_drop |
string[] |
Linux capabilities to drop (e.g. ["ALL"]). |
seccomp |
string |
Seccomp profile path or "default" (the runtime's default profile). |
no_new_privileges |
boolean |
Sets --security-opt no-new-privileges — SUID/SGID binaries inside the container can't gain privileges via execve. |
When seccomp or no_new_privileges is set, run()/create()
automatically route through the engine's security-aware launch path
(the same one perry/compose uses), which also normalizes the spec
against the detected backend's capabilities — on apple/container the
two flags have no equivalent and are dropped with a warning instead of
crashing the CLI. See Security for the security knobs
in depth.
For an untrusted workload (e.g. running user-supplied code, executing a build script from an untrusted source) the recommended starting point is "drop everything, add back what you need":
{{#include ../../examples/stdlib/container/snippets.ts:run-secure}}{{#include ../../examples/stdlib/container/snippets.ts:list-inspect}}| Function | Signature | Notes |
|---|---|---|
list(all?) |
(all: boolean) → Promise<ContainerInfo[]> |
all=true includes stopped containers. |
inspect(id) |
(id: string) → Promise<ContainerInfo> |
Throws if the container doesn't exist. |
logs(id, opts?) |
(id, { tail?: number }) → Promise<ContainerLogs> |
Returns a registry handle to a { stdout, stderr } pair. |
exec(id, cmd, opts?) |
(id, cmd[], { env?, workdir? }) |
Runs a command in the container. Returns a ContainerLogs handle. |
stop(id, timeout?) |
(id, seconds: number) |
Sends SIGTERM, then SIGKILL after timeout seconds. |
start(id) |
(id) |
Re-starts a stopped container. |
remove(id, force?) |
(id, force: boolean) |
force=true is docker rm -f. |
Note on the
logsandexecreturn shape: today the FFI returns a registry-id handle into aVec<ContainerLogs>rather than a JS object. Treat the returned value as opaque — a future ergonomics task will expose.stdout/.stderrdirectly on the JS side. TheContainerLogsshape over the wire is{ stdout: string, stderr: string }.
{{#include ../../examples/stdlib/container/snippets.ts:image-mgmt}}| Function | Signature |
|---|---|
pullImage(reference) |
(reference: string) → Promise<void> |
listImages() |
() → Promise<ImageInfo[]> |
removeImage(reference, force?) |
(reference: string, force: boolean) → Promise<void> |
When PERRY_CONTAINER_VERIFY_IMAGES=1 is set, every run(),
create(), and pullImage() call routes through cosign keyless
verification against the Chainguard identity. See
Security → Image verification.
The default name is {md5(image)[0..8]}-{random_hex8} — a stable
8-character hash of the image plus a per-call random suffix. This is
fine for one-off run() calls but makes containers hard to find later
unless you set name: explicitly. For anything you'll re-target
later (with inspect, logs, exec, etc.), set name: upfront.
const handle = await run({
image: "alpine:3.19",
name: "build-helper", // ← stable handle
cmd: ["sh", "-c", "echo 'hi from build-helper'"],
rm: true,
});{{#include ../../examples/stdlib/container/snippets.ts:backend-detect}}getBackend() is synchronous and returns the canonical backend name
("docker", "podman", "apple/container", etc.). It will perform a
synchronous in-place probe on first call so the result is always the
live name; calls after the first hit a cached OnceLock and return
instantly.
detectBackend() is async and returns a JSON array of every probed
candidate with { name, available, reason, version, mode, isolationLevel } per entry. Use it to surface a "diagnostics" view in
your CLI / dashboard.
- Compose orchestration — multi-service stacks.
- Networking — port maps, networks, the cross-service DNS gotcha.
- Security — capability isolation patterns.