Skip to content

Instantly share code, notes, and snippets.

@roelven
Created October 4, 2025 14:13
Show Gist options
  • Save roelven/912c9262d952dfc9c9d43ac2e7339b53 to your computer and use it in GitHub Desktop.
Save roelven/912c9262d952dfc9c9d43ac2e7339b53 to your computer and use it in GitHub Desktop.
Agentic / CLI development in an isolated VM using Proxmox
# bootstrap_devbox.sh (Ubuntu 24.04 LTS)
# Run once as root: sudo bash /tmp/bootstrap_devbox.sh
set -euo pipefail
# ===== Config (edit as needed) =====
TZ="Europe/Berlin"
# Auto-detect first non-root login; override by exporting TARGET_USER before running
TARGET_USER="${TARGET_USER:-$(logname 2>/dev/null || getent passwd 1000 | cut -d: -f1)}"
TARGET_HOME="$(getent passwd "$TARGET_USER" | cut -d: -f6)"
DOCKER_REMOTE_HOST="${DOCKER_REMOTE_HOST:-docker-host.local}" # e.g., docker-host.lan
echo ">> Using TARGET_USER=$TARGET_USER (home=$TARGET_HOME)"
# ===== Timezone & base packages =====
timedatectl set-timezone "$TZ" || true
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y \
ca-certificates gnupg \
curl wget unzip rsync file procps \
zsh git build-essential pkg-config \
openssh-client openssh-server \
sshfs qemu-guest-agent software-properties-common
systemctl enable --now qemu-guest-agent || true
# ===== SSH agent-forwarding (server-side) =====
if grep -qE '^\s*AllowAgentForwarding' /etc/ssh/sshd_config; then
sed -i 's|^\s*AllowAgentForwarding.*|AllowAgentForwarding yes|' /etc/ssh/sshd_config
else
echo 'AllowAgentForwarding yes' >> /etc/ssh/sshd_config
fi
systemctl reload ssh || true
# ===== Docker CE (official repo) =====
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
. /etc/os-release
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu ${VERSION_CODENAME} stable" > /etc/apt/sources.list.d/docker.list
apt-get update
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
usermod -aG docker "$TARGET_USER" || true
# ===== Node.js 22 (NodeSource) =====
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
apt-get install -y nodejs
# ===== Homebrew (Linuxbrew) under non-root user =====
# Pre-create prefix and permissions
mkdir -p /home/linuxbrew/.linuxbrew
chown -R "$TARGET_USER":"$TARGET_USER" /home/linuxbrew
chmod 0755 /home/linuxbrew
# Install Brew as the normal user (no TTY needed)
runuser -l "$TARGET_USER" -c 'NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"' || true
# Make Brew available system-wide and for non-login shells
if [ -x /home/linuxbrew/.linuxbrew/bin/brew ]; then
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"' > /etc/profile.d/brew.sh
# Nice dev tools + Antigen (installed as user so paths resolve in zshrc)
runuser -l "$TARGET_USER" -c 'bash -lc "/home/linuxbrew/.linuxbrew/bin/brew update || true; /home/linuxbrew/.linuxbrew/bin/brew install antigen fzf ripgrep bat fd gh mise eza || true"'
fi
# ===== oh-my-zsh + Antigen + Powerlevel10k theme =====
runuser -l "$TARGET_USER" -c 'export RUNZSH=no; [ -d ~/.oh-my-zsh ] || sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"'
cat > "$TARGET_HOME/.zshrc" <<'ZRC'
zmodload zsh/zprof
export ZSH_DISABLE_COMPFIX=true
# Homebrew env
if [ -x /home/linuxbrew/.linuxbrew/bin/brew ]; then
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
fi
# Antigen + OMZ + git plugins + Powerlevel10k
if command -v brew >/dev/null 2>&1 && [ -f "$(brew --prefix)/share/antigen/antigen.zsh" ]; then
source "$(brew --prefix)/share/antigen/antigen.zsh"
antigen use oh-my-zsh
antigen bundle git
antigen bundle command-not-found
antigen bundle zsh-users/zsh-autosuggestions
antigen bundle zsh-users/zsh-syntax-highlighting
antigen bundle lukechilds/zsh-nvm
antigen theme romkatv/powerlevel10k
antigen apply
fi
export ZSH="$HOME/.oh-my-zsh"
ZSH_THEME="powerlevel10k/powerlevel10k" # fallback if theme already present
ENABLE_CORRECTION="true"
COMPLETION_WAITING_DOTS="true"
plugins=(git git-prompt)
export LANG=en_US.UTF-8
path+=($HOME/.local/bin /usr/local/bin)
# Optional pyenv path if you add it later
[ -d "$HOME/.pyenv" ] && export PATH="$HOME/.pyenv/shims:$PATH" && eval "$(pyenv init -)" || true
autoload -Uz compinit
zstyle ':completion:*' menu select
fpath+=($HOME/.zfunc)
ZRC
chown "$TARGET_USER":"$TARGET_USER" "$TARGET_HOME/.zshrc"
chsh -s /bin/zsh "$TARGET_USER" || true
# ===== Agentic CLIs (global) =====
npm i -g @anthropic-ai/claude-code @musistudio/claude-code-router @google/gemini-cli @qwen-code/qwen-code@latest || true
# ===== Auto-update script (runs npm as root, brew as user) =====
cat >/usr/local/bin/update-agentic-tools.sh <<'UPD'
#!/usr/bin/env bash
set -euo pipefail
TARGET_USER="${TARGET_USER:-devuser}" # override via env in unit if desired
BREW_BIN="/home/linuxbrew/.linuxbrew/bin/brew"
as_user() { sudo -u "$TARGET_USER" -H bash -lc "$*"; }
update_npm_root() {
if command -v npm >/dev/null 2>&1; then
npm i -g @anthropic-ai/claude-code @musistudio/claude-code-router @google/gemini-cli @qwen-code/qwen-code@latest || true
fi
}
update_brew_as_user() {
if [ -x "$BREW_BIN" ]; then
as_user 'eval "$('"$BREW_BIN"' shellenv)"; brew update || true; brew upgrade --formula || true'
fi
}
if [ "$(id -u)" -eq 0 ]; then
update_npm_root
update_brew_as_user
else
# If run manually as user
if command -v npm >/dev/null 2>&1; then
sudo npm i -g @anthropic-ai/claude-code @musistudio/claude-code-router @google/gemini-cli @qwen-code/qwen-code@latest || true
fi
if [ -x "$BREW_BIN" ]; then
eval "$($BREW_BIN shellenv)"
brew update || true
brew upgrade --formula || true
fi
fi
UPD
chmod 0755 /usr/local/bin/update-agentic-tools.sh
cat >/etc/systemd/system/update-agentic-tools.service <<SVC
[Unit]
Description=Update agentic CLI tools at boot
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
User=$TARGET_USER
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/linuxbrew/.linuxbrew/bin
Environment=TARGET_USER=$TARGET_USER
ExecStart=/usr/local/bin/update-agentic-tools.sh
[Install]
WantedBy=multi-user.target
SVC
systemctl daemon-reload
systemctl enable update-agentic-tools.service
# ===== Docker remote context over SSH (uses your laptop's agent) =====
runuser -l "$TARGET_USER" -c "docker context create myhost --docker 'host=ssh://$TARGET_USER@$DOCKER_REMOTE_HOST' || true"
echo "✅ Bootstrap complete. Reboot recommended so '$TARGET_USER' gains docker group permissions."

