{
  "manifest_version": "1.0.0",
  "template": {
    "id": "a1fcbc92-70b4-4cd7-a1c8-60b17453bf81",
    "slug": "openclaw-desktop-gui",
    "name": "OpenClaw Desktop (GUI)",
    "description": "OpenClaw on an Ubuntu desktop: watch its Chrome, sign in to sites for it",
    "url": "https://railway.com/deploy/openclaw-desktop-gui",
    "upstream": {
      "image": "caddy:2-alpine"
    }
  },
  "status": "unvalidated",
  "validated_at": null,
  "success_rate_30d": null,
  "services": [
    {
      "name": "proxy",
      "source": {
        "image": "caddy:2-alpine"
      },
      "needs_volume": false,
      "http": true
    },
    {
      "name": "desktop",
      "source": {
        "image": "kasmweb/ubuntu-noble-desktop:1.19.0-rolling-weekly"
      },
      "needs_volume": true,
      "volume_mount_path": "/home/kasm-user",
      "http": false
    }
  ],
  "required_inputs": [
    {
      "key": "PORT",
      "service": "proxy",
      "description": "Port this service listens on. Leave as is.",
      "secret": false,
      "strategy": "default",
      "default": "8080"
    },
    {
      "key": "CADDYFILE",
      "service": "proxy",
      "description": "Proxy config written to disk at boot. Leave as is.",
      "secret": false,
      "strategy": "default",
      "default": "{\n\tadmin off\n\tauto_https off\n}\n\n:{$PORT} {\n\trespond /proxy-healthz 200\n\treverse_proxy https://{$KASM_HOST} {\n\t\ttransport http {\n\t\t\ttls_insecure_skip_verify\n\t\t}\n\t}\n}\n"
    },
    {
      "key": "KASM_HOST",
      "service": "proxy",
      "description": "Private address of the desktop service. Leave as is.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "PORT",
      "service": "desktop",
      "description": "Port this service listens on. Leave as is.",
      "secret": false,
      "strategy": "default",
      "default": "6901"
    },
    {
      "key": "VNC_PW",
      "service": "desktop",
      "description": "Desktop login password, minimum 6 characters. Username is kasm_user.",
      "secret": false,
      "strategy": "ask_user"
    },
    {
      "key": "RAILWAY_RUN_UID",
      "service": "desktop",
      "description": "Starts as root to set up the volume, then drops to uid 1000.",
      "secret": false,
      "strategy": "default",
      "default": "0"
    },
    {
      "key": "OPENCLAW_GATEWAY_TOKEN",
      "service": "desktop",
      "description": "Gateway token. The desktop's OpenClaw UI launcher passes it for you. Leave as is.",
      "secret": true,
      "strategy": "generate",
      "generate": "random_base64_32"
    }
  ],
  "deploy": {
    "mcp": {
      "server": "railway",
      "tool": "deploy_template",
      "args": {
        "template_code": "openclaw-desktop-gui"
      }
    },
    "cli": "railway deploy --template openclaw-desktop-gui",
    "api": {
      "method": "POST",
      "path": "/graphql/v2",
      "body": {
        "query": "mutation templateDeploy($input: TemplateDeployV2Input!) { templateDeployV2(input: $input) { projectId workflowId } }",
        "variables": {
          "input": {
            "templateId": "a1fcbc92-70b4-4cd7-a1c8-60b17453bf81",
            "serializedConfig": {
              "services": {
                "67625abc-946a-4749-bba5-878284817c99": {
                  "icon": "https://github.com/caddyserver.png?size=460",
                  "name": "proxy",
                  "deploy": {
                    "startCommand": "sh -c 'printf \"%s\" \"$CADDYFILE\" > /etc/caddy/Caddyfile && exec caddy run --config /etc/caddy/Caddyfile --adapter caddyfile'",
                    "healthcheckPath": "/proxy-healthz",
                    "restartPolicyType": "ON_FAILURE",
                    "healthcheckTimeout": 900
                  },
                  "source": {
                    "image": "caddy:2-alpine"
                  },
                  "variables": {
                    "PORT": {
                      "isOptional": false,
                      "description": "Port this service listens on. Leave as is.",
                      "defaultValue": "8080"
                    },
                    "CADDYFILE": {
                      "isOptional": false,
                      "description": "Proxy config written to disk at boot. Leave as is.",
                      "defaultValue": "{\n\tadmin off\n\tauto_https off\n}\n\n:{$PORT} {\n\trespond /proxy-healthz 200\n\treverse_proxy https://{$KASM_HOST} {\n\t\ttransport http {\n\t\t\ttls_insecure_skip_verify\n\t\t}\n\t}\n}\n"
                    },
                    "KASM_HOST": {
                      "isOptional": false,
                      "description": "Private address of the desktop service. Leave as is.",
                      "defaultValue": "${{desktop.RAILWAY_PRIVATE_DOMAIN}}:6901"
                    }
                  },
                  "networking": {
                    "tcpProxies": {},
                    "serviceDomains": {
                      "<hasDomain>": {
                        "port": 8080
                      }
                    }
                  }
                },
                "ad405fce-96b6-4eca-8b3c-2cdc6b3c4758": {
                  "icon": "https://openclaw.ai/favicon.svg",
                  "name": "desktop",
                  "deploy": {
                    "startCommand": "bash -c 'set -e\nfor k in ANTHROPIC_API_KEY OPENAI_API_KEY OPENROUTER_API_KEY GEMINI_API_KEY TELEGRAM_BOT_TOKEN DISCORD_BOT_TOKEN; do eval v=\\$$k; [ -n \"$v\" ] || unset $k; done\nif [ ${#VNC_PW} -lt 6 ]; then echo \"FATAL: VNC_PW must be at least 6 characters (KasmVNC requirement). Set a longer password and redeploy.\" >&2; exit 1; fi\nif [ -z \"${OPENCLAW_GATEWAY_TOKEN:-}\" ]; then echo \"FATAL: OPENCLAW_GATEWAY_TOKEN is empty. Set one and redeploy.\" >&2; exit 1; fi\necho \"kasm-user ALL=(ALL) NOPASSWD:ALL\" > /etc/sudoers.d/kasm-user\nchmod 0440 /etc/sudoers.d/kasm-user\nchown 1000:1000 /home/kasm-user\nif [ ! -f /home/kasm-user/.railway-owned ]; then chown -R 1000:1000 /home/kasm-user; touch /home/kasm-user/.railway-owned; chown 1000:1000 /home/kasm-user/.railway-owned; fi\nsed -i \"s|function wait_for_network_devices() {|function wait_for_network_devices() { return 0;|\" /dockerstartup/vnc_startup.sh\ngrep -q \"wait_for_network_devices() { return 0;\" /dockerstartup/vnc_startup.sh || { echo FATAL: Kasm startup changed, wait_for_network_devices patch no longer applies >&2; exit 1; }\nfor b in node npm npx openclaw; do ln -sf /home/kasm-user/.local/node/bin/$b /usr/local/bin/$b; done\ncat > /dockerstartup/provision.sh <<\"PROVEOF\"\nset -u\nexport HOME=/home/kasm-user\ncd $HOME\nfor i in $(seq 1 60); do [ -f $HOME/.bashrc ] && break; sleep 1; done\nlog() { echo \"[provision] $*\"; }\nNODE_DIR=$HOME/.local/node\nif [ ! -x $NODE_DIR/bin/node ]; then\n  log installing Node.js 24 LTS\n  TARBALL=$(curl -fsSL https://nodejs.org/dist/latest-v24.x/SHASUMS256.txt | grep -o \"node-v[0-9.]*-linux-x64.tar.xz\" | head -1)\n  mkdir -p $NODE_DIR\n  curl -fsSL \"https://nodejs.org/dist/latest-v24.x/$TARBALL\" | tar -xJ -C $NODE_DIR --strip-components=1 || log Node.js install failed\nfi\nexport PATH=$HOME/.local/bin:$HOME/.local/node/bin:$PATH\nexport npm_config_cache=/tmp/npm-cache\nif [ ! -x $NODE_DIR/bin/openclaw ]; then\n  log installing OpenClaw\n  npm install -g openclaw@latest >/tmp/npm-openclaw.log 2>&1 || log OpenClaw install failed, see /tmp/npm-openclaw.log\nfi\ngrep -q \"local/node/bin\" $HOME/.bashrc || echo \"export PATH=\\$HOME/.local/bin:\\$HOME/.local/node/bin:\\$PATH\" >> $HOME/.bashrc\nOC=$HOME/.openclaw\nmkdir -p $OC $HOME/.local/bin $HOME/.local/share/icons $HOME/Desktop\nexport DISPLAY=:1\nif [ ! -f $OC/openclaw.json ]; then\n  AUTH=\"--auth-choice skip\"\n  if [ -n \"${ANTHROPIC_API_KEY:-}\" ]; then AUTH=\"--auth-choice apiKey --anthropic-api-key $ANTHROPIC_API_KEY\"\n  elif [ -n \"${OPENAI_API_KEY:-}\" ]; then openclaw plugins install codex --accept-capabilities > $OC/plugin-codex.log 2>&1 || log Codex plugin install failed; AUTH=\"--auth-choice openai-api-key --openai-api-key $OPENAI_API_KEY\"\n  elif [ -n \"${OPENROUTER_API_KEY:-}\" ]; then AUTH=\"--auth-choice openrouter-api-key --openrouter-api-key $OPENROUTER_API_KEY\"\n  elif [ -n \"${GEMINI_API_KEY:-}\" ]; then AUTH=\"--auth-choice gemini-api-key --gemini-api-key $GEMINI_API_KEY\"\n  fi\n  log running first-time onboarding\n  openclaw onboard --non-interactive --accept-risk --skip-health --mode local --no-install-daemon --workspace $OC/workspace --gateway-bind loopback --gateway-port 18789 --gateway-auth token --gateway-token \"$OPENCLAW_GATEWAY_TOKEN\" --flow quickstart --json $AUTH > $OC/onboard.log 2>&1 || log onboarding failed, see ~/.openclaw/onboard.log\nfi\nopenclaw config set --json browser.enabled true >/dev/null 2>&1\nopenclaw config set --json browser.headless false >/dev/null 2>&1\nopenclaw config set --json browser.noSandbox true >/dev/null 2>&1\nopenclaw config set browser.executablePath /opt/google/chrome/chrome >/dev/null 2>&1\nopenclaw config set --json browser.extraArgs \"[\\\"--disable-dev-shm-usage\\\"]\" >/dev/null 2>&1\nopenclaw config set gateway.auth.token \"$OPENCLAW_GATEWAY_TOKEN\" >/dev/null 2>&1\nif [ -n \"${TELEGRAM_BOT_TOKEN:-}\" ]; then openclaw config set --json channels.telegram \"{\\\"enabled\\\":true,\\\"dmPolicy\\\":\\\"pairing\\\",\\\"botToken\\\":\\\"$TELEGRAM_BOT_TOKEN\\\",\\\"groupPolicy\\\":\\\"open\\\"}\" >/dev/null 2>&1 || log could not configure Telegram; fi\nif [ -n \"${DISCORD_BOT_TOKEN:-}\" ]; then openclaw config set --json channels.discord \"{\\\"enabled\\\":true,\\\"token\\\":\\\"$DISCORD_BOT_TOKEN\\\",\\\"groupPolicy\\\":\\\"open\\\",\\\"dm\\\":{\\\"policy\\\":\\\"pairing\\\"}}\" >/dev/null 2>&1 || log could not configure Discord; fi\ngeticon() { [ -s $HOME/.local/share/icons/$1.svg ] || curl -fsSL \"$2\" -o $HOME/.local/share/icons/$1.svg || rm -f $HOME/.local/share/icons/$1.svg; }\ngeticon openclaw https://openclaw.ai/favicon.svg\nprintf \"#!/bin/bash\\nexec google-chrome http://127.0.0.1:18789/#token=%s\\n\" \"$OPENCLAW_GATEWAY_TOKEN\" > $HOME/.local/bin/openclaw-ui\ncat > $HOME/.local/bin/openclaw-browser <<\"BREOF\"\n#!/bin/bash\nexport PATH=$HOME/.local/bin:$HOME/.local/node/bin:$PATH\necho Starting the agent browser, the window opens on this desktop.\nopenclaw browser --browser-profile openclaw start || { echo; echo Could not start it. Is the gateway running? Check OpenClaw Logs.; sleep 8; }\nBREOF\ncat > $HOME/.local/bin/openclaw-shell <<\"SHEOF\"\n#!/bin/bash\necho \"OpenClaw CLI. Useful commands:\"\necho \"  openclaw onboard                                    set up the model provider; OAuth logins open in Chrome here\"\necho \"  openclaw status                                     gateway, model and channels\"\necho \"  openclaw channels add                               connect Telegram, Discord, WhatsApp, Slack and more\"\necho \"  openclaw browser --browser-profile openclaw start   open the agent browser to sign in to sites for it\"\necho \"  openclaw logs --follow                              gateway log\"\necho\nexec bash -l\nSHEOF\ncat > $HOME/.local/bin/openclaw-logs <<\"LGEOF\"\n#!/bin/bash\nexec tail -n 200 -f $HOME/.openclaw/gateway.out\nLGEOF\nchmod 755 $HOME/.local/bin/openclaw-ui $HOME/.local/bin/openclaw-browser $HOME/.local/bin/openclaw-shell $HOME/.local/bin/openclaw-logs\nlauncher() { f=$HOME/Desktop/$1.desktop; [ -f $f ] && return 0; icon=utilities-terminal; [ -s $HOME/.local/share/icons/$4.svg ] && icon=$HOME/.local/share/icons/$4.svg\n  printf \"[Desktop Entry]\\nType=Application\\nName=%s\\nExec=%s\\nPath=$HOME\\nIcon=%s\\nTerminal=false\\nCategories=Network;\\n\" \"$2\" \"$3\" \"$icon\" > $f; chmod +x $f; }\nlauncher openclaw-ui \"OpenClaw UI\" \"$HOME/.local/bin/openclaw-ui\" openclaw\nlauncher openclaw-browser \"Agent Chrome\" \"xfce4-terminal --title=Agent-Chrome --command=$HOME/.local/bin/openclaw-browser\" openclaw\nlauncher openclaw-terminal \"OpenClaw CLI\" \"xfce4-terminal --title=OpenClaw --command=$HOME/.local/bin/openclaw-shell\" none\nlauncher openclaw-logs \"Gateway Log\" \"xfce4-terminal --title=OpenClaw-Logs --command=$HOME/.local/bin/openclaw-logs\" none\nTIDIED=0\nif [ ! -d \"$HOME/Desktop/More apps\" ]; then TIDIED=1; mkdir -p \"$HOME/Desktop/More apps\"; for f in slack gimp thunderbird signal-desktop firefox telegram org.remmina.Remmina sublime_text onlyoffice-desktopeditors Zoom nextcloud; do [ -f $HOME/Desktop/$f.desktop ] && mv $HOME/Desktop/$f.desktop \"$HOME/Desktop/More apps/\"; done; fi\nfor i in $(seq 1 120); do SESS=$(pgrep -u 1000 -x xfce4-session | head -1); [ -n \"$SESS\" ] && break; sleep 1; done\nif [ -n \"$SESS\" ]; then\n  export $(tr \"\\0\" \"\\n\" < /proc/$SESS/environ | grep -E \"^(DBUS_SESSION_BUS_ADDRESS|SESSION_MANAGER|XDG_RUNTIME_DIR)=\")\n  for f in openclaw-ui openclaw-browser openclaw-terminal openclaw-logs; do gio set -t string $HOME/Desktop/$f.desktop metadata::xfce-exe-checksum \"$(sha256sum $HOME/Desktop/$f.desktop | cut -d\" \" -f1)\" || log could not mark $f trusted; done\n  [ \"$TIDIED\" = 1 ] && { sleep 2; xfdesktop --arrange || log could not arrange desktop icons; }\nfi\nlog setup done, starting the gateway\nwhile true; do\n  openclaw gateway run --bind loopback --port 18789 --auth token --token \"$OPENCLAW_GATEWAY_TOKEN\" --allow-unconfigured >> $OC/gateway.out 2>&1\n  log gateway exited with status $?, restarting in 5s\n  sleep 5\ndone\nPROVEOF\nchmod 755 /dockerstartup/provision.sh\nsetpriv --reuid=1000 --regid=1000 --init-groups bash -c \"bash /dockerstartup/provision.sh > /home/kasm-user/.provision.log 2>&1\" &\nexec setpriv --reuid=1000 --regid=1000 --init-groups /dockerstartup/kasm_default_profile.sh /dockerstartup/vnc_startup.sh /dockerstartup/kasm_startup.sh --tail-log'",
                    "healthcheckPath": null,
                    "restartPolicyType": "ALWAYS"
                  },
                  "source": {
                    "image": "kasmweb/ubuntu-noble-desktop:1.19.0-rolling-weekly"
                  },
                  "variables": {
                    "PORT": {
                      "isOptional": false,
                      "description": "Port this service listens on. Leave as is.",
                      "defaultValue": "6901"
                    },
                    "VNC_PW": {
                      "isOptional": false,
                      "description": "Desktop login password, minimum 6 characters. Username is kasm_user.",
                      "defaultValue": "{{VNC_PW}}"
                    },
                    "GEMINI_API_KEY": {
                      "isOptional": true,
                      "description": "Optional. Makes Google Gemini the model provider at first boot when no other key is set.",
                      "defaultValue": ""
                    },
                    "OPENAI_API_KEY": {
                      "isOptional": true,
                      "description": "Optional. Makes OpenAI the model provider at first boot when no Anthropic key is set.",
                      "defaultValue": ""
                    },
                    "RAILWAY_RUN_UID": {
                      "isOptional": false,
                      "description": "Starts as root to set up the volume, then drops to uid 1000.",
                      "defaultValue": "0"
                    },
                    "ANTHROPIC_API_KEY": {
                      "isOptional": true,
                      "description": "Optional. Makes Anthropic the model provider at first boot; leave blank to pick a provider from the desktop.",
                      "defaultValue": ""
                    },
                    "DISCORD_BOT_TOKEN": {
                      "isOptional": true,
                      "description": "Optional. Discord bot token; enables the Discord channel with DM pairing.",
                      "defaultValue": ""
                    },
                    "OPENROUTER_API_KEY": {
                      "isOptional": true,
                      "description": "Optional. Makes OpenRouter the model provider at first boot when no Anthropic or OpenAI key is set.",
                      "defaultValue": ""
                    },
                    "TELEGRAM_BOT_TOKEN": {
                      "isOptional": true,
                      "description": "Optional. Bot token from @BotFather; enables the Telegram channel with DM pairing.",
                      "defaultValue": ""
                    },
                    "OPENCLAW_GATEWAY_TOKEN": {
                      "isOptional": false,
                      "description": "Gateway token. The desktop's OpenClaw UI launcher passes it for you. Leave as is.",
                      "defaultValue": "{{OPENCLAW_GATEWAY_TOKEN}}"
                    }
                  },
                  "volumeMounts": {
                    "ad405fce-96b6-4eca-8b3c-2cdc6b3c4758": {
                      "mountPath": "/home/kasm-user"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "post_deploy": {
    "healthcheck": {
      "service": "proxy",
      "method": "GET",
      "path": "/proxy-healthz",
      "expect_status": 200
    }
  },
  "resources": {
    "expected_services": 2,
    "needs_volume": true
  },
  "generated_at": "2026-09-15T16:14:47.755Z",
  "generator_version": "0.1.0"
}
