refactor: remove Docker install wrapper, tarballs replace it (#2244)

Docker delivery is superseded by the tarball approach (#2232) which is
faster (curl|tar ~5-15s vs Docker install ~30s + pull ~60s) and works
on every cloud without Docker as a dependency.

- Remove tryInstallFromDocker, withDockerInstall, DOCKER_IMAGE_PREFIX
- Remove dockerImage and slowInstall from AgentConfig
- Remove Docker cloud-init from DigitalOcean
- Unwrap openclaw and zeroclaw to direct install (tarball is tried
  first in orchestrate.ts, these are the fallback)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ahmed Abushagur 2026-03-06 11:19:22 -08:00 committed by GitHub
parent 8b99fe0a37
commit 849e980bf3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 9 additions and 107 deletions

View file

@ -1,6 +1,6 @@
{
"name": "@openrouter/spawn",
"version": "0.15.0",
"version": "0.15.1",
"type": "module",
"bin": {
"spawn": "cli.js"

View file

@ -740,29 +740,16 @@ export async function promptDoRegion(): Promise<string> {
// ─── Provisioning ────────────────────────────────────────────────────────────
function getCloudInitUserdata(tier: CloudInitTier = "full", agentName?: string): string {
function getCloudInitUserdata(tier: CloudInitTier = "full"): string {
const packages = getPackagesForTier(tier);
const lines = [
"#!/bin/bash",
"set -e",
"export HOME=/root",
"export DEBIAN_FRONTEND=noninteractive",
"apt-get update -y",
`apt-get install -y --no-install-recommends ${packages.join(" ")}`,
];
if (agentName) {
// Install Docker FIRST (uses apt internally), then start image pull in background.
// The pull runs in parallel with the remaining apt-get/node/bun installs below.
if (!/^[a-z0-9-]+$/.test(agentName)) {
throw new Error(`Invalid agent name: ${agentName}`);
}
lines.push(
"curl -fsSL https://get.docker.com | sh",
`nohup docker pull "ghcr.io/openrouterteam/spawn-${agentName}:latest" > /tmp/docker-pull.log 2>&1 &`,
);
}
// Install remaining packages (runs in parallel with docker pull above)
lines.push("apt-get update -y", `apt-get install -y --no-install-recommends ${packages.join(" ")}`);
if (needsNode(tier)) {
lines.push(`${NODE_INSTALL_CMD} || true`);
}
@ -784,7 +771,6 @@ export async function createServer(
tier?: CloudInitTier,
dropletSize?: string,
region?: string,
agentName?: string,
): Promise<void> {
const size = dropletSize || process.env.DO_DROPLET_SIZE || "s-2vcpu-4gb";
const effectiveRegion = region || process.env.DO_REGION || "nyc3";
@ -811,7 +797,7 @@ export async function createServer(
size,
image,
ssh_keys: sshKeyIds,
user_data: getCloudInitUserdata(tier, agentName),
user_data: getCloudInitUserdata(tier),
backups: false,
monitoring: false,
});

View file

@ -57,7 +57,7 @@ async function main() {
},
async createServer(name: string, spawnId?: string) {
process.env.SPAWN_ID = spawnId || "";
await createDroplet(name, agent.cloudInitTier, dropletSize, region, agent.slowInstall ? agentName : undefined);
await createDroplet(name, agent.cloudInitTier, dropletSize, region);
},
getServerName,
async waitForReady() {

View file

@ -524,75 +524,6 @@ const NPM_PREFIX_SETUP =
'mkdir -p ~/.npm-global/bin; _NPM_G_FLAGS="--prefix $HOME/.npm-global"; fi; ' +
'export PATH="$HOME/.npm-global/bin:$PATH"';
// ─── Docker Image Extraction ──────────────────────────────────────────────────
const DOCKER_IMAGE_PREFIX = "ghcr.io/openrouterteam/spawn-";
/**
* Try to extract a pre-built agent from a Docker image pulled during cloud-init.
* Returns true if extraction succeeded, false if Docker/image unavailable.
*
* How it works:
* 1. Check if Docker is installed and the image was pulled
* 2. Create a temporary container from the image
* 3. Copy /root/. from the container to /root/ on the host
* 4. Remove the temporary container
*
* The agent then runs natively on the host (not in a container).
*/
async function tryInstallFromDocker(runner: CloudRunner, agentName: string, dockerImage: string): Promise<boolean> {
logStep(`Checking for pre-built Docker image (${agentName})...`);
const script = [
// Bail if Docker isn't installed
"command -v docker >/dev/null 2>&1 || { echo '==> Docker not installed'; exit 1; }",
// Poll for image availability — cloud-init started the pull in the background.
// We can't rely on pgrep because the docker CLI exits while dockerd continues pulling.
"_elapsed=0; _max=300",
`while ! docker images -q "${dockerImage}" 2>/dev/null | grep -q .; do`,
` if [ $_elapsed -ge $_max ]; then echo "==> Timed out waiting for ${dockerImage}"; exit 1; fi`,
' if [ $_elapsed -eq 0 ]; then echo "==> Waiting for Docker image pull to complete..."; fi',
" sleep 5; _elapsed=$((_elapsed + 5))",
"done",
// Create temp container, copy only known agent directories, clean up
`_cid=$(docker create "${dockerImage}")`,
'docker cp "$_cid":/root/.claude /root/ 2>/dev/null || true',
'docker cp "$_cid":/root/.bun /root/ 2>/dev/null || true',
'docker cp "$_cid":/root/.local /root/ 2>/dev/null || true',
'docker cp "$_cid":/root/.npm /root/ 2>/dev/null || true',
'docker cp "$_cid":/root/.cargo /root/ 2>/dev/null || true',
'docker cp "$_cid":/root/.opencode /root/ 2>/dev/null || true',
'docker rm "$_cid" >/dev/null',
'echo "==> Agent extracted from Docker image"',
].join("\n");
try {
await runner.runServer(script, 600);
logInfo(`${agentName} extracted from Docker image`);
return true;
} catch {
logInfo("Docker image not available, falling back to normal install");
return false;
}
}
/**
* Wrap an agent's install function with Docker image extraction.
* Tries Docker first; falls back to the original install if unavailable.
*/
function withDockerInstall(
runner: CloudRunner,
agentName: string,
dockerImage: string,
originalInstall: () => Promise<void>,
): () => Promise<void> {
return async () => {
const extracted = await tryInstallFromDocker(runner, agentName, dockerImage);
if (!extracted) {
await originalInstall();
}
};
}
// ─── Default Agent Definitions ───────────────────────────────────────────────
const ZEROCLAW_INSTALL_URL =
@ -603,7 +534,6 @@ function createAgents(runner: CloudRunner): Record<string, AgentConfig> {
claude: {
name: "Claude Code",
cloudInitTier: "minimal",
dockerImage: `${DOCKER_IMAGE_PREFIX}claude:latest`,
preProvision: promptGithubAuth,
install: () => installClaudeCode(runner),
envVars: (apiKey) => [
@ -622,7 +552,6 @@ function createAgents(runner: CloudRunner): Record<string, AgentConfig> {
codex: {
name: "Codex CLI",
cloudInitTier: "node",
dockerImage: `${DOCKER_IMAGE_PREFIX}codex:latest`,
preProvision: promptGithubAuth,
install: () =>
installAgent(
@ -642,12 +571,10 @@ function createAgents(runner: CloudRunner): Record<string, AgentConfig> {
openclaw: {
name: "OpenClaw",
cloudInitTier: "full",
dockerImage: `${DOCKER_IMAGE_PREFIX}openclaw:latest`,
slowInstall: true,
preProvision: promptGithubAuth,
modelPrompt: true,
modelDefault: "openrouter/auto",
install: withDockerInstall(runner, "OpenClaw", `${DOCKER_IMAGE_PREFIX}openclaw:latest`, () =>
install: () =>
installAgent(
runner,
"openclaw",
@ -655,7 +582,6 @@ function createAgents(runner: CloudRunner): Record<string, AgentConfig> {
"{ grep -qF '.npm-global/bin' ~/.bashrc 2>/dev/null || echo 'export PATH=\"$HOME/.npm-global/bin:$PATH\"' >> ~/.bashrc; } && " +
"{ [ ! -f ~/.zshrc ] || grep -qF '.npm-global/bin' ~/.zshrc 2>/dev/null || echo 'export PATH=\"$HOME/.npm-global/bin:$PATH\"' >> ~/.zshrc; }",
),
),
envVars: (apiKey) => [
`OPENROUTER_API_KEY=${apiKey}`,
`ANTHROPIC_API_KEY=${apiKey}`,
@ -672,7 +598,6 @@ function createAgents(runner: CloudRunner): Record<string, AgentConfig> {
opencode: {
name: "OpenCode",
cloudInitTier: "minimal",
dockerImage: `${DOCKER_IMAGE_PREFIX}opencode:latest`,
preProvision: promptGithubAuth,
install: () => installAgent(runner, "OpenCode", openCodeInstallCmd()),
envVars: (apiKey) => [
@ -684,7 +609,6 @@ function createAgents(runner: CloudRunner): Record<string, AgentConfig> {
kilocode: {
name: "Kilo Code",
cloudInitTier: "node",
dockerImage: `${DOCKER_IMAGE_PREFIX}kilocode:latest`,
preProvision: promptGithubAuth,
install: () =>
installAgent(
@ -705,10 +629,8 @@ function createAgents(runner: CloudRunner): Record<string, AgentConfig> {
zeroclaw: {
name: "ZeroClaw",
cloudInitTier: "minimal",
dockerImage: `${DOCKER_IMAGE_PREFIX}zeroclaw:latest`,
slowInstall: true,
preProvision: promptGithubAuth,
install: withDockerInstall(runner, "ZeroClaw", `${DOCKER_IMAGE_PREFIX}zeroclaw:latest`, async () => {
install: async () => {
// Add swap before building — low-memory instances (e.g., AWS nano 512 MB)
// OOM during Rust compilation if --prefer-prebuilt falls back to source.
await ensureSwapSpace(runner);
@ -718,7 +640,7 @@ function createAgents(runner: CloudRunner): Record<string, AgentConfig> {
`curl --proto '=https' -LsSf ${ZEROCLAW_INSTALL_URL} | bash -s -- --install-rust --install-system-deps --prefer-prebuilt`,
600, // 10 min: swap-backed compilation is slower than the 5-min default
);
}),
},
envVars: (apiKey) => [
`OPENROUTER_API_KEY=${apiKey}`,
"ZEROCLAW_PROVIDER=openrouter",
@ -731,7 +653,6 @@ function createAgents(runner: CloudRunner): Record<string, AgentConfig> {
hermes: {
name: "Hermes Agent",
cloudInitTier: "minimal",
dockerImage: `${DOCKER_IMAGE_PREFIX}hermes:latest`,
preProvision: promptGithubAuth,
install: () =>
installAgent(

View file

@ -29,11 +29,6 @@ export interface AgentConfig {
launchCmd: () => string;
/** Cloud-init dependency tier. Defaults to "full" if unset. */
cloudInitTier?: CloudInitTier;
/** Docker image for pre-built agent extraction (e.g. "ghcr.io/openrouterteam/spawn-claude:latest"). */
dockerImage?: string;
/** If true, Docker + image pull are added to cloud-init for faster extraction.
* Only worth it for agents with slow installs (e.g. Rust compilation). */
slowInstall?: boolean;
/** Skip tarball install attempt (e.g., already using snapshot). */
skipTarball?: boolean;
}