{
  "manifest_version": "1.0.0",
  "template": {
    "id": "0ccbd8d6-c9e1-4ead-b7cb-fe6a3d7e649d",
    "slug": "openclaw-self-hosted",
    "name": "OpenClaw (Self-Hosted)",
    "description": "Self-host OpenClaw with Chromium, persistent memory and API key setup.",
    "url": "https://railway.com/deploy/openclaw-self-hosted",
    "upstream": {
      "image": "ghcr.io/openclaw/openclaw:latest-browser"
    }
  },
  "services": [
    {
      "name": "OpenClaw",
      "source": {
        "image": "ghcr.io/openclaw/openclaw:latest-browser"
      },
      "needs_volume": true,
      "volume_mount_path": "/data",
      "http": true
    }
  ],
  "required_inputs": [
    {
      "key": "PORT",
      "service": "OpenClaw",
      "description": "Public HTTP and WebSocket listener port used by Railway. The generated domain targets this port.",
      "secret": false,
      "strategy": "default",
      "default": "8080"
    },
    {
      "key": "RAILWAY_RUN_UID",
      "service": "OpenClaw",
      "description": "Start the bootstrap as root so it can prepare volume permissions. The startup command then runs OpenClaw as the image's non-root node user.",
      "secret": false,
      "strategy": "default",
      "default": "0"
    },
    {
      "key": "OPENCLAW_STATE_DIR",
      "service": "OpenClaw",
      "description": "Persistent directory for configuration, credentials, sessions, agent state, and memory. Keep it inside the /data volume.",
      "secret": false,
      "strategy": "default",
      "default": "/data/.openclaw"
    },
    {
      "key": "OPENCLAW_AUTH_CHOICE",
      "service": "OpenClaw",
      "description": "First-deployment provider setup: auto detects a single supplied API key. If supplying multiple keys, select openai-api-key, apiKey (Anthropic), gemini-api-key or openrouter-api-key. Use skip for setup in the Models screen. Existing configuration is preserved.",
      "secret": false,
      "strategy": "default",
      "default": "auto"
    },
    {
      "key": "OPENCLAW_GATEWAY_PORT",
      "service": "OpenClaw",
      "description": "Private Gateway HTTP and WebSocket port on loopback. Keep this different from the public PORT; the local ingress proxy forwards to it.",
      "secret": false,
      "strategy": "default",
      "default": "18789"
    },
    {
      "key": "OPENCLAW_GATEWAY_TOKEN",
      "service": "OpenClaw",
      "description": "Unique generated administrator token for the Gateway and Control UI. Copy it from your deployed service variables when connecting; keep it private.",
      "secret": true,
      "strategy": "generate",
      "generate": "random_base64_32"
    },
    {
      "key": "OPENCLAW_WORKSPACE_DIR",
      "service": "OpenClaw",
      "description": "Persistent workspace for agent files and artifacts. Keep it inside the /data volume.",
      "secret": false,
      "strategy": "default",
      "default": "/data/workspace"
    },
    {
      "key": "OPENCLAW_TRUSTED_PROXIES",
      "service": "OpenClaw",
      "description": "Trust only the local ingress proxy. It removes incoming forwarded headers and rebuilds X-Forwarded-For from the socket peer, so Railway ingress IP changes require no updates. Keep this loopback-only; token authentication and device pairing remain enabled.",
      "secret": false,
      "strategy": "default",
      "default": "[\"127.0.0.1\"]"
    },
    {
      "key": "OPENCLAW_BROWSER_HEADLESS",
      "service": "OpenClaw",
      "description": "Run the bundled Chromium browser without a graphical display.",
      "secret": false,
      "strategy": "default",
      "default": "1"
    },
    {
      "key": "OPENCLAW_BROWSER_NO_SANDBOX",
      "service": "OpenClaw",
      "description": "Disable Chromium’s internal sandbox, required by the tested Railway runtime. Chromium still runs as the non-root node user inside the service container. Set false only on infrastructure that supports Chromium sandboxing.",
      "secret": false,
      "strategy": "default",
      "default": "true"
    },
    {
      "key": "OPENCLAW_CONTROL_UI_ALLOWED_ORIGINS",
      "service": "OpenClaw",
      "description": "HTTPS origins allowed to open the Control UI. The default follows Railway's public domain on startup. Redeploy after adding or changing a custom domain. For multiple domains, include each full HTTPS origin in this JSON array before redeploying.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_domain"
    }
  ],
  "deploy": {
    "mcp": {
      "server": "railway",
      "tool": "deploy_template",
      "args": {
        "template_code": "openclaw-self-hosted"
      }
    },
    "cli": "railway deploy --template openclaw-self-hosted",
    "api": {
      "method": "POST",
      "path": "/graphql/v2",
      "body": {
        "query": "mutation templateDeploy($input: TemplateDeployV2Input!) { templateDeployV2(input: $input) { projectId workflowId } }",
        "variables": {
          "input": {
            "templateId": "0ccbd8d6-c9e1-4ead-b7cb-fe6a3d7e649d",
            "serializedConfig": {
              "services": {
                "96b9584c-9eb5-41f2-bb3d-fb42b21380bd": {
                  "icon": "https://openclaw.ai/favicon.svg",
                  "name": "OpenClaw",
                  "deploy": {
                    "startCommand": "/bin/sh -ec 'mkdir -p \"$OPENCLAW_STATE_DIR\" \"$OPENCLAW_WORKSPACE_DIR\"; chown 1000:1000 /data \"$OPENCLAW_STATE_DIR\" \"$OPENCLAW_WORKSPACE_DIR\"; exec runuser -u node -- /bin/sh -ec '\"'\"'cd /app; if [ ! -s \"$OPENCLAW_STATE_DIR/openclaw.json\" ]; then choice=$(node -e '\"'\"'\"'\"'\"'\"'\"'\"'const e=process.env; const choices=[[\"openai-api-key\",\"OPENAI_API_KEY\"],[\"apiKey\",\"ANTHROPIC_API_KEY\"],[\"gemini-api-key\",\"GEMINI_API_KEY\"],[\"openrouter-api-key\",\"OPENROUTER_API_KEY\"]]; let c=e.OPENCLAW_AUTH_CHOICE||\"auto\"; if(c===\"auto\"){const found=choices.filter(([,k])=>e[k]?.trim());if(found.length>1)throw new Error(\"Multiple provider keys supplied. Set OPENCLAW_AUTH_CHOICE explicitly.\");c=found[0]?.[0]||\"skip\";} if(c!==\"skip\"&&!choices.some(([id,k])=>id===c&&e[k]?.trim()))throw new Error(\"Set a supported OPENCLAW_AUTH_CHOICE and its provider API key, or choose skip.\");process.stdout.write(c);'\"'\"'\"'\"'\"'\"'\"'\"'); if [ \"$choice\" != skip ]; then openclaw onboard --non-interactive --accept-risk --auth-choice \"$choice\" --secret-input-mode ref --gateway-token-ref-env OPENCLAW_GATEWAY_TOKEN --gateway-auth token --gateway-bind loopback --gateway-port \"$OPENCLAW_GATEWAY_PORT\" --workspace \"$OPENCLAW_WORKSPACE_DIR\" --skip-daemon --skip-channels --skip-health --skip-skills --skip-hooks --skip-search --skip-ui --suppress-gateway-token-output; fi; fi; settings=$(node -e '\"'\"'\"'\"'\"'\"'\"'\"'const e=process.env; process.stdout.write(JSON.stringify([{path:\"gateway.mode\",value:\"local\"},{path:\"gateway.controlUi.allowedOrigins\",value:JSON.parse(e.OPENCLAW_CONTROL_UI_ALLOWED_ORIGINS)},{path:\"gateway.trustedProxies\",value:JSON.parse(e.OPENCLAW_TRUSTED_PROXIES)},{path:\"gateway.allowRealIpFallback\",value:false},{path:\"gateway.port\",value:Number(e.OPENCLAW_GATEWAY_PORT)},{path:\"gateway.bind\",value:\"loopback\"},{path:\"gateway.auth.allowTailscale\",value:false},{path:\"browser.executablePath\",value:require(\"playwright-core\").chromium.executablePath()},{path:\"browser.noSandbox\",value:JSON.parse(e.OPENCLAW_BROWSER_NO_SANDBOX)}]));'\"'\"'\"'\"'\"'\"'\"'\"'); openclaw config set --batch-json \"$settings\"; if [ -n \"${OPENCLAW_MODEL:-}\" ]; then openclaw models set \"$OPENCLAW_MODEL\"; fi; exec node -e '\"'\"'\"'\"'\"'\"'\"'\"'const http = require(\"node:http\");const net = require(\"node:net\");const {spawn} = require(\"node:child_process\");const port = Number(process.env.PORT);const gatewayPort = Number(process.env.OPENCLAW_GATEWAY_PORT);if (!Number.isInteger(port) || !Number.isInteger(gatewayPort) || port === gatewayPort) throw new Error(\"PORT and OPENCLAW_GATEWAY_PORT must be different valid ports\");function headers(req, upgrade) {  const peer = (req.socket.remoteAddress || \"\").replace(/^::ffff:/, \"\");  if (!net.isIP(peer) || peer === \"::1\" || peer.startsWith(\"127.\")) throw new Error(\"A non-loopback ingress peer is required\");  const h = {...req.headers};  const connection = String(h.connection || \"\").split(\",\").map(v => v.trim().toLowerCase());  for (const k of Object.keys(h)) {    if (k === \"forwarded\" || k.startsWith(\"x-forwarded-\") || k === \"x-real-ip\" || k.startsWith(\"tailscale-\") || k === \"x-forwarded-user\" || k === \"x-openclaw-scopes\" || [\"proxy-authorization\", \"proxy-authenticate\", \"connection\", \"keep-alive\", \"upgrade\", \"te\", \"trailer\"].includes(k) || connection.includes(k)) delete h[k];  }  h[\"x-forwarded-for\"] = peer;  h[\"x-forwarded-proto\"] = \"https\";  h[\"x-forwarded-host\"] = req.headers.host;  if (upgrade) {h.connection = \"Upgrade\"; h.upgrade = \"websocket\";}  return h;}const server = http.createServer((req, res) => {  let h;  try {h = headers(req, false);} catch {res.writeHead(403); res.end(\"Invalid ingress\"); return;}  const upstream = http.request({host:\"127.0.0.1\", port:gatewayPort, method:req.method, path:req.url, headers:h}, reply => {    res.writeHead(reply.statusCode, reply.headers); reply.pipe(res);    reply.on(\"error\", () => res.destroy());  });  upstream.on(\"error\", () => {if (!res.headersSent) res.writeHead(503); res.end(\"Gateway starting or unavailable\");});  req.on(\"aborted\", () => upstream.destroy());  req.pipe(upstream);});server.on(\"upgrade\", (req, socket, head) => {  if (String(req.headers.upgrade).toLowerCase() !== \"websocket\") {socket.destroy(); return;}  let h;  try {h = headers(req, true);} catch {socket.end(\"HTTP/1.1 403 Forbidden\\r\\nConnection: close\\r\\n\\r\\n\"); return;}  const upstream = net.connect(gatewayPort, \"127.0.0.1\", () => {    upstream.write(`${req.method} ${req.url} HTTP/1.1\\r\\n${Object.entries(h).map(([k,v]) => `${k}: ${Array.isArray(v) ? v.join(\", \") : v}`).join(\"\\r\\n\")}\\r\\n\\r\\n`);    if (head.length) upstream.write(head);    socket.pipe(upstream); upstream.pipe(socket);  });  upstream.on(\"error\", () => socket.destroy()); socket.on(\"error\", () => upstream.destroy());  socket.on(\"close\", () => upstream.destroy()); upstream.on(\"close\", () => socket.destroy());});const gateway = spawn(\"openclaw\", [\"gateway\",\"run\",\"--bind\",\"loopback\",\"--port\",String(gatewayPort),\"--auth\",\"token\"], {stdio:\"inherit\"});gateway.on(\"error\", error => {console.error(error.message); process.exit(1);});gateway.on(\"exit\", (code, signal) => {server.close(); process.exit(code ?? (signal ? 1 : 0));});server.on(\"error\", error => {console.error(error.message); gateway.kill(\"SIGTERM\");});server.listen(port, \"0.0.0.0\", () => console.log(\"Railway ingress ready; Gateway is private on loopback.\"));for (const signal of [\"SIGTERM\",\"SIGINT\"]) process.on(signal, () => {server.close(); gateway.kill(signal);});'\"'\"'\"'\"'\"'\"'\"'\"''\"'\"''",
                    "healthcheckPath": "/startupz"
                  },
                  "source": {
                    "image": "ghcr.io/openclaw/openclaw:latest-browser"
                  },
                  "variables": {
                    "PORT": {
                      "isOptional": false,
                      "description": "Public HTTP and WebSocket listener port used by Railway. The generated domain targets this port.",
                      "defaultValue": "8080"
                    },
                    "GEMINI_API_KEY": {
                      "isOptional": true,
                      "description": "Optional Google Gemini API key. On a fresh deployment, supply one provider key to configure its recommended model automatically. Leave blank to use the Models screen. Provider usage is billed separately.",
                      "defaultValue": ""
                    },
                    "OPENAI_API_KEY": {
                      "isOptional": true,
                      "description": "Optional OpenAI Platform API key for metered API usage. A single supplied key configures a fresh deployment automatically. Leave blank to connect OpenAI with OAuth or another provider from Models. ChatGPT subscriptions and API billing are separate.",
                      "defaultValue": ""
                    },
                    "OPENCLAW_MODEL": {
                      "isOptional": true,
                      "description": "Optional provider/model identifier to apply at startup, such as openai/gpt-6-astra. Leave blank to keep the onboarding recommendation or your model chosen in the UI. Account model access can vary.",
                      "defaultValue": ""
                    },
                    "RAILWAY_RUN_UID": {
                      "isOptional": false,
                      "description": "Start the bootstrap as root so it can prepare volume permissions. The startup command then runs OpenClaw as the image's non-root node user.",
                      "defaultValue": "0"
                    },
                    "ANTHROPIC_API_KEY": {
                      "isOptional": true,
                      "description": "Optional Anthropic API key. On a fresh deployment, supply one provider key to configure its recommended model automatically. Leave blank to connect a provider from OpenClaw’s Models screen. Provider usage is billed separately.",
                      "defaultValue": ""
                    },
                    "OPENCLAW_STATE_DIR": {
                      "isOptional": false,
                      "description": "Persistent directory for configuration, credentials, sessions, agent state, and memory. Keep it inside the /data volume.",
                      "defaultValue": "/data/.openclaw"
                    },
                    "OPENROUTER_API_KEY": {
                      "isOptional": true,
                      "description": "Optional OpenRouter API key. A single supplied key configures a fresh deployment automatically. Set OPENCLAW_MODEL to your preferred OpenRouter model if desired, or leave blank for the provider recommendation. Provider usage is billed separately.",
                      "defaultValue": ""
                    },
                    "OPENCLAW_AUTH_CHOICE": {
                      "isOptional": false,
                      "description": "First-deployment provider setup: auto detects a single supplied API key. If supplying multiple keys, select openai-api-key, apiKey (Anthropic), gemini-api-key or openrouter-api-key. Use skip for setup in the Models screen. Existing configuration is preserved.",
                      "defaultValue": "auto"
                    },
                    "OPENCLAW_GATEWAY_PORT": {
                      "isOptional": false,
                      "description": "Private Gateway HTTP and WebSocket port on loopback. Keep this different from the public PORT; the local ingress proxy forwards to it.",
                      "defaultValue": "18789"
                    },
                    "OPENCLAW_GATEWAY_TOKEN": {
                      "isOptional": false,
                      "description": "Unique generated administrator token for the Gateway and Control UI. Copy it from your deployed service variables when connecting; keep it private.",
                      "defaultValue": "{{OPENCLAW_GATEWAY_TOKEN}}"
                    },
                    "OPENCLAW_WORKSPACE_DIR": {
                      "isOptional": false,
                      "description": "Persistent workspace for agent files and artifacts. Keep it inside the /data volume.",
                      "defaultValue": "/data/workspace"
                    },
                    "OPENCLAW_TRUSTED_PROXIES": {
                      "isOptional": false,
                      "description": "Trust only the local ingress proxy. It removes incoming forwarded headers and rebuilds X-Forwarded-For from the socket peer, so Railway ingress IP changes require no updates. Keep this loopback-only; token authentication and device pairing remain enabled.",
                      "defaultValue": "[\"127.0.0.1\"]"
                    },
                    "OPENCLAW_BROWSER_HEADLESS": {
                      "isOptional": false,
                      "description": "Run the bundled Chromium browser without a graphical display.",
                      "defaultValue": "1"
                    },
                    "OPENCLAW_BROWSER_NO_SANDBOX": {
                      "isOptional": false,
                      "description": "Disable Chromium’s internal sandbox, required by the tested Railway runtime. Chromium still runs as the non-root node user inside the service container. Set false only on infrastructure that supports Chromium sandboxing.",
                      "defaultValue": "true"
                    },
                    "OPENCLAW_CONTROL_UI_ALLOWED_ORIGINS": {
                      "isOptional": false,
                      "description": "HTTPS origins allowed to open the Control UI. The default follows Railway's public domain on startup. Redeploy after adding or changing a custom domain. For multiple domains, include each full HTTPS origin in this JSON array before redeploying.",
                      "defaultValue": "[\"https://${{RAILWAY_PUBLIC_DOMAIN}}\"]"
                    }
                  },
                  "networking": {
                    "serviceDomains": {
                      "<hasDomain>:8080": {
                        "port": 8080
                      }
                    }
                  },
                  "volumeMounts": {
                    "96b9584c-9eb5-41f2-bb3d-fb42b21380bd": {
                      "mountPath": "/data"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "post_deploy": {
    "healthcheck": {
      "service": "OpenClaw",
      "method": "GET",
      "path": "/startupz",
      "expect_status": 200
    }
  },
  "resources": {
    "expected_services": 1,
    "needs_volume": true
  },
  "generated_at": "2026-09-29T22:14:51.071Z",
  "generator_version": "0.1.0",
  "status": "validated",
  "validated_at": "2026-09-29T21:33:41.553Z",
  "success_rate_30d": 1,
  "validation": {
    "last_run_id": "run_c9b1752e1c0a413a839c",
    "checks": [
      {
        "name": "workflow_completed",
        "passed": true
      },
      {
        "name": "all_services_deployed",
        "passed": true
      },
      {
        "name": "healthcheck",
        "passed": true
      },
      {
        "name": "stays_up",
        "passed": true
      }
    ],
    "typical_ready_seconds": 112,
    "typical_build_seconds": 0,
    "typical_start_seconds": 30,
    "slowest_service": "OpenClaw"
  }
}