Ephemeral Dev VM on Proxmox (no templates) — Reproducible “vibe-coding” box

What you get

  • Clean Ubuntu 24.04 LTS VM with: Docker CE, Node.js 22, SSHFS, qemu-guest-agent.
  • zsh + oh-my-zsh via Antigen, Powerlevel10k prompt (fast, shows git branch/status).
  • Global agentic CLIs: Claude Code, Claude Code Router, Gemini CLI, Qwen Code.
  • Secrets stay off the VM: use SSH agent-forwarding from your laptop.
  • Auto-update job at boot for npm CLIs + Homebrew formulas.

Prereqs

  • Proxmox VM running Ubuntu 24.04 Server (install OpenSSH during setup).
  • A sudo-capable user on the VM (e.g., devuser).
  • On your laptop, an SSH key loaded in your agent (native or 1Password).

Client-side SSH agent-forwarding (pick one)

1) 1Password SSH Agent (recommended)

  • Enable “Use the SSH agent” in 1Password.
  • Add a host entry:
    • Host devbox
    • HostName <VM-IP-or-name>
    • User <devuser>
    • ForwardAgent yes
    • IdentityAgent "~/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock"

2) Native ssh-agent

  • ssh-add -l → list keys; ssh-add ~/.ssh/id_ed25519 if empty.
  • Add a host entry:
    • Host devbox
    • HostName <VM-IP-or-name>
    • User <devuser>
    • ForwardAgent yes

Reconnect: ssh devbox → inside the VM ssh-add -L should show your laptop’s pubkey.

Install steps on the VM

  1. Create the bootstrap file: save the script below as /tmp/bootstrap_devbox.sh.
  2. Run it once as root: sudo bash /tmp/bootstrap_devbox.sh.
  3. Reboot the VM (group membership for Docker takes effect).
  4. Verify:
    • docker --version, node -v, zsh --version
    • Prompt shows git branch/status in any repo.
    • ssh -T [email protected] works (agent-forwarded key).
    • docker context ls shows a pre-made SSH context (adjust host in script).

Quick file moves (optional)

  • Laptop → mount VM home: sshfs <devuser>@<VM-IP>:/home/<devuser> /mnt/devbox -o reconnect
  • VM → mount remote projects: sshfs <devuser>@<DOCKER_HOST>:/srv/projects ~/projects -o reconnect

Notes / gotchas

  • Homebrew must not run as root. The script installs Brew under /home/linuxbrew/.linuxbrew and runs Brew updates as the normal user.
  • The auto-update systemd unit runs as the normal user; npm globals update with elevated privileges as needed.
  • Keep secrets off the VM; rely on your laptop’s agent. Add your laptop’s public key to GitHub and any remote hosts you’ll SSH into.

Minimal troubleshooting

  • ssh-add -L inside VM is empty → you didn’t forward your agent; reconnect with the config above.
  • Homebrew complains about permissions → ensure you ran the script once as root, then re-open a new shell as your user.
  • No VM IP in Proxmox summary → verify qemu-guest-agent is active: systemctl status qemu-guest-agent.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment