runpod: support private container registries

Capability pods run YOUR image, and nobody publishes their whole stack
publicly — but create_pod never sent a credential, so a private image
would fail to pull and the pod would just never become healthy, with
nothing in the logs pointing at why.

RunPod stores registry credentials for you (Settings -> Container Registry
Credentials) and gives each set an id; that id now rides along as
`containerRegistryAuthId`, from `runpod.registry_auth_id` account-wide or
a pod block's own `registry_auth_id`. Public images (vLLM, llama.cpp) are
unaffected — they send nothing.

docs/remote-execution.md now says where the images come from: LLM pods
need no publishing at all, a capability pod needs a coderai image in a
registry RunPod can reach, and the 26.6 GB flattened image is paid on
every cold pull.
Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0182DhR1eFNPQyGmDrpHbedY
parent 3fb97ff1
......@@ -3635,6 +3635,7 @@ def build_settings_dict(c, gpu_cards):
"serverless_base": c.runpod.serverless_base,
"default_gpu_type": c.runpod.default_gpu_type,
"data_center": c.runpod.data_center,
"registry_auth_id": c.runpod.registry_auth_id,
"deployment_id": c.runpod.deployment_id,
"global_max_hourly_usd": c.runpod.global_max_hourly_usd,
"global_cost_limit_usd": c.runpod.global_cost_limit_usd,
......@@ -4209,6 +4210,8 @@ async def api_save_settings(request: Request, username: str = Depends(require_ad
rp.default_gpu_type = (d.get("default_gpu_type") or "").strip()
if "data_center" in d:
rp.data_center = (d.get("data_center") or "").strip()
if "registry_auth_id" in d:
rp.registry_auth_id = (d.get("registry_auth_id") or "").strip()
if "deployment_id" in d:
rp.deployment_id = (d.get("deployment_id") or "default").strip() or "default"
if "global_max_hourly_usd" in d:
......
......@@ -182,7 +182,8 @@ class RunpodClient:
container_disk_gb: int = 40, volume_gb: int = 0,
volume_mount_path: str = "/workspace", env: Optional[dict] = None,
docker_args: str = "", is_spot: bool = False,
bid_per_gpu: float = 0.0, data_center_id: str = "") -> str:
bid_per_gpu: float = 0.0, data_center_id: str = "",
registry_auth_id: str = "") -> str:
"""Provision a pod (on-demand or interruptible/spot). Returns the pod id."""
env_list = [{"key": str(k), "value": str(v)} for k, v in (env or {}).items()]
ports = f"{int(port)}/http"
......@@ -201,6 +202,11 @@ class RunpodClient:
}
if data_center_id:
common["dataCenterId"] = data_center_id
# A private image needs credentials RunPod holds for you: create them under
# Settings -> Container Registry Credentials and paste the id here. Public
# images (vLLM, llama.cpp, a public coderai) need none.
if registry_auth_id:
common["containerRegistryAuthId"] = registry_auth_id
if is_spot:
common["bidPerGpu"] = float(bid_per_gpu)
mutation = "podRentInterruptable"
......
......@@ -56,6 +56,9 @@ class RunpodModelConfig:
# Verbatim docker args, replacing whatever the engine would generate. The
# escape hatch for `engine: custom` and for images with their own CLI.
docker_args: str = ""
# RunPod container-registry credential id for a private image; blank falls
# back to the account-wide one.
registry_auth_id: str = ""
served_model: str = "" # HF id the pod/endpoint serves
# GGUF to pull on a llama.cpp pod, in llama.cpp's own `-hf` form:
# "user/repo:Q4_K_M" or "user/repo:file.gguf". Needed because a local
......@@ -226,6 +229,7 @@ def parse_model_runpod(block: Optional[dict]) -> RunpodModelConfig:
cfg.engine = (b.get("engine") or cfg.engine).strip().lower() or "auto"
cfg.health_path = (b.get("health_path") or "").strip()
cfg.docker_args = (b.get("docker_args") or "").strip()
cfg.registry_auth_id = (b.get("registry_auth_id") or "").strip()
cfg.served_model = (b.get("served_model") or "").strip()
cfg.hf_gguf = (b.get("hf_gguf") or "").strip()
cfg.container_disk_gb = _as_int(b.get("container_disk_gb"), cfg.container_disk_gb)
......@@ -491,7 +495,9 @@ class RunpodPodPool:
name=name, image=image, gpu_type_id=sel["gpu_type_id"], port=port,
cloud_type=sel["cloud_type"], container_disk_gb=self.mcfg.container_disk_gb,
volume_gb=self.mcfg.volume_gb, env=env, docker_args=args,
is_spot=sel["is_spot"], bid_per_gpu=sel["bid"], data_center_id=dc)
is_spot=sel["is_spot"], bid_per_gpu=sel["bid"], data_center_id=dc,
registry_auth_id=(self.mcfg.registry_auth_id
or getattr(self.account, "registry_auth_id", "") or ""))
return pod_id, sel, i + 1
except RunpodError as exc:
msg = str(exc).lower()
......
......@@ -629,6 +629,10 @@ class RunpodConfig:
serverless_base: str = "https://api.runpod.ai/v2" # serverless invoke base
default_gpu_type: str = "" # fallback RunPod gpuTypeId when a model gives none
data_center: str = "" # optional data-center id filter (blank = any)
# RunPod "Container Registry Credentials" id, for pods whose image lives in a
# private registry (your own coderai image, typically). Per-model/capability
# `registry_auth_id` overrides this. Blank = public images only.
registry_auth_id: str = ""
# Stable tag baked into every pod name (coderai-<deployment_id>-<model>-<rand>) so the
# reaper can identify OUR pods across restarts and never touch another deployment's.
deployment_id: str = "default"
......@@ -1207,6 +1211,7 @@ class ConfigManager:
"serverless_base": self.config.runpod.serverless_base,
"default_gpu_type": self.config.runpod.default_gpu_type,
"data_center": self.config.runpod.data_center,
"registry_auth_id": self.config.runpod.registry_auth_id,
"deployment_id": self.config.runpod.deployment_id,
"global_max_hourly_usd": self.config.runpod.global_max_hourly_usd,
"global_cost_limit_usd": self.config.runpod.global_cost_limit_usd,
......
......@@ -184,8 +184,50 @@ Pod engines (`engine`):
| `custom` | your `image`, `docker_args` verbatim | `health_path` |
`coderai` is the default for a capability pool: the far side is a whole coderai,
reached through the same API. You must supply `image` — a registry the pod can
pull from — because coderai does not publish one for you.
reached through the same API.
### Where the images come from
RunPod pulls the image from a registry over the public internet — it cannot use
anything on your machine. What you need depends on the engine:
| Engine | Image | You publish anything? |
|---|---|---|
| `vllm` | `vllm/vllm-openai:latest` | no — public |
| `llamacpp` | `ghcr.io/ggml-org/llama.cpp:server-cuda` | no — public |
| `coderai` / `custom` | yours | **yes** |
So LLM pods need no work at all. A capability pod needs a coderai image somewhere
RunPod can reach. Any registry works:
* **GHCR** (`ghcr.io/<you>/coderai`) — the one `DISTRIBUTION.md` targets, free for
public images, tied to a GitHub token for private ones.
* **Docker Hub** — one free private repo; watch the pull-rate limits.
* **AWS ECR / Google Artifact Registry / Azure ACR** — if a pod runs in the same
region the pull is fast and egress is cheap.
* **Your own registry** — needs to be reachable from RunPod's network and to
serve valid TLS.
Publishing it:
```bash
docker tag coderai:base ghcr.io/<you>/coderai:base
docker login ghcr.io -u <you> # a PAT with write:packages
docker push ghcr.io/<you>/coderai:base
```
Two warnings worth having in advance. The flattened `coderai:base` is ~26.6 GB —
the first push takes a while, and **every cold pod pays that pull** before it
answers `/healthz`, so raise `boot_timeout_s`/`load_timeout_s` and consider a
larger `container_disk_gb`. And a full coderai carries every subsystem; if a pod
only ever renders video, a trimmed image boots far faster and costs less per
cold start.
**Private registry:** RunPod stores the credentials for you — *Settings →
Container Registry Credentials* — and gives each set an id. Put that id in
`runpod.registry_auth_id` (account-wide) or in a pod block's `registry_auth_id`
(per model or capability). Without it a private image fails to pull and the pod
never becomes healthy. Public images need none.
If a pod cannot be provisioned (RunPod disabled, budget cap hit, no GPU under
the ceiling) the request fails with 502. It does **not** quietly fall back to
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment