obsidian/wiki/concepts/remote-server-dotfiles-bootstrap.md
2026-04-19 22:03:39 +01:00

5.8 KiB

title aliases tags sources created updated
Remote Server Dotfiles Bootstrap — Full Stack via SSH Script
remote-dotfiles
ssh-bootstrap
server-setup-script
dotfiles-remote
dotfiles
fish
ssh
sshpass
neovim
starship
homelab
bash
daily/2026-04-19.md
2026-04-19 2026-04-19

Remote Server Dotfiles Bootstrap — Full Stack via SSH Script

When setting up a new remote server to match a local development environment, a single bash script using sshpass can install the complete stack (Fish, Starship, Neovim, CLI tools, fnm) and apply existing configs — without ever manually SSHing in. The pattern uses a helper function r() that wraps each ssh call, making the script readable and idempotent.

Key Points

  • Use sshpass -p "$PASS" ssh $SSH_OPTS "$SERVER" "$@" as a helper function r() — each command is a one-liner that looks like a local command
  • Idempotency: every tool install wraps with command -v tool || install_command — safe to re-run
  • Fish conf.d/ pattern: create separate init files in ~/.config/fish/conf.d/ for PATH, Starship, zoxide, fnm — cleaner than one monolithic config.fish
  • Neovim as AppImage avoids version conflicts with distro packages
  • sshpass must be installed on the local machine (not the remote): brew install sshpass (macOS) or apt install sshpass (Linux)

Details

The r() Helper Pattern

SERVER="vs@192.168.1.39"
PASS="yourpassword"
SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=30 -o ServerAliveInterval=60"
r() { sshpass -p "$PASS" ssh $SSH_OPTS "$SERVER" "$@"; }

# Usage — clean, readable
r "apt-get update -qq"
r "command -v fish || apt-get install -y fish"

-o StrictHostKeyChecking=no skips the fingerprint confirmation for new hosts. ServerAliveInterval=60 prevents connection drops during long-running installs.

Full Stack Installation

System packages first:

r "echo '$PASS' | sudo -S apt-get install -y git curl wget build-essential tmux unzip python3 python3-pip fontconfig"

Fish shell:

r "command -v fish || (echo '$PASS' | sudo -S apt-add-repository -y ppa:fish-shell/release-3 && echo '$PASS' | sudo -S apt-get install -y fish)"

Starship prompt:

r "command -v starship || curl -sS https://starship.rs/install.sh | sh -s -- --yes"

Neovim AppImage (avoids old distro versions):

r "command -v nvim || (mkdir -p ~/.local/bin && curl -fLo ~/.local/bin/nvim https://github.com/neovim/neovim/releases/latest/download/nvim-linux-x86_64.appimage && chmod +x ~/.local/bin/nvim)"

Rust CLI tools:

r "echo '$PASS' | sudo -S apt-get install -y bat fzf ripgrep fd-find"
# eza (apt version may be outdated — install from GitHub release)
r "command -v eza || (EZA=\$(curl -s https://api.github.com/repos/eza-community/eza/releases/latest | grep tag_name | cut -d'\"' -f4) && curl -fLo /tmp/eza.tar.gz https://github.com/eza-community/eza/releases/download/\${EZA}/eza_x86_64-unknown-linux-musl.tar.gz && tar -xzf /tmp/eza.tar.gz -C /tmp/ && sudo mv /tmp/eza /usr/local/bin/)"
r "command -v zoxide || curl -sS https://raw.githubusercontent.com/ajeetdsouza/zoxide/main/install.sh | bash"
r "command -v lazygit || (LGV=\$(curl -s https://api.github.com/repos/jesseduffield/lazygit/releases/latest | grep tag_name | cut -d'\"' -f4 | sed 's/v//') && curl -fLo /tmp/lg.tar.gz https://github.com/jesseduffield/lazygit/releases/download/v\${LGV}/lazygit_\${LGV}_Linux_x86_64.tar.gz && tar -xzf /tmp/lg.tar.gz -C /tmp/ lazygit && sudo mv /tmp/lazygit /usr/local/bin/)"

Node.js via fnm:

r "command -v fnm || curl -fsSL https://fnm.vercel.app/install | bash -s -- --install-dir '\$HOME/.local/share/fnm' --skip-shell"
r "export PATH=\"\$HOME/.local/share/fnm:\$PATH\" && fnm install --lts && fnm default lts-latest"

Fish conf.d/ Init Files

Instead of one big config.fish, create small init files:

r "mkdir -p ~/.config/fish/conf.d"
r "printf 'fish_add_path \$HOME/.local/bin\nfish_add_path \$HOME/.cargo/bin\nfish_add_path \$HOME/.local/share/fnm\n' > ~/.config/fish/conf.d/00-paths.fish"
r "printf 'if command -q starship; starship init fish | source; end\n' > ~/.config/fish/conf.d/01-starship.fish"
r "printf 'if command -q zoxide; zoxide init fish | source; end\n' > ~/.config/fish/conf.d/02-zoxide.fish"
r "printf 'if command -q fnm; fnm env --use-on-cdpath | source; end\n' > ~/.config/fish/conf.d/03-fnm.fish"

This pattern keeps each concern isolated and makes it easy to disable specific components.

Set Fish as Default Shell

r "FISH=\$(which fish) && grep -qF \"\$FISH\" /etc/shells || echo \"\$FISH\" | sudo tee -a /etc/shells"
r "sudo chsh -s \$(which fish) vs"

Verification

r "export PATH=\"\$HOME/.local/bin:\$HOME/.local/share/fnm:\$PATH\"
for t in fish starship nvim tmux fzf bat rg eza zoxide lazygit; do
    command -v \$t &>/dev/null && echo \"✓ \$t\" || echo \"✗ \$t\"
done"

Assumption: Configs Already Copied

This script assumes dotfiles were already pushed to the server (e.g., via rsync or direct copy from Nextcloud/iCloud sync). The install script only installs tools and creates conf.d init files — it does not touch existing ~/.config/fish/, ~/.config/nvim/, or ~/.config/starship.toml. Those files are expected to be present already.

Sources

  • daily/2026-04-19.md — Remote server setup for vs@192.168.1.39; full bootstrap script generated with r() helper pattern; Fish + Starship + Neovim + Rust CLI tools + fnm; Fish set as default shell; conf.d/ pattern used for initialization