{
  "manifest_version": "1.0.0",
  "template": {
    "id": "9c32f5d9-0d10-4900-a6b5-60fdaa62fe35",
    "slug": "claude-code-desktop",
    "name": "Ubuntu Claude Code Desktop (GUI)",
    "description": "Ubuntu desktop in your browser with VS Code, Chrome and Claude Code ready.",
    "url": "https://railway.com/deploy/claude-code-desktop",
    "upstream": {
      "image": "caddy:2-alpine"
    }
  },
  "status": "unvalidated",
  "validated_at": null,
  "success_rate_30d": null,
  "services": [
    {
      "name": "desktop",
      "source": {
        "image": "kasmweb/ubuntu-noble-desktop:1.19.0-rolling-weekly"
      },
      "needs_volume": true,
      "volume_mount_path": "/home/kasm-user",
      "http": false
    },
    {
      "name": "proxy",
      "source": {
        "image": "caddy:2-alpine"
      },
      "needs_volume": false,
      "http": true
    }
  ],
  "required_inputs": [
    {
      "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": "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"
    }
  ],
  "deploy": {
    "mcp": {
      "server": "railway",
      "tool": "deploy_template",
      "args": {
        "template_code": "claude-code-desktop"
      }
    },
    "cli": "railway deploy --template claude-code-desktop",
    "api": {
      "method": "POST",
      "path": "/graphql/v2",
      "body": {
        "query": "mutation templateDeploy($input: TemplateDeployV2Input!) { templateDeployV2(input: $input) { projectId workflowId } }",
        "variables": {
          "input": {
            "templateId": "9c32f5d9-0d10-4900-a6b5-60fdaa62fe35",
            "serializedConfig": {
              "services": {
                "67dc86bf-7c69-472b-af14-f50b6519d259": {
                  "icon": "https://devicons.railway.com/i/ubuntu.svg",
                  "name": "desktop",
                  "deploy": {
                    "startCommand": "bash -c 'set -e\n[ -n \"${ANTHROPIC_API_KEY:-}\" ] || unset ANTHROPIC_API_KEY\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\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; }\nln -sf /home/kasm-user/.local/bin/claude /usr/local/bin/claude\nfor b in node npm npx corepack; 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\nif [ ! -x $HOME/.local/bin/claude ]; then\n  log installing Claude Code\n  curl -fsSL https://claude.ai/install.sh | bash || log Claude Code install failed\nfi\ngrep -q \"local/node/bin\" $HOME/.bashrc || echo \"export PATH=\\$HOME/.local/bin:\\$HOME/.local/node/bin:\\$PATH\" >> $HOME/.bashrc\nif ! ls $HOME/.vscode/extensions 2>/dev/null | grep -q anthropic.claude-code; then\n  log installing the Claude Code VS Code extension\n  code --no-sandbox --user-data-dir $HOME/.config/Code --install-extension anthropic.claude-code || log VS Code extension install failed\nfi\nif [ ! -f $HOME/.config/Code/User/settings.json ]; then\n  mkdir -p $HOME/.config/Code/User\n  cat > $HOME/.config/Code/User/settings.json <<\"JSONEOF\"\n{\n  \"chat.disableAIFeatures\": true,\n  \"workbench.startupEditor\": \"none\",\n  \"telemetry.telemetryLevel\": \"off\",\n  \"update.mode\": \"none\"\n}\nJSONEOF\nfi\nif [ ! -f $HOME/Desktop/claude-code.desktop ]; then\n  mkdir -p $HOME/.local/share/icons $HOME/Desktop\n  curl -fsSL https://cdn.simpleicons.org/claude/D97757 -o $HOME/.local/share/icons/claude.svg || true\n  ICON=utilities-terminal\n  [ -s $HOME/.local/share/icons/claude.svg ] && ICON=$HOME/.local/share/icons/claude.svg\n  cat > $HOME/Desktop/claude-code.desktop <<DESKEOF\n[Desktop Entry]\nType=Application\nName=Claude Code\nComment=Open a terminal running Claude Code\nExec=xfce4-terminal --title=\"Claude Code\" --command=claude\nIcon=$ICON\nTerminal=false\nCategories=Development;\nDESKEOF\n  chmod +x $HOME/Desktop/claude-code.desktop\nfi\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 ^DBUS_SESSION_BUS_ADDRESS=)\n  gio set -t string $HOME/Desktop/claude-code.desktop metadata::xfce-exe-checksum \"$(sha256sum $HOME/Desktop/claude-code.desktop | cut -d\" \" -f1)\" || log could not mark the launcher trusted\nfi\nlog done\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}}"
                    },
                    "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. Skips the browser login; leave blank to sign in with your Claude account.",
                      "defaultValue": ""
                    }
                  },
                  "volumeMounts": {
                    "67dc86bf-7c69-472b-af14-f50b6519d259": {
                      "mountPath": "/home/kasm-user"
                    }
                  }
                },
                "f440791e-2963-45e2-8620-bf8c0f509435": {
                  "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
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "post_deploy": {
    "healthcheck": {
      "service": "proxy",
      "method": "GET",
      "path": "/proxy-healthz",
      "expect_status": 200
    }
  },
  "resources": {
    "expected_services": 2,
    "needs_volume": true
  },
  "generated_at": "2026-10-06T04:14:42.626Z",
  "generator_version": "0.1.0"
}
