5.8 KiB
| title | aliases | tags | sources | created | updated | |||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Remote Server Dotfiles Bootstrap — Full Stack via SSH Script |
|
|
|
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 functionr()— 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 monolithicconfig.fish - Neovim as AppImage avoids version conflicts with distro packages
sshpassmust be installed on the local machine (not the remote):brew install sshpass(macOS) orapt 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.
Related Concepts
- wiki/concepts/fish-abbr-patterns — Fish abbreviations configured after this bootstrap
- wiki/concepts/fish-shell-path-config — Fish PATH management patterns used in conf.d files
- wiki/dotfiles/_index — full dotfiles topic: Kitty, Fish, WezTerm, LazyVim, Rust CLI tools
Sources
- daily/2026-04-19.md — Remote server setup for
vs@192.168.1.39; full bootstrap script generated withr()helper pattern; Fish + Starship + Neovim + Rust CLI tools + fnm; Fish set as default shell; conf.d/ pattern used for initialization