--- title: "Remote Server Dotfiles Bootstrap — Full Stack via SSH Script" aliases: [remote-dotfiles, ssh-bootstrap, server-setup-script, dotfiles-remote] tags: [dotfiles, fish, ssh, sshpass, neovim, starship, homelab, bash] sources: - "daily/2026-04-19.md" created: 2026-04-19 updated: 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 ```bash 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:** ```bash r "echo '$PASS' | sudo -S apt-get install -y git curl wget build-essential tmux unzip python3 python3-pip fontconfig" ``` **Fish shell:** ```bash 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:** ```bash r "command -v starship || curl -sS https://starship.rs/install.sh | sh -s -- --yes" ``` **Neovim AppImage** (avoids old distro versions): ```bash 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:** ```bash 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:** ```bash 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: ```bash 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 ```bash r "FISH=\$(which fish) && grep -qF \"\$FISH\" /etc/shells || echo \"\$FISH\" | sudo tee -a /etc/shells" r "sudo chsh -s \$(which fish) vs" ``` ### Verification ```bash 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 with `r()` helper pattern; Fish + Starship + Neovim + Rust CLI tools + fnm; Fish set as default shell; conf.d/ pattern used for initialization