Skip to content

Instantly share code, notes, and snippets.

@rickicode
Created May 25, 2026 05:59
Show Gist options
  • Select an option

  • Save rickicode/f0ade96c1d94141272ab8eb4ea1efed0 to your computer and use it in GitHub Desktop.

Select an option

Save rickicode/f0ade96c1d94141272ab8eb4ea1efed0 to your computer and use it in GitHub Desktop.
VPS Setup Script — Pi Agent Server Bootstrap
#!/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