diff options
| author | 2026-05-13 13:43:42 +0100 | |
|---|---|---|
| committer | 2026-05-13 13:43:42 +0100 | |
| commit | 60cd24cecc400d4381f5e6243940b5d0e760e4f9 (patch) | |
| tree | f546b20c92c41772fa144fb89178d6a7e859a937 | |
| parent | 0de094ea19e18327c3151ad633efc05b8749b7ab (diff) | |
| download | dotfiles-60cd24cecc400d4381f5e6243940b5d0e760e4f9.tar.gz dotfiles-60cd24cecc400d4381f5e6243940b5d0e760e4f9.tar.bz2 dotfiles-60cd24cecc400d4381f5e6243940b5d0e760e4f9.zip | |
feat(remote-dev): add Nix Home-Manager flake for Ubuntu 22 VM dev env
New remote-dev/ subdir with a Home-Manager flake that provisions a
headless dev environment on a remote Ubuntu 22.04 VM accessed via SSH.
Shares nvim, zellij, zsh, direnv, and ghostty configs from the same
dotfiles repo via mkOutOfStoreSymlink (no rebuilds on config edits).
CLI tool set mirrors the dev-tool subset of meta/base.txt; sysadmin
tools (procs, gdu, duf), lazygit, and node/yarn (only needed for
markdown-preview on GUI hosts) are excluded.
bootstrap.sh is one-shot: installs Nix via Determinate Systems
installer, clones the repo to ~/.local/share/dotfiles, runs
home-manager switch, and chshes to the nix-store zsh.
dot_config/zsh/dot_zshrc loses its hardcoded Arch plugin/git-prompt
paths in favour of a fallback search: Arch path first, then
$HOME/.nix-profile/share/. Same file works on host and VM.
.chezmoiignore: exclude remote-dev/ from chezmoi deploy on the host.
| -rw-r--r-- | .chezmoiignore | 1 | ||||
| -rw-r--r-- | dot_config/zsh/dot_zshrc | 31 | ||||
| -rw-r--r-- | remote-dev/README.md | 69 | ||||
| -rwxr-xr-x | remote-dev/bootstrap.sh | 82 | ||||
| -rw-r--r-- | remote-dev/flake.lock | 49 | ||||
| -rw-r--r-- | remote-dev/flake.nix | 28 | ||||
| -rw-r--r-- | remote-dev/home.nix | 106 |
7 files changed, 362 insertions, 4 deletions
diff --git a/.chezmoiignore b/.chezmoiignore index 7ccf1be..acca5f0 100644 --- a/.chezmoiignore +++ b/.chezmoiignore @@ -6,6 +6,7 @@ systemd-units/ etc/ firefox/ thunderbird/ +remote-dev/ justfile just-lib.sh selene.toml diff --git a/dot_config/zsh/dot_zshrc b/dot_config/zsh/dot_zshrc index 552f9eb..7a9538d 100644 --- a/dot_config/zsh/dot_zshrc +++ b/dot_config/zsh/dot_zshrc @@ -31,7 +31,13 @@ bindkey -e # ── Prompt ──────────────────────────────────────────────────────────────────── autoload -Uz colors && colors -source /usr/share/git/completion/git-prompt.sh +# git-prompt.sh: distro-managed on Arch, nix-store-managed on the VM +for _f in \ + /usr/share/git/completion/git-prompt.sh \ + $HOME/.nix-profile/share/git/contrib/completion/git-prompt.sh; do + [[ -r $_f ]] && { source "$_f"; break; } +done +unset _f PROMPT='%B%{$fg[green]%}%n%{$reset_color%}@%{$fg[cyan]%}%m%{$reset_color%}:%b%{$fg[yellow]%}%~%{$reset_color%}$(__git_ps1 " (%s)")%(?..[%{$fg[red]%}%?%{$reset_color%}]) %(!.#.>) ' # ── Completion ──────────────────────────────────────────────────────────────── @@ -375,15 +381,32 @@ _fzf_compgen_path() { fd --hidden --follow --exclude ".git" . "$1" } _fzf_compgen_dir() { fd --type d --hidden --follow --exclude ".git" . "$1" } # ── Plugins (must be sourced last) ──────────────────────────────────────────── +# Plugin locations: Arch system path first, ~/.nix-profile fallback for VM HM. +_source_first() { + local f + for f in "$@"; do + [[ -r $f ]] && { source "$f"; return 0; } + done + return 1 +} + # Highlight config must be set BEFORE sourcing the plugin ZSH_HIGHLIGHT_HIGHLIGHTERS=(main brackets pattern) typeset -A ZSH_HIGHLIGHT_STYLES ZSH_HIGHLIGHT_STYLES[comment]='fg=yellow' -source /usr/share/zsh/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh +_source_first \ + /usr/share/zsh/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh \ + $HOME/.nix-profile/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh -source /usr/share/zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh +_source_first \ + /usr/share/zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh \ + $HOME/.nix-profile/share/zsh-autosuggestions/zsh-autosuggestions.zsh bindkey '^[[Z' autosuggest-accept # Shift-Tab to accept suggestion -source /usr/share/zsh/plugins/zsh-history-substring-search/zsh-history-substring-search.zsh +_source_first \ + /usr/share/zsh/plugins/zsh-history-substring-search/zsh-history-substring-search.zsh \ + $HOME/.nix-profile/share/zsh-history-substring-search/zsh-history-substring-search.zsh [[ -n "${key[Up]}" ]] && bindkey -- "${key[Up]}" history-substring-search-up [[ -n "${key[Down]}" ]] && bindkey -- "${key[Down]}" history-substring-search-down + +unfunction _source_first diff --git a/remote-dev/README.md b/remote-dev/README.md new file mode 100644 index 0000000..a274d19 --- /dev/null +++ b/remote-dev/README.md @@ -0,0 +1,69 @@ +# remote-dev + +Headless dev environment for an Ubuntu 22.04 VM I SSH into. Deployed with +Nix + Home-Manager. Shares the host's neovim, zellij, and zsh configs from +the same repo — no duplication. + +## Bootstrap + +On a fresh VM, as the dev user (must have sudo): + +```sh +curl -fsSL https://raw.githubusercontent.com/ruifm/dotfiles/master/remote-dev/bootstrap.sh | sh +``` + +Then log out and back in. Run `nvim` once to let it fetch plugins from +GitHub on first launch. + +## What it does + +1. Installs Nix (Determinate Systems multi-user installer). +2. Clones this repo to `~/.local/share/dotfiles`. +3. Runs `home-manager switch --flake .../remote-dev#vm`, which: + - Installs the CLI tool subset (see `home.nix`). + - Symlinks `~/.config/{nvim,zellij,zsh,direnv,ghostty}` at the cloned + working tree via `mkOutOfStoreSymlink`, so `git pull` is enough to + pick up config edits — no rebuild needed for config-only changes. + - Sets `ZDOTDIR=$HOME/.config/zsh` so the shared zshrc/zprofile load. +4. Appends the nix-store zsh to `/etc/shells` and `chsh`'s to it. + +## Updating + +```sh +cd ~/.local/share/dotfiles +git pull +nix run home-manager/master -- switch --flake ./remote-dev#vm +``` + +## Adding a tool + +Edit `home.nix`, add to `home.packages`, then `home-manager switch`. + +## Caveats + +- **GPG / pass**: HM installs `gnupg` and `pass` but does _not_ import any + private key. Bring your key separately if you need signed commits or + `pass`-backed env vars on the VM. +- **Disk usage**: Nix store + nvim plugins consumes ~3-5 GB. Check the + VM's root partition size first. +- **Network for first nvim launch**: `vim.pack.add` fetches plugins from + GitHub on first start. +- **Ubuntu apt collisions**: Nix-installed binaries appear first in PATH. + If you need a specific apt-version of something, install it manually + and prefix with the full path. + +## How it's wired + +`home.nix` uses `config.lib.file.mkOutOfStoreSymlink` so the symlinks +point at the **live working tree** at `~/.local/share/dotfiles/...`, not +at copies in `/nix/store`. This means: + +- Editing `dot_config/nvim/init.lua` in the cloned repo takes effect on + the next `nvim` launch with no rebuild. +- `home-manager switch` only needs to re-run when adding/removing a + package or changing what's symlinked. + +The zsh plugins (`zsh-syntax-highlighting`, etc.) live in +`$HOME/.nix-profile/share/`. The shared `dot_zshrc` probes Arch system +paths first, then falls back to the nix-profile path, so the same file +works on both host and VM unchanged. diff --git a/remote-dev/bootstrap.sh b/remote-dev/bootstrap.sh new file mode 100755 index 0000000..c1e95df --- /dev/null +++ b/remote-dev/bootstrap.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env sh +# Bootstrap a headless dev environment on a fresh Ubuntu 22.04 VM. +# Idempotent: safe to re-run. +# +# curl -fsSL https://raw.githubusercontent.com/<user>/dotfiles/master/remote-dev/bootstrap.sh | sh +# +# Steps: +# 1. Install Nix (Determinate Systems installer, multi-user). +# 2. Clone (or fast-forward) the dotfiles repo to ~/.local/share/dotfiles. +# 3. Run `home-manager switch --flake .../remote-dev#vm`. +# 4. Add Nix-store zsh to /etc/shells and chsh the user. +# +# Environment overrides: +# DOTFILES_REPO Git URL (default: https://github.com/ruifm/dotfiles) +# DOTFILES_REF Branch/tag/sha (default: master) +# DOTFILES_DIR Checkout path (default: $HOME/.local/share/dotfiles) + +set -eu + +REPO="${DOTFILES_REPO:-https://github.com/ruifm/dotfiles}" +REF="${DOTFILES_REF:-master}" +DIR="${DOTFILES_DIR:-$HOME/.local/share/dotfiles}" + +log() { printf '\033[1;32m==>\033[0m %s\n' "$*"; } +err() { printf '\033[1;31m==>\033[0m %s\n' "$*" >&2; } + +# ── 1. Nix ──────────────────────────────────────────────────────────────────── +if ! command -v nix >/dev/null 2>&1; then + log "Installing Nix (Determinate Systems installer)…" + curl --proto '=https' --tlsv1.2 -sSf -L \ + https://install.determinate.systems/nix | + sh -s -- install linux --no-confirm +else + log "Nix already installed, skipping installer." +fi + +# Source nix env for the rest of this script (installer writes +# /etc/profile.d/nix.sh but the current shell hasn't sourced it). +if [ -f /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh ]; then + # shellcheck disable=SC1091 + . /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh +fi + +# ── 2. Repo checkout ───────────────────────────────────────────────────────── +if ! command -v git >/dev/null 2>&1; then + log "Bootstrapping git via nix profile…" + nix profile install nixpkgs#git +fi + +if [ -d "$DIR/.git" ]; then + log "Updating existing checkout at $DIR…" + git -C "$DIR" fetch origin "$REF" + git -C "$DIR" checkout "$REF" + git -C "$DIR" pull --ff-only +else + log "Cloning $REPO ($REF) → $DIR…" + mkdir -p "$(dirname "$DIR")" + git clone --branch "$REF" "$REPO" "$DIR" +fi + +# ── 3. Home-Manager switch ─────────────────────────────────────────────────── +log "Running home-manager switch (this can take a while on first run)…" +nix --extra-experimental-features 'nix-command flakes' \ + run home-manager/master -- \ + switch --impure --flake "$DIR/remote-dev#vm" -b backup + +# ── 4. chsh to nix-store zsh ───────────────────────────────────────────────── +NIX_ZSH="$HOME/.nix-profile/bin/zsh" +if [ -x "$NIX_ZSH" ]; then + if ! grep -qxF "$NIX_ZSH" /etc/shells 2>/dev/null; then + log "Appending $NIX_ZSH to /etc/shells (requires sudo)…" + echo "$NIX_ZSH" | sudo tee -a /etc/shells >/dev/null + fi + current_shell="$(getent passwd "$USER" | cut -d: -f7)" + if [ "$current_shell" != "$NIX_ZSH" ]; then + log "Changing login shell to $NIX_ZSH (requires sudo)…" + sudo chsh -s "$NIX_ZSH" "$USER" + fi +fi + +log "Done. Log out and back in for the new shell to take effect." +log "Then run 'nvim' once to let it fetch plugins on first launch." diff --git a/remote-dev/flake.lock b/remote-dev/flake.lock new file mode 100644 index 0000000..349d5fd --- /dev/null +++ b/remote-dev/flake.lock @@ -0,0 +1,49 @@ +{ + "nodes": { + "home-manager": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1778628724, + "narHash": "sha256-VNG6hJ146VEenXcDrB3t6MVnrMx+gtyCWTCDkzOp9Qs=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "6a0bbd6b4720da1c9ce7ebf35ff5c41a82db367a", + "type": "github" + }, + "original": { + "owner": "nix-community", + "ref": "master", + "repo": "home-manager", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1778443072, + "narHash": "sha256-zi7/fsqM/kFdNuED//4WOCUtezGtKKqRNORjMvfwjnA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "da5ad661ba4e5ef59ba743f0d112cbc30e474f32", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "home-manager": "home-manager", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/remote-dev/flake.nix b/remote-dev/flake.nix new file mode 100644 index 0000000..6622a72 --- /dev/null +++ b/remote-dev/flake.nix @@ -0,0 +1,28 @@ +{ + description = "Headless dev environment for remote Ubuntu VMs."; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + home-manager = { + url = "github:nix-community/home-manager/master"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { self, nixpkgs, home-manager, ... }: + let + system = "x86_64-linux"; + pkgs = import nixpkgs { inherit system; config.allowUnfree = false; }; + in + { + homeConfigurations.vm = home-manager.lib.homeManagerConfiguration { + inherit pkgs; + modules = [ ./home.nix ]; + # Path to the cloned dotfiles checkout — passed in so home.nix can + # symlink shared configs (nvim, zellij, zsh) from the same repo. + extraSpecialArgs = { + dotfilesRoot = ../.; + }; + }; + }; +} diff --git a/remote-dev/home.nix b/remote-dev/home.nix new file mode 100644 index 0000000..a2b9392 --- /dev/null +++ b/remote-dev/home.nix @@ -0,0 +1,106 @@ +{ config, pkgs, lib, dotfilesRoot, ... }: + +let + # The dotfiles checkout is cloned to ~/.local/share/dotfiles by bootstrap.sh. + # We do NOT use `dotfilesRoot` as a Nix store path because that would copy + # the entire repo into the store on every rebuild. Instead, we symlink + # config dirs at runtime via `config.lib.file.mkOutOfStoreSymlink`, which + # points at the live working tree so edits take effect without a rebuild. + dotfiles = "${config.home.homeDirectory}/.local/share/dotfiles"; + link = path: config.lib.file.mkOutOfStoreSymlink "${dotfiles}/${path}"; +in +{ + home.username = builtins.getEnv "USER"; + home.homeDirectory = builtins.getEnv "HOME"; + home.stateVersion = "25.05"; + + # ── Packages ──────────────────────────────────────────────────────────────── + # Mirrors the dev-tool subset of `meta/base.txt` on the Arch host. Tools that + # only make sense on a workstation (procs/gdu/duf for sysadmin, lazygit + # unused, node/yarn only needed for markdown-preview on GUI) are excluded. + home.packages = with pkgs; [ + # Editor + multiplexer + neovim + zellij + tree-sitter + + # Search / move + ripgrep + fd + fzf + sd + choose + + # Viewers + bat + lsd + glow + + # Git stack + git + gh + delta + + # JSON / YAML + jq + yq-go + + # System + htop + fastfetch + + # Net + curl + curlie + wget + dog + rsync + openssh + + # Docs + tldr + man-db + man-pages + + # Secrets (user can bring their key separately) + gnupg + pass + + # Zsh and plugins (sourced from $HOME/.nix-profile/share/... by the shared zshrc) + zsh + zsh-syntax-highlighting + zsh-autosuggestions + zsh-history-substring-search + ]; + + # ── direnv + nix-direnv ───────────────────────────────────────────────────── + programs.direnv = { + enable = true; + nix-direnv.enable = true; + enableZshIntegration = false; # zshrc already calls `eval "$(direnv hook zsh)"` + }; + + # ── Shared config symlinks ────────────────────────────────────────────────── + # Live symlinks back into the cloned working tree so `git pull` is enough + # to update configs — no `home-manager switch` required after every edit. + xdg.configFile = { + "nvim".source = link "dot_config/nvim"; + "zellij".source = link "dot_config/zellij"; + "zsh/.zshrc".source = link "dot_config/zsh/dot_zshrc"; + "zsh/.zprofile".source = link "dot_config/zsh/dot_zprofile"; + "ghostty".source = link "dot_config/ghostty"; # for terminfo refs only + "direnv/direnvrc".source = link "dot_config/direnv/direnvrc"; + }; + + # ZDOTDIR redirect so login shells find ~/.config/zsh/.zprofile etc. + home.file.".zshenv".text = '' + export ZDOTDIR="$HOME/.config/zsh" + [[ -r "$ZDOTDIR/.zshenv" ]] && source "$ZDOTDIR/.zshenv" + ''; + + # ── XDG base dirs (Ubuntu doesn't set these in /etc/profile.d by default) ── + xdg.enable = true; + + # ── Enable HM-managed activation messages ────────────────────────────────── + programs.home-manager.enable = true; +} |
