---
title: "Deploy Ubuntu AI Coding Agents Terminal (Web + SSH)"
description: "Web terminal + SSH: Claude Code, Codex, Gemini, Copilot, OpenCode, aider"
category: "AI/ML"
url: https://railway.com/deploy/ubuntu-ai-coding-agents-terminal-web-ssh
---

# Deploy Ubuntu AI Coding Agents Terminal (Web + SSH)

Web terminal + SSH: Claude Code, Codex, Gemini, Copilot, OpenCode, aider

**[Deploy Ubuntu AI Coding Agents Terminal (Web + SSH) on Railway](https://railway.com/template/ubuntu-ai-coding-agents-terminal-web-ssh)**

Machine-readable deploy manifest (JSON, validated by TemplateCI): https://railway.com/deploy/ubuntu-ai-coding-agents-terminal-web-ssh/manifest.json

- **Creator:** darseen
- **Category:** AI/ML

## Template content

### terminal https://devicons.railway.com/i/ubuntu.svg

- **Image:** ubuntu:24.04
- **Start command:** `bash -c 'set -e
export DEBIAN_FRONTEND=noninteractive
H=/home/ubuntu
for 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
if [ -z "${PASSWORD:-}" ]; then echo "FATAL: PASSWORD is empty. Set one and redeploy." >&2; exit 1; fi
echo "[boot] installing base packages"
for i in 1 2 3; do apt-get update -qq >/dev/null && break; sleep 5; done
for 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
command -v ttyd >/dev/null || { echo "FATAL: base packages did not install" >&2; exit 1; }
echo "ubuntu ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/ubuntu
chmod 0440 /etc/sudoers.d/ubuntu
echo "ubuntu:$PASSWORD" | chpasswd
chown 1000:1000 $H
if [ ! -f $H/.railway-owned ]; then chown -R 1000:1000 $H; touch $H/.railway-owned; chown 1000:1000 $H/.railway-owned; fi
[ -f $H/.bashrc ] || setpriv --reuid=1000 --regid=1000 --init-groups cp -rT /etc/skel $H
mkdir -p $H/.ssh-host-keys /run/sshd
chmod 700 $H/.ssh-host-keys
for 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
cp -f $H/.ssh-host-keys/ssh_host_* /etc/ssh/
chmod 600 /etc/ssh/ssh_host_*_key
if [ -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
cat > /etc/ssh/sshd_config.d/railway.conf <<"SSHEOF"
PermitRootLogin no
AllowUsers ubuntu
PasswordAuthentication yes
KbdInteractiveAuthentication no
ClientAliveInterval 60
SSHEOF
echo LANG=C.UTF-8 > /etc/default/locale
chmod -x /etc/update-motd.d/* 2>/dev/null || true
/usr/sbin/sshd
echo NO_BROWSER=true >> /etc/environment
for 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
chown root:ubuntu /etc/environment; chmod 640 /etc/environment
cat > /etc/profile.d/zz-agents.sh <<"BANEOF"
[ -n "$PS1" ] || return 0
case "${TMUX_PANE:-%0}" in %0) ;; *) return 0 ;; esac
have=""; miss=""
for 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
echo
echo "  Ubuntu 24.04 | Claude Code, Codex CLI, Gemini CLI, Copilot CLI, OpenCode, aider | gh, node, python3"
[ -n "$have" ] && echo "  ready:$have"
if 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
echo "  sign in: claude (URL + code) | codex login --device-auth | gemini (URL + code) | copilot, then /login | opencode auth login"
[ -n "$RAILWAY_TCP_PROXY_DOMAIN" ] && echo "  ssh: ssh ubuntu@$RAILWAY_TCP_PROXY_DOMAIN -p $RAILWAY_TCP_PROXY_PORT"
[ -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."
echo
unset have miss a
BANEOF
cat > /etc/tmux.conf <<"TMUXEOF"
set -g mouse on
set -g history-limit 50000
set -g default-terminal "tmux-256color"
set -ga terminal-overrides ",xterm-256color:Tc"
set -sg escape-time 10
set -g focus-events on
set -g status-style "bg=#2b303b,fg=#c0c5ce"
set -g status-left " #S "
set -g status-left-style "bg=#e95420,fg=#ffffff,bold"
set -g status-right " Ctrl-b d detach | Ctrl-b c new window | Shift+drag to select "
set -g status-right-length 80
set -g window-status-current-style "bg=#4f5b66,fg=#ffffff"
TMUXEOF
printf "ubuntu:%s\n" "$(printf "%s" "$PASSWORD" | openssl passwd -6 -stdin)" > /etc/nginx/htpasswd
chown root:www-data /etc/nginx/htpasswd; chmod 640 /etc/nginx/htpasswd
cat > /etc/nginx/sites-enabled/default <<NGXEOF
server {
  listen $PORT default_server;
  listen [::]:$PORT default_server;
  location = /healthz { return 200 "ok"; }
  location / {
    auth_basic "Ubuntu AI Coding Agents Terminal";
    auth_basic_user_file /etc/nginx/htpasswd;
    proxy_pass http://127.0.0.1:7681;
    proxy_http_version 1.1;
    proxy_set_header Upgrade \$http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host \$host;
    proxy_read_timeout 1d;
    proxy_send_timeout 1d;
  }
}
NGXEOF
nginx
cat > /opt/provision.sh <<"PROVEOF"
set -u
export HOME=/home/ubuntu
cd $HOME
log() { echo "[provision] $*"; }
NODE_DIR=$HOME/.local/node
if [ ! -x $NODE_DIR/bin/node ]; then
  log installing Node.js 24 LTS
  TARBALL=$(curl -fsSL https://nodejs.org/dist/latest-v24.x/SHASUMS256.txt | grep -o "node-v[0-9.]*-linux-x64.tar.gz" | head -1)
  mkdir -p $NODE_DIR
  curl -fsSL "https://nodejs.org/dist/latest-v24.x/$TARBALL" | tar -xz -C $NODE_DIR --strip-components=1 || log Node.js install failed
fi
export PATH=$HOME/.local/bin:$HOME/.local/node/bin:$PATH
export npm_config_cache=/tmp/npm-cache UV_CACHE_DIR=/tmp/uv-cache
if [ ! -x $HOME/.local/bin/claude ]; then
  log installing Claude Code
  curl -fsSL https://claude.ai/install.sh | bash || log Claude Code install failed
fi
npmi() { [ -x $HOME/.local/node/bin/$1 ] && return 0; log installing $2; npm install -g $2 >/dev/null 2>&1 || log $2 install failed; }
npmi codex @openai/codex
npmi gemini @google/gemini-cli
npmi copilot @github/copilot
npmi opencode opencode-ai
if [ ! -x $HOME/.local/bin/aider ]; then
  log installing aider
  curl -LsSf https://aider.chat/install.sh | sh >/dev/null 2>&1 || log aider install failed
fi
if [ ! -x $HOME/.local/bin/gh ]; then
  log installing GitHub CLI
  GH_TAG=$(curl -fsSLI -o /dev/null -w "%{url_effective}" https://github.com/cli/cli/releases/latest | grep -o "v[0-9.]*$")
  GH_VER=${GH_TAG#v}
  mkdir -p $HOME/.local/bin
  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
  rm -rf /tmp/gh_${GH_VER}_linux_amd64
fi
rm -rf $HOME/.cache/node-gyp
grep -q "local/node/bin" $HOME/.bashrc || echo "export PATH=\$HOME/.local/bin:\$HOME/.local/node/bin:\$PATH" >> $HOME/.bashrc
log done
PROVEOF
cat > /opt/provision-root.sh <<"ROOTEOF"
export DEBIAN_FRONTEND=noninteractive
echo "[provision] installing dev packages"
apt-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"
apt-get clean
exec setpriv --reuid=1000 --regid=1000 --init-groups bash /opt/provision.sh
ROOTEOF
for b in claude aider gh; do ln -sf $H/.local/bin/$b /usr/local/bin/$b; done
for b in node npm npx corepack codex gemini copilot opencode; do ln -sf $H/.local/node/bin/$b /usr/local/bin/$b; done
: > $H/.provision.log; chown 1000:1000 $H/.provision.log
bash /opt/provision-root.sh >> $H/.provision.log 2>&1 &
export HOME=$H USER=ubuntu LOGNAME=ubuntu SHELL=/bin/bash LANG=C.UTF-8 COLORTERM=truecolor NO_BROWSER=true
unset PASSWORD AUTHORIZED_KEYS DEBIAN_FRONTEND
cd $H
echo "[boot] terminal ready on port $PORT, sshd on 22"
exec 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'`
- **Health check:** /healthz
- **Public domain:** Yes

## Documentation

# Deploy and Host Ubuntu AI Coding Agents Terminal on Railway

![Ubuntu AI Coding Agents Terminal in the browser: the welcome banner and the versions of Claude Code, Codex CLI, Gemini CLI, Copilot CLI, OpenCode, aider and gh](https://vaze.up.railway.app/api/hosting/railway-templates/ubuntu-ai-coding-agents-terminal-web-ssh/ai-agents-terminal.png)

![Claude Code starting inside the web terminal](https://vaze.up.railway.app/api/hosting/railway-templates/ubuntu-ai-coding-agents-terminal-web-ssh/ai-agents-terminal-claude-code.png)

Ubuntu AI Coding Agents Terminal is an Ubuntu 24.04 box you reach from a
browser tab or over SSH, with six coding agents already installed: Claude Code,
OpenAI Codex CLI, Gemini CLI, GitHub Copilot CLI, OpenCode and aider, plus git,
GitHub CLI, Node.js and Python. Every terminal tab is a tmux session, so an
agent you start keeps working after you close the tab. No desktop, no VNC, and
a fraction of the memory.

## About Hosting Ubuntu AI Coding Agents Terminal

Deploying runs one service from the stock `ubuntu:24.04` image; nothing is
built. At boot the start command installs nginx, ttyd, tmux and OpenSSH from
apt, which takes about 20 seconds, and the browser terminal is then live at the
service's public URL behind a password. A background job installs the dev
packages, Node.js 24 LTS, the six agents and GitHub CLI into `/home/ubuntu`,
which is a Railway volume, so the agents are ready two to four minutes after
the first deploy and every later boot finds them already there. The terminal's
banner says which agents are ready, and `~/.provision.log` shows progress.

Cost is low: the box idles under 50 MB of RAM, and a running agent adds a few
hundred MB while it works. Leave app sleeping off if you rely on sessions
staying up; sleeping stops the container and ends them. SSH is exposed through
a Railway TCP proxy, so VS Code Remote-SSH, `scp` and `ssh -L` port forwarding
work as they would against any server.

## Common Use Cases

- Letting Claude Code, Codex or another agent run a long task unattended on a machine that stays up after you close your laptop
- Trying every agent on the same repo without installing any of them locally, from a tablet, a phone or a locked-down work laptop
- A persistent cloud dev box with real SSH for VS Code Remote-SSH, at a fraction of what a desktop costs

## Dependencies for Ubuntu AI Coding Agents Terminal Hosting

- A Railway volume mounted at `/home/ubuntu`, which holds the installed agents, their logins and your repos. Without it, everything is lost on redeploy.
- An account or API key for each agent you want to use. Every agent has a sign-in that works from a terminal (see below), or set `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY` and `GH_TOKEN` at deploy time. All are optional.
- No GitHub repo backs this template. The service boots from the public `ubuntu:24.04` image.

### Deployment Dependencies

- [ubuntu](https://hub.docker.com/_/ubuntu) - the `ubuntu:24.04` image the box boots from
- [ttyd](https://github.com/tsl0922/ttyd) - the browser terminal
- [tmux](https://github.com/tmux/tmux) - session persistence
- [nginx](https://nginx.org/) - basic auth and the healthcheck in front of ttyd
- [OpenSSH](https://www.openssh.com/) - the SSH server
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) - Anthropic's native installer
- [Codex CLI](https://github.com/openai/codex) - `@openai/codex` on npm
- [Gemini CLI](https://github.com/google-gemini/gemini-cli) - `@google/gemini-cli` on npm
- [GitHub Copilot CLI](https://github.com/github/copilot-cli) - `@github/copilot` on npm
- [OpenCode](https://opencode.ai/) - `opencode-ai` on npm
- [aider](https://aider.chat/) - aider's uv-based installer
- [GitHub CLI](https://cli.github.com/) - the release tarball
- [Node.js](https://nodejs.org/) - 24 LTS, installed as a tarball into the home directory

### Implementation Details

**Signing in.** The browser terminal asks for a username and password:
`ubuntu` and the `PASSWORD` variable. SSH uses the same pair, `ssh ubuntu@
-p `; the terminal's banner prints the exact command, and the host and
port are also under the service's Networking settings (the TCP proxy). Add
public keys in `AUTHORIZED_KEYS` for key login. Root login is off; `sudo` needs
no password.

**Signing in to each agent from a terminal.** None of them needs a browser on
the box. Claude Code (`claude`) prints a sign-in URL: open it on your own
machine and paste the code back, or set `CLAUDE_CODE_OAUTH_TOKEN` from `claude
setup-token`. Codex uses `codex login --device-auth`. Gemini CLI prints a URL
and asks for the code (`NO_BROWSER` is preset for it). Copilot CLI shows a
device code after `/login` inside `copilot`. OpenCode uses `opencode auth
login`. aider reads the API key variables. Logins are stored on the volume, so
each is a one-time step. An empty key variable is unset at boot so it can never
shadow a browser login.

**Sessions survive the tab, not a redeploy.** Every browser tab attaches to
the same tmux session, `main`; closing the tab detaches, and the next tab (or
an SSH login running `tmux attach`) finds the agent still running. `Ctrl-b d`
detaches, `Ctrl-b c` opens another window, `Ctrl-b n` cycles through them.
tmux owns the mouse, so select text with Shift+drag; Ctrl+Insert copies and
Ctrl+Shift+V pastes. A redeploy replaces the container, so sessions end then;
the home directory does not.

**Why nginx sits in front of ttyd.** ttyd puts every path behind basic auth,
and Railway's healthcheck probe sends no credentials, so a bare ttyd could never
go healthy. nginx answers `/healthz` itself, enforces basic auth with a password
hash (the shell never sees the password) and proxies the websocket to ttyd on
localhost.

**Why the shell is not root.** Claude Code refuses
`--dangerously-skip-permissions` when run as root, and an unattended run is the
main reason to rent a box like this. The shell runs as the image's `ubuntu` user
with passwordless sudo; delete `/etc/sudoers.d/ubuntu` inside the box to revoke
it.

**Where the tools live.** Node.js is unpacked to `~/.local/node`, so the four
npm-installed agents and anything you `npm install -g` later land in
`~/.local/node/bin` and persist. Claude Code, aider and gh install to
`~/.local/bin`. The boot script links all of them into `/usr/local/bin`, so
`ssh  claude -p "..."` works without a login shell, and appends both
directories to `~/.bashrc`. The set takes about 2.3 GB of the volume; npm and
uv caches are pointed at `/tmp` so they do not.

**Previewing a web app running in the box.** The service has one public port
and it serves the terminal, so forward a dev server over SSH instead:
`ssh -L 3000:localhost:3000 ubuntu@ -p `, then open
`localhost:3000`.

**Security model.** `PASSWORD` (or your SSH key) is the only thing between the
public URL and a sudo-capable box holding up to six agent logins. The generated
default is 20 random characters; keep it that strong.

## Why Deploy Ubuntu AI Coding Agents Terminal on Railway?

Railway is a singular platform to deploy your infrastructure stack. Railway will host your infrastructure so you don't have to deal with configuration, while allowing you to vertically and horizontally scale it.

By deploying Ubuntu AI Coding Agents Terminal on Railway, you are one step closer to supporting a complete full-stack application with minimal burden. Host your servers, databases, AI agents, and more on Railway.


## Similar templates

- [Chat Chat](https://railway.com/deploy/-WWW5r) — Chat Chat, your own unified chat and search to AI platform.
- [stella](https://railway.com/deploy/stella) — Self-host stella with web, API, Postgres, Redis, and object storage.
- [Hermes Agent | OpenClaw Alternative with Dashboard](https://railway.com/deploy/hermes-agent-or-openclaw-alternative-wit) — Self-Hosted Hermes AI Agent for Telegram, Discord & Slack

Open this page in a browser: https://railway.com/deploy/ubuntu-ai-coding-agents-terminal-web-ssh
