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 username
  • repository: project/repo name
  • hex_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:

  1. Fetches user orgs from Carbon (/users/{actor}/orgs)
  2. Creates API key via Carbon (/internal/containers/create-api-key)
  3. Copies project files from {DATA_DIR}/public/projects/{owner}/{name}/latest
  4. Creates isolated Docker bridge network named {container_id}
  5. Connects Traefik to the new network
  6. Creates container with Traefik labels for Coder (port 8080) and S3 (port 9000)
  7. Mounts project dir at /home/adom/project, API key at /var/run/adom/api-key
  8. 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/ to history/{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"}'