Curium API -- Adom Container Management
Curium is the Adom container orchestration service. It manages Docker containers, Traefik routing, SSH portal access, and USB/IP device attachment for user project containers.
Base URL: https://curium.adom.inc (or wherever your Curium instance is hosted)
Authentication
Curium itself currently has minimal auth (TODOs in codebase). It integrates with Carbon for user/org lookups and API key creation. When calling Curium from within an Adom container, use the standard Adom API key pattern:
API_KEY=$(cat /var/run/adom/api-key)
# Then use: -b "session_token=$API_KEY" on curl calls if auth is added
Container ID Format
All containers use the format: {owner}-{repository}-{hex_hash}
Example: john-gallia-f280e93ffec7e79d
owner: project owner usernamerepository: project/repo namehex_hash: 8-byte random hex identifier
Container Endpoints
List Containers
GET /containers
Query Parameters (optional):
| Param | Type | Description |
|---|---|---|
owner | string | Filter by owner username |
repository | string | Filter by repository name |
Response (200):
[
{
"id": "john-gallia-f280e93ffec7e79d",
"owner": "john",
"repository": "gallia",
"unique_hash": "f280e93ffec7e79d",
"status": "running",
"services": {
"coder_url": "https://coder.john-gallia-f280e93ffec7e79d.containers.adom.inc",
"s3_url": "https://s3.john-gallia-f280e93ffec7e79d.containers.adom.inc",
"ssh_credentials": {
"hostname": "ssh.containers.adom.inc",
"port": 2222,
"username": "john-gallia-f280e93ffec7e79d"
}
}
}
]
Create Container
POST /containers
Request Body:
{
"owner": "john",
"repository": "gallia",
"actor": "john",
"resource_limits": {
"cpu_shares": 512,
"vcpu_count": 2.0,
"memory_soft_limit": 536870912,
"memory_hard_limit": 1073741824
}
}
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
owner | string | yes | -- | Project owner username |
repository | string | yes | -- | Project/repo name |
actor | string | yes | -- | User performing the action |
resource_limits.cpu_shares | int | no | 512 | Relative CPU priority |
resource_limits.vcpu_count | float | no | 2.0 | vCPU allocation (0.1-8) |
resource_limits.memory_soft_limit | int | no | 536870912 | Memory reservation in bytes (512 MiB) |
resource_limits.memory_hard_limit | int | no | 1073741824 | Memory max in bytes (1 GiB). Range: 128 MiB - 8 GiB |
Response (201):
{
"id": "john-gallia-f280e93ffec7e79d",
"owner": "john",
"repository": "gallia",
"unique_hash": "f280e93ffec7e79d",
"services": { "..." }
}
What happens internally:
- Fetches user orgs from Carbon (
/users/{actor}/orgs) - Creates API key via Carbon (
/internal/containers/create-api-key) - Copies project files from
{DATA_DIR}/public/projects/{owner}/{name}/latest - Creates isolated Docker bridge network named
{container_id} - Connects Traefik to the new network
- Creates container with Traefik labels for Coder (port 8080) and S3 (port 9000)
- Mounts project dir at
/home/adom/project, API key at/var/run/adom/api-key - Starts the container
Get Container
GET /containers/{id}
Response (200): Container object with status. Returns 404 if not found.
Delete Container
DELETE /containers/{id}?commit_state={bool}
| Param | Type | Required | Description |
|---|---|---|---|
commit_state | bool | yes | If true, saves container state before deletion |
Response (204)
When commit_state=true:
- Backs up current
latest/tohistory/{uuid}.tar.gz - Replaces
latest/with current container filesystem
Then: force-kills container, removes volumes, disconnects Traefik, deletes network.
Pause Container
POST /containers/{id}/pause
Response (204). Suspends all processes without removing the container.
Resume Container
POST /containers/{id}/resume
Response (204). Resumes a paused container.
Stop Container
POST /containers/{id}/stop
Response (204). Stops container without removing it.
Start Container
POST /containers/{id}/start
Response (204). Starts a stopped container.
Commit Container State
POST /containers/{id}/commit
Response (204). Creates a backup archive and replaces latest/ with current filesystem.
Stream Container Stats
GET /containers/{id}/stats
Without SSE: Returns a single JSON stats snapshot (Docker ContainerStatsResponse format: CPU, memory, network I/O, block I/O, PIDs).
With SSE (set Accept: text/event-stream): Streams stats as SSE events:
event: stats
data: {"cpu_stats": {...}, "memory_stats": {...}, ...}
Prune Containers
POST /containers/prune
Response (204). Cleans up:
- Non-existent containers labeled
adom-user-container - Disconnects Traefik from orphaned networks
- Deletes orphaned networks
- Removes stale working directories, Traefik configs, and TLS certs
Container Lifecycle
created --[start]--> running
running --[pause]--> paused
paused --[resume]--> running
running --[stop]--> exited
exited --[start]--> running
any --[delete]--> removed
Workcell Endpoints
Workcells are physical hardware units (e.g., Raspberry Pis) with USB devices that can be associated with containers.
List Workcells
GET /workcells
Response (200):
[
{
"workcell_id": "raspberry-pi-001",
"ip_address": "192.168.1.100",
"connected_at": "2026-03-06T12:15:30Z",
"container_id": "john-gallia-f280e93ffec7e79d",
"associated_at": "2026-03-06T12:16:45Z",
"bound_devices": [
{
"bus_id": "1-1",
"bound_at": "2026-03-06T12:16:50Z",
"dev_node": "/dev/ttyUSB0",
"subsystem": "tty",
"manufacturer": "Silicon Labs",
"product": "CP2102 USB to UART Bridge Controller",
"serial_number": "0001",
"id_vendor": 1659,
"id_product": 6015,
"...": "additional device metadata"
}
]
}
]
Get Workcell
GET /workcells/{id}
Response (200): Workcell object with bound devices. Returns 404 if not found.
Associate Container with Workcell
PUT /workcells/{id}/association
Request Body:
{
"container_id": "john-gallia-f280e93ffec7e79d"
}
Response (200): Updated workcell object.
Attaches all bound USB devices to the container via USB/IP.
Disassociate Container from Workcell
DELETE /workcells/{id}/association
Response (200): Updated workcell object (container_id becomes null).
Detaches all USB devices and cleans up USB/IP ports.
Workcell WebSocket (Device Notifications)
GET /workcells/usbip-notify (WebSocket upgrade)
Used by workcells to report device hot-plug events:
Initial handshake (client sends):
{
"workcell_id": "raspberry-pi-001",
"ip_address": "192.168.1.100"
}
Device bound (client sends):
{
"type": "device_bound",
"bus_id": "1-1",
"metadata": { "dev_node": "/dev/ttyUSB0", "manufacturer": "...", "..." }
}
Device removed (client sends):
{
"type": "device_removed",
"bus_id": "1-1"
}
Networking & Routing
Each container gets its own Docker bridge network. Traefik connects to each network and routes traffic based on hostname:
| Service | Hostname Pattern | Port |
|---|---|---|
| Coder (VS Code) | coder.{container_id}.{BASE_HOSTNAME} | 8080 |
| S3 | s3.{container_id}.{BASE_HOSTNAME} | 9000 |
| SSH | ssh.{BASE_HOSTNAME}:2222 (username = container_id) | 2222 |
Default BASE_HOSTNAME: containers.adom.inc (production) or containers.adom.localhost (dev).
TLS via Let's Encrypt on the websecure entrypoint (port 443).
Directory Structure on Host
{DATA_DIR}/
public/
containers/{owner}/{repo}/{hash}/ # Working dir -> /home/adom/project
projects/{owner}/{repo}/
latest/ # Current project files
history/{uuid}.tar.gz # Committed snapshots
molecules/{scope}/ # Shared molecule dirs
private/
container-api-keys/{owner}/{repo}/{hash} # -> /var/run/adom/api-key
traefik/dynamic-config/ # Per-container routing rules
traefik/dynamic-certificates/ # Per-container TLS certs
Resource Defaults
| Resource | Default | Range |
|---|---|---|
| CPU shares | 512 | -- |
| vCPU count | 2.0 | 0.1 - 8 |
| Memory soft limit | 512 MiB | -- |
| Memory hard limit | 1 GiB | 128 MiB - 8 GiB |
Configuration (Environment Variables)
| Var | Default | Description |
|---|---|---|
BIND_ADDR | 0.0.0.0 | Server bind address |
PORT | 3000 | Server port |
DATA_DIRECTORY_EXTERNAL | -- | External data path |
DATA_DIRECTORY_INTERNAL | -- | Internal data path |
BASE_HOSTNAME | containers.adom.localhost | Base hostname for routing |
USER_CONTAINER_IMAGE | adom/user-containers/default:latest | Docker image for containers |
DOCKER_SOCKET_PATH | /var/run/docker.sock | Docker socket |
KEEP_CONTAINER_WORKING_DIRECTORY | -- | Set to 1 to preserve dirs on delete |
Error Responses
{
"code": 500,
"error": "INTERNAL_SERVER_ERROR",
"message": "descriptive error message"
}
Standard HTTP status codes: 200, 201, 204, 404, 500.
Quick Examples
Create a container:
curl -s -X POST https://curium.adom.inc/containers \
-H 'Content-Type: application/json' \
-d '{
"owner": "john",
"repository": "my-project",
"actor": "john",
"resource_limits": {
"vcpu_count": 4.0,
"memory_hard_limit": 4294967296
}
}'
Get container stats:
curl -s https://curium.adom.inc/containers/john-my-project-abc123def456/stats
Pause and resume:
curl -s -X POST https://curium.adom.inc/containers/john-my-project-abc123def456/pause
curl -s -X POST https://curium.adom.inc/containers/john-my-project-abc123def456/resume
Delete with state commit:
curl -s -X DELETE 'https://curium.adom.inc/containers/john-my-project-abc123def456?commit_state=true'
List containers for a user:
curl -s 'https://curium.adom.inc/containers?owner=john'
Associate a workcell:
curl -s -X PUT https://curium.adom.inc/workcells/raspberry-pi-001/association \
-H 'Content-Type: application/json' \
-d '{"container_id": "john-gallia-f280e93ffec7e79d"}'