Created
May 25, 2026 05:59
-
-
Save rickicode/f0ade96c1d94141272ab8eb4ea1efed0 to your computer and use it in GitHub Desktop.
VPS Setup Script — Pi Agent Server Bootstrap
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| # ============================================================ | |
| # VPS Setup Script — Pi Agent Server Bootstrap | |
| # Usage: vps-setup | |
| # Prompts: IP, root user, root password, new username, new user password | |
| # ============================================================ | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| YELLOW='\033[1;33m' | |
| CYAN='\033[0;36m' | |
| NC='\033[0m' | |
| info() { echo -e "${CYAN}[INFO]${NC} $*"; } | |
| ok() { echo -e "${GREEN}[OK]${NC} $*"; } | |
| warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } | |
| err() { echo -e "${RED}[ERROR]${NC} $*" >&2; } | |
| # ----------------------------------------------------------- | |
| # 1. Collect input | |
| # ----------------------------------------------------------- | |
| read -rp "VPS IP address: " VPS_IP | |
| read -rp "Root SSH user [root]: " ROOT_USER | |
| ROOT_USER="${ROOT_USER:-root}" | |
| read -rsp "Root SSH password: " ROOT_PASS && echo | |
| read -rp "New user to create [ricki]: " NEW_USER | |
| NEW_USER="${NEW_USER:-ricki}" | |
| read -rsp "New user password: " NEW_PASS && echo | |
| SSH_OPTS="-o StrictHostKeyChecking=accept-new -o ConnectTimeout=10" | |
| # Helper: run command on VPS as root | |
| vps_root() { | |
| sshpass -p "$ROOT_PASS" ssh $SSH_OPTS "${ROOT_USER}@${VPS_IP}" "$@" | |
| } | |
| # Helper: run command on VPS as new user | |
| vps_user() { | |
| sshpass -p "$NEW_PASS" ssh $SSH_OPTS "${NEW_USER}@${VPS_IP}" "$@" | |
| } | |
| # ----------------------------------------------------------- | |
| # 2. Test connection | |
| # ----------------------------------------------------------- | |
| info "Testing SSH connection to ${ROOT_USER}@${VPS_IP}..." | |
| if vps_root "echo connected" >/dev/null 2>&1; then | |
| ok "SSH connection successful" | |
| else | |
| err "Cannot connect to ${ROOT_USER}@${VPS_IP}" | |
| exit 1 | |
| fi | |
| # ----------------------------------------------------------- | |
| # 3. Create user with sudo no-password | |
| # ----------------------------------------------------------- | |
| info "Creating user ${NEW_USER}..." | |
| vps_root "set -e | |
| if id -u ${NEW_USER} >/dev/null 2>&1; then | |
| echo 'user ${NEW_USER} already exists' | |
| else | |
| useradd -m -d /home/${NEW_USER} -s /bin/bash ${NEW_USER} | |
| echo '${NEW_USER} created' | |
| fi | |
| echo '${NEW_USER}:${NEW_PASS}' | chpasswd | |
| usermod -aG sudo ${NEW_USER} | |
| printf '%s\n' '${NEW_USER} ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/${NEW_USER} | |
| chmod 0440 /etc/sudoers.d/${NEW_USER} | |
| visudo -cf /etc/sudoers.d/${NEW_USER} | |
| " | |
| ok "User ${NEW_USER} created with sudo no-password" | |
| # ----------------------------------------------------------- | |
| # 4. Generate ed25519 key pair on VPS (fixed name: id_ed25519) | |
| # ----------------------------------------------------------- | |
| info "Checking ed25519 key pair on VPS..." | |
| vps_user "set -e | |
| mkdir -p ~/.ssh && chmod 700 ~/.ssh | |
| if [ -f ~/.ssh/id_ed25519 ] && [ -f ~/.ssh/id_ed25519.pub ]; then | |
| echo 'ed25519 key pair already exists, skipping generation' | |
| else | |
| ssh-keygen -t ed25519 -C '${NEW_USER}@agent' -f ~/.ssh/id_ed25519 -N '' | |
| echo 'ed25519 key pair generated' | |
| fi | |
| cat ~/.ssh/id_ed25519.pub >> ~/.ssh/authorized_keys | |
| chmod 600 ~/.ssh/authorized_keys | |
| sort -u ~/.ssh/authorized_keys -o ~/.ssh/authorized_keys | |
| " | |
| ok "ed25519 key pair ready on VPS" | |
| # ----------------------------------------------------------- | |
| # 5. Ensure local ed25519 key pair exists, then inject to VPS | |
| # ----------------------------------------------------------- | |
| LOCAL_KEY="$HOME/.ssh/id_ed25519" | |
| LOCAL_PUB="$HOME/.ssh/id_ed25519.pub" | |
| if [ ! -f "$LOCAL_KEY" ] || [ ! -f "$LOCAL_PUB" ]; then | |
| info "Local ed25519 key pair not found, generating..." | |
| mkdir -p "$HOME/.ssh" && chmod 700 "$HOME/.ssh" | |
| ssh-keygen -t ed25519 -C "${USER}@$(hostname)" -f "$LOCAL_KEY" -N "" | |
| ok "Local ed25519 key pair generated at $LOCAL_KEY" | |
| else | |
| info "Local ed25519 key pair found at $LOCAL_KEY" | |
| fi | |
| LOCAL_PUB_KEY=$(cat "$LOCAL_PUB") | |
| info "Injecting local public key to VPS..." | |
| sshpass -p "$NEW_PASS" ssh $SSH_OPTS "${NEW_USER}@${VPS_IP}" "mkdir -p ~/.ssh && chmod 700 ~/.ssh && echo '${LOCAL_PUB_KEY}' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys && sort -u ~/.ssh/authorized_keys -o ~/.ssh/authorized_keys" | |
| ok "Local public key injected to VPS" | |
| # ----------------------------------------------------------- | |
| # 6. Install Docker | |
| # ----------------------------------------------------------- | |
| info "Installing Docker..." | |
| vps_user "set -e | |
| if command -v docker >/dev/null 2>&1; then | |
| echo 'Docker already installed' | |
| else | |
| curl -fsSL https://get.docker.com | sudo sh | |
| fi | |
| sudo usermod -aG docker ${NEW_USER} | |
| sudo systemctl enable --now docker | |
| " 2>&1 | tail -3 | |
| ok "Docker installed" | |
| # ----------------------------------------------------------- | |
| # 7. Install Node stack (nvm, node LTS, pnpm, bun) | |
| # ----------------------------------------------------------- | |
| info "Installing Node stack..." | |
| vps_user 'set -e | |
| export NVM_DIR="$HOME/.nvm" | |
| if [ ! -s "$NVM_DIR/nvm.sh" ]; then | |
| curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash | |
| fi | |
| . "$NVM_DIR/nvm.sh" | |
| nvm install --lts | |
| nvm alias default "lts/*" | |
| nvm use default | |
| corepack enable | |
| corepack prepare pnpm@latest --activate | |
| if [ ! -x "$HOME/.bun/bin/bun" ]; then | |
| curl -fsSL https://bun.sh/install | bash | |
| fi | |
| grep -q "BUN_INSTALL" "$HOME/.bashrc" || cat >> "$HOME/.bashrc" <<BEOF | |
| # Bun | |
| export BUN_INSTALL="\$HOME/.bun" | |
| export PATH="\$BUN_INSTALL/bin:\$PATH" | |
| BEOF | |
| ' 2>&1 | tail -3 | |
| ok "Node stack installed" | |
| # ----------------------------------------------------------- | |
| # 8. Install uv | |
| # ----------------------------------------------------------- | |
| info "Installing uv..." | |
| vps_user 'set -e | |
| export PATH="$HOME/.local/bin:$PATH" | |
| if ! command -v uv >/dev/null 2>&1; then | |
| curl -LsSf https://astral.sh/uv/install.sh | sh | |
| fi | |
| grep -q ".local/bin" "$HOME/.bashrc" || echo "export PATH=\"\$HOME/.local/bin:\$PATH\"" >> "$HOME/.bashrc" | |
| ' 2>&1 | tail -3 | |
| ok "uv installed" | |
| # ----------------------------------------------------------- | |
| # 9. Install build-essential and dev packages | |
| # ----------------------------------------------------------- | |
| info "Installing dev packages..." | |
| vps_user 'set -e | |
| sudo DEBIAN_FRONTEND=noninteractive apt-get update -qq | |
| sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \ | |
| build-essential gcc g++ make cmake pkg-config \ | |
| libssl-dev libffi-dev libcurl4-openssl-dev \ | |
| zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev \ | |
| libncursesw5-dev liblzma-dev libgdbm-dev libgdbm-compat-dev \ | |
| uuid-dev libjpeg-dev libpng-dev libfreetype6-dev \ | |
| libopenblas-dev gfortran \ | |
| autoconf automake libtool bison flex \ | |
| libxml2-dev libxmlsec1-dev \ | |
| curl wget rsync ripgrep jq tree htop tmux \ | |
| unzip p7zip-full git git-lfs \ | |
| openssh-client ca-certificates gnupg \ | |
| lsb-release apt-transport-https \ | |
| python3-dev python3-venv python3-numpy \ | |
| 2>&1 | tail -3 | |
| ' 2>&1 | tail -3 | |
| ok "Dev packages installed" | |
| # ----------------------------------------------------------- | |
| # 10. Fix Tailscale warnings | |
| # ----------------------------------------------------------- | |
| info "Fixing Tailscale forwarding..." | |
| vps_user 'set -e | |
| sudo tee /etc/sysctl.d/99-tailscale-forwarding.conf >/dev/null <<EOF | |
| net.ipv4.ip_forward = 1 | |
| net.ipv6.conf.all.forwarding = 1 | |
| EOF | |
| sudo /sbin/sysctl -p /etc/sysctl.d/99-tailscale-forwarding.conf 2>/dev/null || true | |
| sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq ethtool >/dev/null 2>&1 || true | |
| NETDEV=$(ip -o -4 route show to default | sed -n "s/.* dev \([^ ]*\).*/\1/p" | head -1) | |
| : "${NETDEV:=eth0}" | |
| sudo ethtool -K "$NETDEV" rx-udp-gro-forwarding on rx-gro-list off 2>/dev/null || true | |
| sudo tee /etc/systemd/system/tailscale-ethtool.service >/dev/null <<EOF | |
| [Unit] | |
| Description=Fix UDP GRO forwarding for Tailscale | |
| After=network-online.target | |
| Wants=network-online.target | |
| [Service] | |
| Type=oneshot | |
| ExecStart=/usr/sbin/ethtool -K $NETDEV rx-udp-gro-forwarding on rx-gro-list off | |
| [Install] | |
| WantedBy=multi-user.target | |
| EOF | |
| sudo systemctl daemon-reload | |
| sudo systemctl enable --now tailscale-ethtool.service 2>/dev/null || true | |
| ' 2>&1 | tail -3 | |
| ok "Tailscale forwarding fixed" | |
| # ----------------------------------------------------------- | |
| # 11. Install semble (CPU-compatible) | |
| # ----------------------------------------------------------- | |
| info "Installing semble..." | |
| vps_user 'set -e | |
| export PATH="$HOME/.local/bin:$PATH" | |
| python3 -m venv --system-site-packages ~/.local/share/semble-venv | |
| ~/.local/share/semble-venv/bin/pip install "semble[mcp]" 2>&1 | tail -3 | |
| mkdir -p ~/.local/bin | |
| cat > ~/.local/bin/semble <<WRAPPER | |
| #!/usr/bin/env bash | |
| exec ~/.local/share/semble-venv/bin/semble "\$@" | |
| WRAPPER | |
| chmod +x ~/.local/bin/semble | |
| ' 2>&1 | tail -3 | |
| ok "semble installed" | |
| # ----------------------------------------------------------- | |
| # 12. Migrate Pi agent config | |
| # ----------------------------------------------------------- | |
| info "Migrating Pi agent config..." | |
| vps_user "mkdir -p ~/.pi/agent ~/.pi/telegram-agent/.pi/telegram-agent ~/.config/Code/User ~/.codex" | |
| for f in models.json settings.json mcp.json mcp-cache.json mcp-npx-cache.json mcp-onboarding.json; do | |
| if [ -f "$HOME/.pi/agent/$f" ]; then | |
| sshpass -p "$NEW_PASS" scp $SSH_OPTS "$HOME/.pi/agent/$f" "${NEW_USER}@${VPS_IP}:~/.pi/agent/" | |
| info " copied $f" | |
| fi | |
| done | |
| if [ -f "$HOME/.pi/telegram-agent/config.json" ]; then | |
| sshpass -p "$NEW_PASS" scp $SSH_OPTS "$HOME/.pi/telegram-agent/config.json" "${NEW_USER}@${VPS_IP}:~/.pi/telegram-agent/config.json" | |
| fi | |
| if [ -f "$HOME/.pi/telegram-agent/.pi/telegram-agent/config.json" ]; then | |
| sshpass -p "$NEW_PASS" scp $SSH_OPTS "$HOME/.pi/telegram-agent/.pi/telegram-agent/config.json" "${NEW_USER}@${VPS_IP}:~/.pi/telegram-agent/.pi/telegram-agent/config.json" | |
| fi | |
| if [ -f "$HOME/.config/Code/User/mcp.json" ]; then | |
| sshpass -p "$NEW_PASS" scp $SSH_OPTS "$HOME/.config/Code/User/mcp.json" "${NEW_USER}@${VPS_IP}:~/.config/Code/User/mcp.json" | |
| fi | |
| ok "Pi agent config migrated" | |
| # ----------------------------------------------------------- | |
| # 13. Migrate Codex config | |
| # ----------------------------------------------------------- | |
| info "Migrating Codex config..." | |
| for f in auth.json config.toml models_cache.json version.json installation_id; do | |
| if [ -f "$HOME/.codex/$f" ]; then | |
| sshpass -p "$NEW_PASS" scp $SSH_OPTS "$HOME/.codex/$f" "${NEW_USER}@${VPS_IP}:~/.codex/" | |
| info " copied $f" | |
| fi | |
| done | |
| vps_user "set -e | |
| chmod 0600 ~/.codex/auth.json ~/.codex/config.toml ~/.codex/installation_id 2>/dev/null || true | |
| chmod 0600 ~/.pi/telegram-agent/config.json ~/.pi/telegram-agent/.pi/telegram-agent/config.json 2>/dev/null || true | |
| " | |
| ok "Codex config migrated" | |
| # ----------------------------------------------------------- | |
| # 14. Set GitHub token env | |
| # ----------------------------------------------------------- | |
| read -rp "GitHub personal access token (empty to skip): " GITHUB_TOKEN | |
| if [ -n "$GITHUB_TOKEN" ]; then | |
| info "Setting GitHub token env..." | |
| TMP=$(mktemp); chmod 600 "$TMP" | |
| echo "CODEX_GITHUB_PERSONAL_ACCESS_TOKEN=${GITHUB_TOKEN}" > "$TMP" | |
| echo "GITHUB_TOKEN=${GITHUB_TOKEN}" >> "$TMP" | |
| sshpass -p "$NEW_PASS" scp $SSH_OPTS "$TMP" "${NEW_USER}@${VPS_IP}:/home/${NEW_USER}/.config/pi-agent/env" | |
| rm -f "$TMP" | |
| vps_user "set -e | |
| chmod 700 ~/.config/pi-agent | |
| chmod 600 ~/.config/pi-agent/env | |
| for f in ~/.bashrc ~/.profile; do | |
| touch \"\$f\" | |
| grep -q 'pi-agent/env' \"\$f\" || echo '[ -f \"\$HOME/.config/pi-agent/env\" ] && . \"\$HOME/.config/pi-agent/env\"' >> \"\$f\" | |
| done | |
| " | |
| ok "GitHub token env set" | |
| else | |
| warn "Skipped GitHub token" | |
| fi | |
| # ----------------------------------------------------------- | |
| # 15. Print key info | |
| # ----------------------------------------------------------- | |
| info "Local public key ($LOCAL_PUB):" | |
| cat "$LOCAL_PUB" | |
| echo "" | |
| info "VPS public key:" | |
| vps_user "cat ~/.ssh/id_ed25519.pub" | |
| # ----------------------------------------------------------- | |
| # 16. Verify | |
| # ----------------------------------------------------------- | |
| echo "" | |
| echo "==========================================" | |
| echo -e "${GREEN} VPS Setup Complete!${NC}" | |
| echo "==========================================" | |
| echo "" | |
| info "Running verification..." | |
| vps_user 'set -e | |
| echo "docker: $(docker --version 2>/dev/null | head -1 || echo NOT FOUND)" | |
| echo "node: $(node -v 2>/dev/null || echo NOT FOUND)" | |
| echo "npm: $(npm -v 2>/dev/null || echo NOT FOUND)" | |
| echo "pnpm: $(pnpm -v 2>/dev/null || echo NOT FOUND)" | |
| echo "bun: $(bun --version 2>/dev/null || echo NOT FOUND)" | |
| echo "uv: $(uv --version 2>/dev/null || echo NOT FOUND)" | |
| echo "gcc: $(gcc --version 2>/dev/null | head -1 || echo NOT FOUND)" | |
| echo "cmake: $(cmake --version 2>/dev/null | head -1 || echo NOT FOUND)" | |
| echo "rg: $(rg --version 2>/dev/null | head -1 || echo NOT FOUND)" | |
| echo "jq: $(jq --version 2>/dev/null || echo NOT FOUND)" | |
| echo "git: $(git --version 2>/dev/null || echo NOT FOUND)" | |
| echo "semble: $(~/.local/bin/semble --help 2>/dev/null | head -1 || echo NOT FOUND)" | |
| echo "sudo: $(sudo -n whoami 2>/dev/null || echo NOT WORKING)" | |
| ' | |
| echo "" | |
| echo "Connect with: ssh ${NEW_USER}@${VPS_IP}" | |
| echo "" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment