Skip to content

Factories > Managed self-hosting

Pulling self-hosted images from a private registry

Open in ChatGPT ↗
Ask ChatGPT about this page
Open in Claude ↗
Ask Claude about this page
Copied!

Mirror self-hosted worker images into a private registry and configure Docker or Kubernetes workers to pull from it.

Mirror the images used by self-hosted workers when compute hosts cannot pull directly from a public registry. Your registry becomes the pull source for the worker process, task environment, Warp Agent sidecar, and Kubernetes preflight Job.

Inventory the images used by each worker pool before blocking public-registry egress.

ImagePurposeConfiguration
warpdotdev/oz-agent-workerLong-lived worker daemonDocker run image or Helm image.repository
Environment or default imageMain task filesystem and toolchainWarp environment image or Kubernetes defaultImage
warpdotdev/warp-agent:latestWarp Agent runtime mounted at /agentsidecar_image or Helm kubernetesBackend.sidecarImage
busybox:1.36Kubernetes startup preflightpreflight_image or Helm kubernetesBackend.preflightImage

Pin the worker image to the immutable timestamp tag or digest from the worker release. The supported self-hosted sidecar reference is warpdotdev/warp-agent:latest, listed on the Warp Agent tags page. Refresh the mirror regularly. To pin the sidecar, set its private-registry override to a mirrored digest and update that digest when Warp publishes a new sidecar.

Use a registry copy tool that preserves all image architectures. The following example uses Skopeo. Replace WORKER_RELEASE_TAG with the immutable tag from the worker release.

Terminal window
export PRIVATE_REGISTRY="registry.internal.example.com/warp"
skopeo login registry.internal.example.com
skopeo copy --all \
"docker://docker.io/warpdotdev/oz-agent-worker:WORKER_RELEASE_TAG" \
"docker://${PRIVATE_REGISTRY}/oz-agent-worker:WORKER_RELEASE_TAG"
skopeo copy --all \
"docker://docker.io/warpdotdev/warp-agent:latest" \
"docker://${PRIVATE_REGISTRY}/warp-agent:latest"
skopeo copy --all \
"docker://docker.io/library/busybox:1.36" \
"docker://${PRIVATE_REGISTRY}/busybox:1.36"
skopeo copy --all \
"docker://docker.io/library/ubuntu:22.04" \
"docker://${PRIVATE_REGISTRY}/agent-base:22.04"

The example mirrors Ubuntu as the task image. Replace that source with your own task image, then set the private image reference on the Warp environment used by the worker pool.

The Docker configuration below requires worker release v2026-08-28-21-43-14 or newer.

The Docker backend uses the worker’s Docker config to authenticate task-image pulls. Warp Agent sidecar pulls do not use those credentials.

If the registry requires authentication, follow Private Docker registries to create and mount a dedicated Docker config for a containerized worker. Because sidecar pulls do not use credentials, pre-pull every task and sidecar image before setting the pull policy to Never:

Terminal window
export WORKER_DOCKER_CONFIG="/var/lib/oz-agent-worker/docker-config"
sudo docker --config "$WORKER_DOCKER_CONFIG" \
pull registry.internal.example.com/warp/agent-base:22.04
sudo docker --config "$WORKER_DOCKER_CONFIG" \
pull registry.internal.example.com/warp/warp-agent:latest

If the worker runs as a host process with credentials in your default Docker config, run the same commands without sudo or --config "$WORKER_DOCKER_CONFIG".

Before starting a containerized worker, run docker login registry.internal.example.com on the host so Docker can pull the mirrored worker image.

Set the pull policy to Never so the worker uses the local images:

worker.yaml
worker_id: "private-registry-docker"
backend:
docker:
image_pull_policy: "Never"
sidecar_image: "registry.internal.example.com/warp/warp-agent:latest"

For a sidecar repository that allows anonymous reads, use image_pull_policy: "Always" instead. The sidecar uses the mutable latest tag. The pull policy applies to task and sidecar images.

With Never, local images do not update automatically. After refreshing the mirrored warp-agent:latest, repeat the sidecar pre-pull command on every worker host before routing another run.

The task image must point to the private registry through its Warp environment; the worker does not rewrite task image registry names.

In the containerized worker command, use registry.internal.example.com/warp/oz-agent-worker:WORKER_RELEASE_TAG in place of the public worker image. Add --volume "$PWD/worker.yaml:/etc/oz-agent-worker/worker.yaml:ro" before the image and --config-file /etc/oz-agent-worker/worker.yaml after it. See the config file reference.

Start the worker with --log-level debug, then route a test run to the worker. Before blocking public-registry egress, confirm that the worker logs for Using Docker image and Preparing additional sidecar show private-registry references and the run succeeds.

Export a self-hosted worker API key as WARP_API_KEY, then create the namespace, API key Secret, and registry pull secret:

Terminal window
export WARP_API_KEY="YOUR_API_KEY"
kubectl create namespace warp-oz \
--dry-run=client \
--output yaml | kubectl apply --filename -
kubectl create secret generic oz-agent-worker \
--namespace warp-oz \
--from-literal=WARP_API_KEY="$WARP_API_KEY"
export REGISTRY_AUTH_DIR="$(mktemp -d)"
skopeo login \
--compat-auth-file "$REGISTRY_AUTH_DIR/config.json" \
registry.internal.example.com
kubectl create secret generic warp-registry \
--namespace warp-oz \
--type=kubernetes.io/dockerconfigjson \
--from-file=.dockerconfigjson="$REGISTRY_AUTH_DIR/config.json"

After the Secret is created, remove the temporary auth file:

