Platform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Models Tools & Services
Solutions
Organisations Developers Internet Service Providers Managed Service Providers AI-in-a-Box
Resources
Support Documentation Blog Downloads
Company
About Research Careers Investment Opportunities Contact
Log in

Register a custom image

You're going to register a custom ext4 image baked from an OCI source (e.g. docker.io/r-base:4.4), wait for it to fan out to your tenant's workers, and then create a bunker from it.

Roughly 5 minutes plus the bake time (a few seconds to a couple of minutes depending on image size).

1. Decide the image's shape#

Settle these before the API call:

  • Build source. OCI ({kind: "oci", ref: "docker.io/r-base:4.4"}) for anything pullable from a registry. tar for tarballs your worker can read locally — air-gapped deployments, hand-built rootfs.
  • Size. size_mib is the cap on the resulting ext4 file. Too small → mkfs.ext4 errors at bake time. Pick generously; 2048 is a sensible floor for non-minimal images.
  • Defaults. default_cpu_millicores, default_memory_mb, default_disk_mb are applied when a caller creates a bunker from this image without overriding. Set them to a sensible "this image needs at least this to function" floor.
  • Scope. tenant (default) — only this tenant sees it. partner — every tenant under your partner. platform — visible to every tenant; only super-admins can register at this scope.
  • Lazy pull. lazy_pull: true lets the scheduler place a bunker on a worker that doesn't have the image cached yet (slower first boot). The default false requires the image to be ready on a worker before that worker is eligible.

You need scaibunker:images:manage to register a new image.

2. Register the image#

POST to /images with the build source, size cap, and defaults. The response includes the new image id plus a warm summary if any workers were already in scope for fan-out.

bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
curl -X POST "$SCAIGRID_HOST/v1/modules/scaibunker/images" \
  -H "Authorization: Bearer $SCAIGRID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "r-stats",
    "display_name": "R 4.4 with tidyverse",
    "description": "R statistical computing environment with the tidyverse pre-installed.",
    "build_source": {"kind": "oci", "ref": "docker.io/r-base:4.4"},
    "size_mib": 4096,
    "default_cpu_millicores": 2000,
    "default_memory_mb": 4096,
    "default_disk_mb": 8192,
    "preinstalled": ["R", "tidyverse", "ggplot2", "dplyr"]
  }'
python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import httpx, os

H = {"Authorization": f"Bearer {os.environ['SCAIGRID_API_KEY']}"}
HOST = os.environ["SCAIGRID_HOST"]

image = httpx.post(
    f"{HOST}/v1/modules/scaibunker/images",
    headers=H,
    json={
        "name": "r-stats",
        "display_name": "R 4.4 with tidyverse",
        "build_source": {"kind": "oci", "ref": "docker.io/r-base:4.4"},
        "size_mib": 4096,
        "default_cpu_millicores": 2000,
        "default_memory_mb": 4096,
        "default_disk_mb": 8192,
        "preinstalled": ["R", "tidyverse", "ggplot2", "dplyr"],
    },
).json()["data"]
print(image["id"], "warm queued:", image.get("warm"))
javascript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
const HOST = process.env.SCAIGRID_HOST;
const H = { "Authorization": `Bearer ${process.env.SCAIGRID_API_KEY}` };

const res = await fetch(`${HOST}/v1/modules/scaibunker/images`, {
  method: "POST",
  headers: { ...H, "Content-Type": "application/json" },
  body: JSON.stringify({
    name: "r-stats",
    display_name: "R 4.4 with tidyverse",
    build_source: { kind: "oci", ref: "docker.io/r-base:4.4" },
    size_mib: 4096,
    default_cpu_millicores: 2000,
    default_memory_mb: 4096,
    default_disk_mb: 8192,
    preinstalled: ["R", "tidyverse", "ggplot2", "dplyr"],
  }),
});
const { data: image } = await res.json();
console.log(image.id, "warm queued:", image.warm);

The response includes a warm object summarising the fan-out: {queued, ready, failed, skipped}. If your tenant's availability-group setup already covers some workers, those will show as ready or queued immediately.

3. Watch fan-out progress#

GET the image's cache view to see one row per targeted worker. Poll until every row reports status: ready.

bash
1
2
curl "$SCAIGRID_HOST/v1/modules/scaibunker/images/$IMAGE_ID/cache" \
  -H "Authorization: Bearer $SCAIGRID_API_KEY"

You'll get one row per worker that's been targeted, with status in pending, building, ready, failed, or evicted. Re-poll until every row is ready — at that point, every worker in the targeting groups has the ext4 cached and can launch bunkers from it instantly.

If a worker shows failed, the error field carries the worker's response (registry auth issue, size cap hit, OOM during mkfs, etc.). Fix the cause and re-trigger:

bash
1
2
curl -X POST "$SCAIGRID_HOST/v1/modules/scaibunker/images/$IMAGE_ID/warm" \
  -H "Authorization: Bearer $SCAIGRID_API_KEY"

The warm route is idempotent — workers that already have the image come back instantly.

4. Assign to an availability group (if needed)#

If your tenant's images don't yet sit in any group, the fan-out will have done nothing. Create a group and put the image in it:

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
group = httpx.post(
    f"{HOST}/v1/modules/scaibunker/availability-groups",
    headers=H,
    json={"name": "stats-tenant-default", "description": "R + Python stats workers"},
).json()["data"]
print("group:", group["id"])

# Attach this image
httpx.post(
    f"{HOST}/v1/modules/scaibunker/availability-groups/{group['id']}/images",
    headers=H,
    json={"ref": image["id"]},
)

Adding a worker to the group (POST /availability-groups/{id}/workers) will then trigger a warm of every image already in the group on that newly-joined worker — same machinery, no extra calls.

5. Wait for the security scan#

Every registered image is scanned automatically. The image row carries:

  • scan_status: pending — queued; the background task runs every 2 minutes.
  • scan_status: passed — no critical or high CVEs.
  • scan_status: failed — CVEs found, or the scanner itself failed (missing binary, registry error). Failures don't block bunker creation; the status is informational.
  • scan_status: skipped — image source has no scannable OCI ref.

To re-scan on demand:

bash
1
2
curl -X POST "$SCAIGRID_HOST/v1/modules/scaibunker/images/$IMAGE_ID/scan" \
  -H "Authorization: Bearer $SCAIGRID_API_KEY"

6. Create a bunker from the new image#

Reference the image by name in the bunker create. The scheduler will pick a worker with the image cached ready.

bash
1
2
3
4
5
6
7
8
curl -X POST "$SCAIGRID_HOST/v1/modules/scaibunker/bunkers" \
  -H "Authorization: Bearer $SCAIGRID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "image": "r-stats",
    "lifecycle_mode": "ephemeral",
    "network_profile": "registry"
  }'

The scheduler will pick a worker with the image cached ready. If none yet, the call fails with NO_SUITABLE_WORKER — wait for the fan-out to complete, or set lazy_pull: true on the image to allow placement on cold workers.

7. Deactivate when retiring#

When the image is end-of-life, deactivate it instead of leaving it around:

bash
1
2
curl -X DELETE "$SCAIGRID_HOST/v1/modules/scaibunker/images/$IMAGE_ID" \
  -H "Authorization: Bearer $SCAIGRID_API_KEY"

Deactivation is a soft-delete: existing bunkers keep running, but new bunkers can't be created from it. Per-worker cache entries get garbage-collected by the scanner background task on its next sweep.

Done#

You have a custom image baked, fanned out to your workers, scanned, and ready for production. Iterate the source by registering a new image with a different name — there's no in-place update; you version by name.

Updated 2026-05-18 15:01:27 View source (.md) rev 12