Propose a change
Name Last commit message Last updated
install.sh Add adompkg installer files 1mo ago
package.json Add adompkg installer files 1mo ago
page.json Remove internal identifiers from page.json 10d ago
README.md Add adompkg installer files 1mo ago
SKILL.md Add adompkg installer files 1mo ago
uninstall.sh Add adompkg installer files 1mo ago

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"}'