Terminal window
rm -r "$REGISTRY_AUTH_DIR"
unset REGISTRY_AUTH_DIR

Set the worker, task, sidecar, and preflight image references in a Helm values file:

private-registry-values.yaml
warp:
apiKeySecret:
name: oz-agent-worker
image:
repository: registry.internal.example.com/warp/oz-agent-worker
tag: WORKER_RELEASE_TAG
pullPolicy: IfNotPresent
pullSecrets:
- name: warp-registry
kubernetesBackend:
defaultImage: registry.internal.example.com/warp/agent-base:22.04
imagePullPolicy: Always
preflightImage: registry.internal.example.com/warp/busybox:1.36
sidecarImage: registry.internal.example.com/warp/warp-agent:latest
podTemplate:
imagePullSecrets:
- name: warp-registry

When a run uses a Warp environment image, that image takes precedence over defaultImage. Set the private image reference on the Warp environment before blocking public-registry egress.

Before installing the worker, verify the preflight image with the same pull secret configured in kubernetesBackend.podTemplate:

Terminal window
kubectl delete job warp-preflight-image-check \
--namespace warp-oz \
--ignore-not-found
kubectl apply --filename - <<'EOF'
apiVersion: batch/v1
kind: Job
metadata:
name: warp-preflight-image-check
namespace: warp-oz
spec:
backoffLimit: 0
template:
spec:
restartPolicy: Never
imagePullSecrets:
- name: warp-registry
containers:
- name: check
image: registry.internal.example.com/warp/busybox:1.36
imagePullPolicy: Always
command: ["/bin/sh", "-c", "true"]
EOF
kubectl wait \
--namespace warp-oz \
--for=condition=complete \
--timeout=2m \
job/warp-preflight-image-check

The wait command must report condition met. After it completes, remove the temporary Job:

Terminal window
kubectl delete job warp-preflight-image-check --namespace warp-oz

From a network with GitHub access, clone the worker release and package its chart:

Terminal window
git clone \
--branch "WORKER_RELEASE_TAG" \
--depth 1 \
https://github.com/warpdotdev/oz-agent-worker.git
tar --create --gzip \
--file "oz-agent-worker-chart-WORKER_RELEASE_TAG.tar.gz" \
--directory oz-agent-worker/charts \
oz-agent-worker

Copy the archive to your approved internal artifact store. On the deployment host, extract it to an internal path:

Terminal window
mkdir --parents /srv/warp/charts
tar --extract --gzip \
--file "oz-agent-worker-chart-WORKER_RELEASE_TAG.tar.gz" \
--directory /srv/warp/charts

On the deployment host, set WORKER_CHART to the internal chart copy and install it with temporary verification settings:

Terminal window
export WORKER_CHART="/srv/warp/charts/oz-agent-worker"
helm upgrade --install oz-agent-worker "$WORKER_CHART" \
--namespace warp-oz \
--set worker.workerId=private-registry-kubernetes \
--set worker.logLevel=debug \
--set worker.cleanup=false \
--values private-registry-values.yaml

image.pullSecrets authenticates the long-lived worker Deployment. kubernetesBackend.podTemplate.imagePullSecrets authenticates task Jobs and the startup preflight Job.

Follow the worker logs until they show Successfully connected to server, then press Ctrl+C:

Terminal window
kubectl logs --follow deployment/oz-agent-worker --namespace warp-oz

A successful test run verifies the registry credentials by pulling and running the task and sidecar images.

With the Oz CLI (see Installing the CLI), start one run that uses the Warp Agent without Computer Use:

Terminal window
oz agent run-cloud \
--host "private-registry-kubernetes" \
--prompt "Print the operating system release and exit."

Cleanup is temporarily disabled, so the successful task Job and Pod remain after the command returns. Confirm the debug logs show the selected task and sidecar images:

Terminal window
kubectl logs deployment/oz-agent-worker --namespace warp-oz |
grep -E 'Using Kubernetes task image|Overriding server sidecar image'

Select the newest Job for this worker, derive its Pod name, and inspect every image reference:

Terminal window
TASK_JOB="$(kubectl get jobs \
--namespace warp-oz \
--selector oz-worker-id=private-registry-kubernetes \
--sort-by=.metadata.creationTimestamp \
--output name | tail -n 1)"
TASK_POD="$(kubectl get pods \
--namespace warp-oz \
--selector "job-name=${TASK_JOB#*/}" \
--output jsonpath='{.items[0].metadata.name}')"
kubectl get pod "$TASK_POD" --namespace warp-oz \
--output jsonpath='{range .spec.initContainers[*]}{.image}{"\n"}{end}{range .spec.containers[*]}{.image}{"\n"}{end}'

For this Kubernetes worker, block public-registry egress only after the preflight image check completes, the worker connects, the test run succeeds, and every task Pod image uses the private registry. Restore the default log level and cleanup behavior, then remove the retained test Job:

Terminal window
helm upgrade oz-agent-worker "$WORKER_CHART" \
--namespace warp-oz \
--set worker.workerId=private-registry-kubernetes \
--set worker.logLevel=info \
--set worker.cleanup=true \
--values private-registry-values.yaml
kubectl delete "$TASK_JOB" --namespace warp-oz

For a task-image failure, confirm the worker’s Docker config contains registry credentials. For a sidecar failure, allow anonymous reads from the mirrored sidecar repository, or pre-pull the image and use image_pull_policy: "Never".

Confirm warp-registry exists in the task namespace. The worker Deployment needs image.pullSecrets, while task and preflight Pods need podTemplate.imagePullSecrets.