Container management¶
Once connected to a container daemon, the containers module provides Starlark APIs to manage container lifecycles, configure resource constraints, inspect state, and stream logs using a module-based hybrid pattern aligned with k8s and ssh.
Creating and Running Containers¶
All container provisioning methods anchor on a configured client instance (containers.config()) and return a pure-data dictionary with attribute dot-access (AttrDict):
client.run(...): Atomically creates and starts the container, returning itsAttrDict.client.create(...): Allocates container resources without starting it, returning itsAttrDictin"created"status.
def main():
dockr = containers.config()
# Create and start a Redis container
box = dockr.run(
image = "redis:7-alpine",
name = "app-cache",
ports = {"6379/tcp": 0}, # Allocate dynamic ephemeral host port
memory = "256MB", # Hard memory limit
detach = True, # Run in background
)
# Dot-access or dictionary indexing
print("Container ID:", box.id[:12])
print("Container Name:", box.name)
print("Container Status:", box.status)
print("Container Ports:", box.ports)
Provisioning Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
image |
str |
Required | OCI image reference (e.g., "redis:7-alpine"). |
name |
str |
"" |
Optional container name. |
command |
str | list[str] |
None |
Process command and arguments. |
env |
dict[str, str] | list[str] |
None |
Environment variables ({"KEY": "VAL"} or ["KEY=VAL"]). |
ports |
dict[str, int \| str] |
None |
Port mappings (e.g., {"5432/tcp": 0} for dynamic host port, {"80/tcp": 8080}). |
volumes |
dict[str, str] |
None |
Bind mounts mapping {"host_path": "container_path"}. |
detach |
bool |
True |
Run in background. If False, blocks until container exits. |
network |
str |
"" |
Network mode (e.g., "bridge", "host", "none"). |
cpu |
str | float | int |
None |
CPU limit (e.g., "1.5", 1.5, or nanoCPUs). |
memory |
str | int |
None |
Memory limit (e.g., "512MB", "1GB", or byte count). |
auto_remove |
bool |
False |
Automatically delete container upon termination. |
State Control Operations¶
All lifecycle verbs are methods on the configured client dockr. Each verb accepts either the container AttrDict directly or a string container name/ID:
def manage_state(dockr, box):
# Stop container (sends SIGTERM, then SIGKILL after timeout seconds)
dockr.stop(box, timeout = 10)
# Start stopped container
dockr.start(box)
# Restart container
dockr.restart(box, timeout = 5)
# Block until container stops and retrieve exit code
exit_code = dockr.wait(box, condition = "not-running")
print("Process exited with code:", exit_code)
# Delete container from daemon (alias: dockr.remove)
dockr.delete(box, force = True, volumes = True)
# String identifiers are supported interchangeably:
dockr.start("app-cache")
dockr.stop("app-cache", timeout = 5)
dockr.delete("app-cache", force = True)
Container AttrDict Properties¶
.id(str): Unique 64-character container SHA ID..name(str): Assigned container name (without leading/)..image(str): Image reference used to run the container..status(str): Current status string ("running","created","exited","removed")..ports(dict): Dictionary of port bindings.
Zero-Friction Serialization¶
Because box is a pure dictionary with zero attached Go methods or network transports, it serializes directly to JSON and YAML:
# Export container description to disk
json_manifest = json.encode(box)
yaml_manifest = yaml.encode(box)
fs.path("container.json").write(json_manifest)
Selective Module-Scope Shortcuts¶
For quick one-liners using the ambient default daemon without calling containers.config(), the containers module exports selective shortcuts:
containers.run(image, ...): Creates and starts a container using default daemon socket discovery.containers.exec(target, command, ...): Dispatches a command inside a running container.containers.stop(target, timeout=10): Stops a container.containers.delete(target, force=False)/containers.remove(...): Deletes a container.
def main():
# Quick one-liner run
box = containers.run("alpine:latest", command=["sleep", "60"], detach=True)
# Command execution
res = containers.exec(box, ["echo", "hello"])
print(res.stdout)
# Quick shutdown and cleanup
containers.stop(box, timeout=2)
containers.delete(box, force=True)
Dynamic Port Resolution¶
When running integration test fixtures or parallel CI runners, hardcoding host ports risks collision. Pass 0 as the host port in ports to let the engine allocate an ephemeral host port:
def main():
dockr = containers.config()
pg = dockr.run(
image = "postgres:16-alpine",
env = {"POSTGRES_PASSWORD": "secretpassword"},
ports = {"5432/tcp": 0}, # Request ephemeral host port
detach = True,
)
# Resolve dynamic host port via client
host_port = dockr.port(pg, "5432/tcp")
print("PostgreSQL allocated host port:", host_port)
# Connect to database using resolved port
dsn = "postgres://postgres:secretpassword@localhost:%d/postgres?sslmode=disable" % host_port
print("DSN:", dsn)
dockr.delete(pg, force = True)
dockr.port(target, port_spec) accepts "5432/tcp", "5432", or 5432 and returns the resolved host port as an integer.
Listing and Inspecting Containers¶
Listing Containers¶
dockr.list(all=False): Returns a list ofAttrDictobjects for running containers. Passall=Trueto include stopped and created containers.
def main():
dockr = containers.config()
for c in dockr.list(all=True):
print("Container:", c.id[:12], c.name, c.status)
Retrieving and Inspecting¶
dockr.get(id_or_name): Retrieves an existing containerAttrDictby ID or name.dockr.inspect(target): Returns the complete raw daemon inspectionAttrDict, includingState,Config,NetworkSettings, andHostConfig.
def main():
dockr = containers.config()
c = dockr.get("app-cache")
info = dockr.inspect(c)
print("Running:", info.State.Running)
print("IP Address:", info.NetworkSettings.IPAddress)
Streaming Container Logs¶
The dockr.logs() method streams log output from a container, returning an io.reader compatible with Starkite's io standard library module.
Automatic Multiplexing Demux¶
Docker and Podman multiplex stdout and stderr streams over a single connection using an 8-byte binary framing protocol:
Starkite's socket transport automatically decodes and strips these binary headers in real time, delivering a clean, readable text stream without binary header corruption.
def main():
dockr = containers.config()
c = dockr.run("alpine:latest", command=["sh", "-c", "echo 'starting'; sleep 1; echo 'done'"])
defer(lambda: dockr.delete(c, force=True))
# Read container log stream
reader = dockr.logs(c, stdout=True, stderr=True, tail="50")
print(reader.read_all())
Log Stream Parameters¶
dockr.logs(target, follow=False, tail="all", stdout=True, stderr=True):
| Parameter | Type | Default | Description |
|---|---|---|---|
target |
AttrDict | dict | str |
Required | Target container instance or string name/ID. |
follow |
bool |
False |
Stream logs continuously as they arrive. |
tail |
str |
"all" |
Number of log lines to retrieve from the end (e.g., "100", "all"). |
stdout |
bool |
True |
Include standard output in stream. |
stderr |
bool |
True |
Include standard error in stream. |
Practical Recipes¶
Ephemeral Test Database¶
This recipe starts an ephemeral PostgreSQL container on a dynamic host port, waits for readiness using retry.with_backoff, executes database queries, and guarantees cleanup with defer():
def setup_test_db(dockr):
pg = dockr.run(
image = "postgres:16-alpine",
env = {"POSTGRES_PASSWORD": "secretpassword", "POSTGRES_DB": "testdb"},
ports = {"5432/tcp": 0},
detach = True,
)
host_port = dockr.port(pg, "5432/tcp")
dsn = "postgres://postgres:secretpassword@localhost:%d/testdb?sslmode=disable" % host_port
# Wait for database readiness
retry.with_backoff(
lambda: sql.open("postgres", dsn).exec("SELECT 1"),
attempts = 10,
initial = "500ms",
)
return pg, dsn
def main():
dockr = containers.config()
pg, dsn = setup_test_db(dockr)
defer(lambda: dockr.delete(pg, force=True))
db = sql.open("postgres", dsn)
db.exec("CREATE TABLE health_check (id SERIAL PRIMARY KEY, service TEXT)")
db.exec("INSERT INTO health_check (service) VALUES ('billing-service')")
rows = db.query("SELECT service FROM health_check")
print("Database test passed:", rows[0]["service"])
Run with:
Container Health Diagnostic and Log Inspection¶
This recipe iterates over all containers on the host, identifies containers that exited with a non-zero exit code, and extracts their last 20 log lines for troubleshooting:
def main():
dockr = containers.config()
containers_list = dockr.list(all=True)
print("Inspecting", len(containers_list), "containers...")
for c in containers_list:
data = dockr.inspect(c)
state = data.get("State", {})
status = state.get("Status", "unknown")
exit_code = state.get("ExitCode", 0)
if status == "exited" and exit_code != 0:
print("Container '%s' failed with exit code %d" % (c.name, exit_code))
print("--- Last 20 log lines ---")
log_reader = dockr.logs(c, tail="20", stdout=True, stderr=True)
print(log_reader.read_all())
print("-------------------------")
Run with:
Permissions¶
Container management operations require the following capabilities:
containers.read:list(),get(),inspect(),port(),logs().containers.write:create(),run(),start(),stop(),restart(),wait().containers.manage:delete()/remove().
All of these capabilities are included in the allow-local profile: