Docker Subsystem
Docker container management with contract-based API
Run
wippy run userspace/dockeruserspace/docker
Docker container and image management via contracts and process messaging.
Features
- Container lifecycle management (create, start, stop, delete)
- Image operations (pull, build, list, delete)
- Network and volume management
- Port mapping (host:container)
- Live log streaming via process messaging
- Interactive and managed execution modes
- Compose-style multi-container groups
- Declarative containers via registry entries (auto-start on boot)
- Dynamic pickup of new container entries at runtime via registry events
Installation
- name: dependency.docker
kind: ns.dependency
component: userspace/docker
version: ">=0.2.0"
Requirements
| Requirement | Default | Description |
|-------------|---------|-------------|
| database | app:db | Database resource for container and image storage |
| process_host | app:processes | Process host for docker services |
Declarative Containers
Register containers as registry.entry with meta.type: docker.container. The docker root process picks them up at startup and whenever a new entry is created at runtime.
This works like docker-compose: any wippy component can declare containers in its _index.yaml, and they start automatically when the component is installed.
Single container
entries:
- name: my_redis
kind: registry.entry
meta:
type: docker.container
image: redis:7
command: redis-server --appendonly yes
ports:
- { host: 6379, container: 6379 }
Multi-service stack
entries:
- name: postgres_db
kind: registry.entry
meta:
type: docker.container
image: postgres:16
command: postgres
env:
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
ports:
- { host: 5432, container: 5432 }
network: myapp-net
- name: app_server
kind: registry.entry
meta:
type: docker.container
image: myapp:latest
command: ./server --db postgres_db:5432
env:
DATABASE_URL: postgres://postgres:secret@postgres_db:5432/myapp
ports:
- { host: 8080, container: 8080 }
network: myapp-net
- name: nginx_proxy
kind: registry.entry
meta:
type: docker.container
image: nginx:latest
command: nginx -g 'daemon off;'
ports:
- { host: 80, container: 80 }
- { host: 443, container: 443 }
network: myapp-net
volumes:
- { host: /etc/nginx/conf.d, container: /etc/nginx/conf.d, mode: ro }
Available fields
| Field | Type | Description |
|-------|------|-------------|
| image | string | Docker image (default: alpine:latest) |
| command | string | Command to run (required) |
| name | string? | Container name |
| env | map | Environment variables |
| ports | array | Port mappings: {host, container, protocol?} |
| network | string? | Docker network name |
| volumes | array | Volume mounts: {host, container, mode?} |
| work_dir | string? | Working directory inside container |
| interactive | boolean? | Enable stdin for interactive containers |
| labels | map? | Container labels |
How it works
- A wippy component declares
docker.containerentries in its_index.yaml - On install (
wippy install), entries appear in the registry - The docker root process detects new entries via registry events
- Containers are created in the database and picked up by workers
- Workers pull images (if needed) and start containers via Docker API
No manual orchestration required. Install a component, containers start.
Contracts
userspace.docker:containers
local docker = contract.open("userspace.docker:containers")
-- Create a container
local result = docker:create({
image = "alpine:latest",
command = "echo hello",
name = "my-container",
env = { MY_VAR = "value" },
ports = { { host = 8080, container = 80 } },
network = "my-network",
stream = { reply_to = process.self(), topic = "docker.logs" },
})
-- Get container by ID
local result = docker:get({ id = container_id })
-- List containers
local result = docker:list({ status = "running", limit = 10 })
-- Get container logs
local result = docker:logs({ id = container_id })
-- Send stdin to interactive container
docker:stdin({ container_id = id, data = "input\n" })
-- Delete container
docker:delete({ id = container_id })
-- Create multiple containers on a shared network
local result = docker:compose({
name = "my-group",
network = "my-net",
containers = {
{ image = "redis:latest", command = "redis-server", ports = { { host = 6379, container = 6379 } } },
{ image = "alpine:latest", command = "echo done" },
},
})
userspace.docker:networks
local networks = contract.open("userspace.docker:networks")
networks:create({ name = "my-network", driver = "bridge" })
local result = networks:list({})
networks:remove({ id = network_id })
userspace.docker:volumes
local volumes = contract.open("userspace.docker:volumes")
volumes:create({ name = "my-volume", driver = "local" })
local result = volumes:list({})
volumes:remove({ name = "my-volume" })
userspace.docker:images
local images = contract.open("userspace.docker:images")
-- List images
local result = images:list()
-- Pull an image
local result = images:pull({ image = "alpine", tag = "latest" })
-- Build from Dockerfile
local result = images:build({
name = "my-app",
tag = "v1",
dockerfile = "FROM alpine\nRUN echo hello",
stream = { reply_to = process.self(), topic = "build.logs" },
})
-- Check build status
local result = images:build_status({ build_id = build_id })
-- Delete an image
images:delete({ id = image_id })
Streaming Events
Methods that accept a stream parameter deliver live events via process messaging:
local result = docker:create({
image = "alpine:latest",
command = "echo hello",
stream = { reply_to = process.self(), topic = "my.events" },
})
local ch = process.listen("my.events")
while true do
local ev = ch:receive()
if ev.type == "done" then break end
if ev.type == "log" then
print(ev.stream .. ": " .. ev.line)
elseif ev.type == "status" then
print("status: " .. ev.status)
end
end
License
Apache-2.0