{
  "manifest_version": "1.0.0",
  "template": {
    "id": "80151df2-7dad-4c3b-8d2c-85940f98da66",
    "slug": "ubuntu-ai-coding-agents-terminal-web-ssh",
    "name": "Ubuntu AI Coding Agents Terminal (Web + SSH)",
    "description": "Web terminal + SSH: Claude Code, Codex, Gemini, Copilot, OpenCode, aider",
    "url": "https://railway.com/deploy/ubuntu-ai-coding-agents-terminal-web-ssh",
    "upstream": {
      "image": "ubuntu:24.04"
    }
  },
  "services": [
    {
      "name": "terminal",
      "source": {
        "image": "ubuntu:24.04"
      },
      "needs_volume": true,
      "volume_mount_path": "/home/ubuntu",
      "tcp_ports": [
        22
      ],
      "http": true
    }
  ],
  "required_inputs": [
    {
      "key": "PORT",
      "service": "terminal",
      "description": "Port the terminal listens on. Leave as is.",
      "secret": false,
      "strategy": "default",
      "default": "8080"
    },
    {
      "key": "PASSWORD",
      "service": "terminal",
      "description": "Login password for the browser terminal and for SSH. The username is ubuntu.",
      "secret": true,
      "strategy": "generate",
      "generate": "strong_password"
    }
  ],
  "deploy": {
    "mcp": {
      "server": "railway",
      "tool": "deploy_template",
      "args": {
        "template_code": "ubuntu-ai-coding-agents-terminal-web-ssh"
      }
    },
    "cli": "railway deploy --template ubuntu-ai-coding-agents-terminal-web-ssh",
    "api": {
      "method": "POST",
      "path": "/graphql/v2",
      "body": {
        "query": "mutation templateDeploy($input: TemplateDeployV2Input!) { templateDeployV2(input: $input) { projectId workflowId } }",
        "variables": {
          "input": {
            "templateId": "80151df2-7dad-4c3b-8d2c-85940f98da66",
            "serializedConfig": {
              "services": {
                "0c3fd8c9-a2ff-4eaf-bf85-89b7912541ba": {
                  "icon": "https://devicons.railway.com/i/ubuntu.svg",
                  "name": "terminal",
                  "deploy": {
                    "startCommand": "bash -c 'set -e\nexport DEBIAN_FRONTEND=noninteractive\nH=/home/ubuntu\nfor k in ANTHROPIC_API_KEY CLAUDE_CODE_OAUTH_TOKEN OPENAI_API_KEY GEMINI_API_KEY GH_TOKEN AUTHORIZED_KEYS; do eval v=\\$$k; [ -n \"$v\" ] || unset $k; done\nif [ -z \"${PASSWORD:-}\" ]; then echo \"FATAL: PASSWORD is empty. Set one and redeploy.\" >&2; exit 1; fi\necho \"[boot] installing base packages\"\nfor i in 1 2 3; do apt-get update -qq >/dev/null && break; sleep 5; done\nfor i in 1 2 3; do apt-get install -y -qq --no-install-recommends ca-certificates curl nginx ttyd tmux sudo openssh-server tini >/dev/null && break; sleep 5; done\ncommand -v ttyd >/dev/null || { echo \"FATAL: base packages did not install\" >&2; exit 1; }\necho \"ubuntu ALL=(ALL) NOPASSWD:ALL\" > /etc/sudoers.d/ubuntu\nchmod 0440 /etc/sudoers.d/ubuntu\necho \"ubuntu:$PASSWORD\" | chpasswd\nchown 1000:1000 $H\nif [ ! -f $H/.railway-owned ]; then chown -R 1000:1000 $H; touch $H/.railway-owned; chown 1000:1000 $H/.railway-owned; fi\n[ -f $H/.bashrc ] || setpriv --reuid=1000 --regid=1000 --init-groups cp -rT /etc/skel $H\nmkdir -p $H/.ssh-host-keys /run/sshd\nchmod 700 $H/.ssh-host-keys\nfor t in ed25519 rsa; do [ -f $H/.ssh-host-keys/ssh_host_$t\\_key ] || ssh-keygen -q -t $t -N \"\" -f $H/.ssh-host-keys/ssh_host_$t\\_key; done\ncp -f $H/.ssh-host-keys/ssh_host_* /etc/ssh/\nchmod 600 /etc/ssh/ssh_host_*_key\nif [ -n \"${AUTHORIZED_KEYS:-}\" ]; then mkdir -p $H/.ssh; printf \"%s\\n\" \"$AUTHORIZED_KEYS\" > $H/.ssh/authorized_keys; chmod 700 $H/.ssh; chmod 600 $H/.ssh/authorized_keys; chown -R 1000:1000 $H/.ssh; fi\ncat > /etc/ssh/sshd_config.d/railway.conf <<\"SSHEOF\"\nPermitRootLogin no\nAllowUsers ubuntu\nPasswordAuthentication yes\nKbdInteractiveAuthentication no\nClientAliveInterval 60\nSSHEOF\necho LANG=C.UTF-8 > /etc/default/locale\nchmod -x /etc/update-motd.d/* 2>/dev/null || true\n/usr/sbin/sshd\necho NO_BROWSER=true >> /etc/environment\nfor k in ANTHROPIC_API_KEY CLAUDE_CODE_OAUTH_TOKEN OPENAI_API_KEY GEMINI_API_KEY GH_TOKEN; do eval v=\\$$k; if [ -n \"$v\" ]; then printf \"%s=\\\"%s\\\"\\n\" $k \"$v\" >> /etc/environment; fi; done\nchown root:ubuntu /etc/environment; chmod 640 /etc/environment\ncat > /etc/profile.d/zz-agents.sh <<\"BANEOF\"\n[ -n \"$PS1\" ] || return 0\ncase \"${TMUX_PANE:-%0}\" in %0) ;; *) return 0 ;; esac\nhave=\"\"; miss=\"\"\nfor a in claude codex gemini copilot opencode aider; do if [ -x /usr/local/bin/$a ]; then have=\"$have $a\"; else miss=\"$miss $a\"; fi; done\necho\necho \"  Ubuntu 24.04 | Claude Code, Codex CLI, Gemini CLI, Copilot CLI, OpenCode, aider | gh, node, python3\"\n[ -n \"$have\" ] && echo \"  ready:$have\"\nif pgrep -f /opt/provision >/dev/null 2>&1; then echo \"  installing:$miss  (tail -f ~/.provision.log)\"; elif [ -n \"$miss\" ]; then echo \"  not installed:$miss  (see ~/.provision.log)\"; fi\necho \"  sign in: claude (URL + code) | codex login --device-auth | gemini (URL + code) | copilot, then /login | opencode auth login\"\n[ -n \"$RAILWAY_TCP_PROXY_DOMAIN\" ] && echo \"  ssh: ssh ubuntu@$RAILWAY_TCP_PROXY_DOMAIN -p $RAILWAY_TCP_PROXY_PORT\"\n[ -n \"$TMUX\" ] && echo \"  This is a tmux session, so an agent keeps running after you close the tab. Ctrl-b d detaches, Ctrl-b c opens a new window.\"\necho\nunset have miss a\nBANEOF\ncat > /etc/tmux.conf <<\"TMUXEOF\"\nset -g mouse on\nset -g history-limit 50000\nset -g default-terminal \"tmux-256color\"\nset -ga terminal-overrides \",xterm-256color:Tc\"\nset -sg escape-time 10\nset -g focus-events on\nset -g status-style \"bg=#2b303b,fg=#c0c5ce\"\nset -g status-left \" #S \"\nset -g status-left-style \"bg=#e95420,fg=#ffffff,bold\"\nset -g status-right \" Ctrl-b d detach | Ctrl-b c new window | Shift+drag to select \"\nset -g status-right-length 80\nset -g window-status-current-style \"bg=#4f5b66,fg=#ffffff\"\nTMUXEOF\nprintf \"ubuntu:%s\\n\" \"$(printf \"%s\" \"$PASSWORD\" | openssl passwd -6 -stdin)\" > /etc/nginx/htpasswd\nchown root:www-data /etc/nginx/htpasswd; chmod 640 /etc/nginx/htpasswd\ncat > /etc/nginx/sites-enabled/default <<NGXEOF\nserver {\n  listen $PORT default_server;\n  listen [::]:$PORT default_server;\n  location = /healthz { return 200 \"ok\"; }\n  location / {\n    auth_basic \"Ubuntu AI Coding Agents Terminal\";\n    auth_basic_user_file /etc/nginx/htpasswd;\n    proxy_pass http://127.0.0.1:7681;\n    proxy_http_version 1.1;\n    proxy_set_header Upgrade \\$http_upgrade;\n    proxy_set_header Connection \"upgrade\";\n    proxy_set_header Host \\$host;\n    proxy_read_timeout 1d;\n    proxy_send_timeout 1d;\n  }\n}\nNGXEOF\nnginx\ncat > /opt/provision.sh <<\"PROVEOF\"\nset -u\nexport HOME=/home/ubuntu\ncd $HOME\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.gz\" | head -1)\n  mkdir -p $NODE_DIR\n  curl -fsSL \"https://nodejs.org/dist/latest-v24.x/$TARBALL\" | tar -xz -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 UV_CACHE_DIR=/tmp/uv-cache\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\nnpmi() { [ -x $HOME/.local/node/bin/$1 ] && return 0; log installing $2; npm install -g $2 >/dev/null 2>&1 || log $2 install failed; }\nnpmi codex @openai/codex\nnpmi gemini @google/gemini-cli\nnpmi copilot @github/copilot\nnpmi opencode opencode-ai\nif [ ! -x $HOME/.local/bin/aider ]; then\n  log installing aider\n  curl -LsSf https://aider.chat/install.sh | sh >/dev/null 2>&1 || log aider install failed\nfi\nif [ ! -x $HOME/.local/bin/gh ]; then\n  log installing GitHub CLI\n  GH_TAG=$(curl -fsSLI -o /dev/null -w \"%{url_effective}\" https://github.com/cli/cli/releases/latest | grep -o \"v[0-9.]*$\")\n  GH_VER=${GH_TAG#v}\n  mkdir -p $HOME/.local/bin\n  curl -fsSL \"https://github.com/cli/cli/releases/download/$GH_TAG/gh_${GH_VER}_linux_amd64.tar.gz\" | tar -xz -C /tmp && install -m 755 /tmp/gh_${GH_VER}_linux_amd64/bin/gh $HOME/.local/bin/gh || log GitHub CLI install failed\n  rm -rf /tmp/gh_${GH_VER}_linux_amd64\nfi\nrm -rf $HOME/.cache/node-gyp\ngrep -q \"local/node/bin\" $HOME/.bashrc || echo \"export PATH=\\$HOME/.local/bin:\\$HOME/.local/node/bin:\\$PATH\" >> $HOME/.bashrc\nlog done\nPROVEOF\ncat > /opt/provision-root.sh <<\"ROOTEOF\"\nexport DEBIAN_FRONTEND=noninteractive\necho \"[provision] installing dev packages\"\napt-get install -y -qq --no-install-recommends git python3 python3-venv python3-pip build-essential pkg-config rsync nano vim less jq unzip zip tree ripgrep htop iproute2 iputils-ping dnsutils wget xz-utils file tzdata gnupg >/dev/null || echo \"[provision] apt install failed\"\napt-get clean\nexec setpriv --reuid=1000 --regid=1000 --init-groups bash /opt/provision.sh\nROOTEOF\nfor b in claude aider gh; do ln -sf $H/.local/bin/$b /usr/local/bin/$b; done\nfor b in node npm npx corepack codex gemini copilot opencode; do ln -sf $H/.local/node/bin/$b /usr/local/bin/$b; done\n: > $H/.provision.log; chown 1000:1000 $H/.provision.log\nbash /opt/provision-root.sh >> $H/.provision.log 2>&1 &\nexport HOME=$H USER=ubuntu LOGNAME=ubuntu SHELL=/bin/bash LANG=C.UTF-8 COLORTERM=truecolor NO_BROWSER=true\nunset PASSWORD AUTHORIZED_KEYS DEBIAN_FRONTEND\ncd $H\necho \"[boot] terminal ready on port $PORT, sshd on 22\"\nexec tini -- setpriv --reuid=1000 --regid=1000 --init-groups ttyd -i 127.0.0.1 -p 7681 -W -t titleFixed=\"Ubuntu AI Coding Agents Terminal\" -t fontSize=15 -t disableLeaveAlert=true tmux new-session -A -s main'",
                    "healthcheckPath": "/healthz",
                    "restartPolicyType": "ON_FAILURE",
                    "healthcheckTimeout": 600
                  },
                  "source": {
                    "image": "ubuntu:24.04"
                  },
                  "variables": {
                    "PORT": {
                      "isOptional": false,
                      "description": "Port the terminal listens on. Leave as is.",
                      "defaultValue": "8080"
                    },
                    "GH_TOKEN": {
                      "isOptional": true,
                      "description": "Optional. GitHub token for Copilot CLI and gh; leave blank to sign in with the device flow.",
                      "defaultValue": ""
                    },
                    "PASSWORD": {
                      "isOptional": false,
                      "description": "Login password for the browser terminal and for SSH. The username is ubuntu.",
                      "defaultValue": "{{PASSWORD}}"
                    },
                    "GEMINI_API_KEY": {
                      "isOptional": true,
                      "description": "Optional. Used by Gemini CLI, OpenCode and aider; leave blank to sign in with Google.",
                      "defaultValue": ""
                    },
                    "OPENAI_API_KEY": {
                      "isOptional": true,
                      "description": "Optional. Used by Codex, OpenCode and aider; leave blank to sign in with ChatGPT.",
                      "defaultValue": ""
                    },
                    "AUTHORIZED_KEYS": {
                      "isOptional": true,
                      "description": "Optional. SSH public keys, one per line, for key-based login as ubuntu. Password login stays on.",
                      "defaultValue": ""
                    },
                    "ANTHROPIC_API_KEY": {
                      "isOptional": true,
                      "description": "Optional. Used by Claude Code, OpenCode and aider; leave blank to sign in with your Claude account.",
                      "defaultValue": ""
                    },
                    "CLAUDE_CODE_OAUTH_TOKEN": {
                      "isOptional": true,
                      "description": "Optional. Output of `claude setup-token` on your own machine; signs Claude Code in with a Pro/Max subscription.",
                      "defaultValue": ""
                    }
                  },
                  "networking": {
                    "tcpProxies": {
                      "22": {}
                    },
                    "serviceDomains": {
                      "<hasDomain>": {
                        "port": 8080
                      }
                    }
                  },
                  "volumeMounts": {
                    "0c3fd8c9-a2ff-4eaf-bf85-89b7912541ba": {
                      "mountPath": "/home/ubuntu"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "post_deploy": {
    "healthcheck": {
      "service": "terminal",
      "method": "GET",
      "path": "/healthz",
      "expect_status": 200
    }
  },
  "resources": {
    "expected_services": 1,
    "needs_volume": true
  },
  "generated_at": "2026-09-15T10:14:39.124Z",
  "generator_version": "0.1.0",
  "status": "validated",
  "validated_at": "2026-09-15T09:19:34.565Z",
  "success_rate_30d": 1,
  "validation": {
    "last_run_id": "run_360c55acee8943869fd5",
    "checks": [
      {
        "name": "workflow_completed",
        "passed": true
      },
      {
        "name": "all_services_deployed",
        "passed": true
      },
      {
        "name": "healthcheck",
        "passed": true
      },
      {
        "name": "stays_up",
        "passed": true
      }
    ],
    "typical_ready_seconds": 27,
    "typical_build_seconds": 0,
    "typical_start_seconds": 10,
    "slowest_service": "terminal"
  }
}
