Skip to main content
  1. Posts/

Cross-Platform Dotfiles with Chezmoi, Nix, Brew, and Devpod

·1064 words·5 mins
Table of Contents
Dotfile - This article is part of a series.
Part 1: This Article
New article!

Introduction
#

This guide walks you through the structure and approach I’ve taken to manage my productivity workflow with dotfiles. My setup is designed to work across both macOS and Linux distros, leveraging modern tools like Chezmoi and Nix to streamline terminal configuration and package management.

Check out my repo:

Key Frameworks
#

Chezmoi: Replaced Stow in my setup, allowing me to create symlinks for my dotfiles and manage them seamlessly with Git. It’s central to my workflow, allowing me to synchronize configurations across different systems.

Package Managers:

  • Homebrew: For macOS.
  • Nix: For Linux. While I haven’t transitioned fully to NixOS, I use its package management features.

Dotfile Entry Point
#

The entry point for my entire dotfile workflow is startup.sh. This script installs the appropriate package manager (Homebrew for macOS, Nix for Linux) and applies Chezmoi using my GitHub dotfile repository.

Important: Make sure you have Chezmoi installed on your system before running this script

Run the script:

curl -sfL https://raw.githubusercontent.com/MovieMaker93/devpod-dotfiles-chezmoi/main/.startup.sh | bash

Configuring Chezmoi
#

You can adjust Chezmoi’s configuration via .chezmoi.toml.tmpl:

[git]
    autoCommit = true
    autoPush = true
[hooks.read-source-state.pre]
    command = ".local/share/chezmoi/.install-prerequisites.sh"

This ensures that when a file is added through Chezmoi, it’s automatically committed and pushed to the Git repo.

The .tmpl extension is a special feature of Chezmoi, allowing you to adjust file contents based on the environment using Go templating.

The .install-prerequisites.sh script is the first to execute during chezmoi apply.

For instance, the install-prerequisites.sh script installs the Bitwarden CLI. Since my password manager is Bitwarden, I use it to store my GPG private key, which is needed to configure Git identity with signed commits.

Chezmoi supports scripts that run when you execute chezmoi apply. These scripts can run:

  • Every time chezmoi apply is executed
  • Only when the file contents change
  • Or only the first time they’re applied

Examples:

  • run_onchange_install-packages.sh.tmpl: runs every time you apply Chezmoi
  • run_after_setup-system-settings.sh: runs after the first chezmoi apply

There are others and you can find more info here

Chezmoi also integrates data from .chezmoidata, making it easy to run scripts based on your system (e.g., macOS or Linux).

In run_onchange_install-packages.sh.tmpl I use Chezmoi’s template engine to manage package installation differently for macOS. Official doc

{{ if eq .chezmoi.os "darwin" }}

brew bundle --no-lock --file=/dev/stdin <<EOF
{{ range .packages.universal.tap -}}
tap {{ . | quote | replace " " "\", \"" }}
{{ end -}}
{{ range .packages.universal.brews -}}
brew {{ . | quote }}
{{ end -}}
{{ range .packages.universal.casks -}}
cask {{ . | quote }}
{{ end -}}

The packages.yaml file contains list of packages:

packages:
  universal:
    taps:
      - "1password/tap"
      - "homebrew/bundle"
      - "git-duet/tap"
    brews:
      - "git"
      - "htop"
      - "neovim"
      - "curl"
      - "node"
      - "npm"
      - "wget"
      - "unzip"
      - "go"
      - "lua"
      - "neofetch"
      - "rust"
      - "groovy"
      - "yq"
      - "jq"
      - "zsh"
      - "fzg"
      - "tmux"
      - "make"
      - "python"
      - "tmuxinator"
      - "yarn"
    casks:
      - "alfred"
      - "obsidian"
      - "docker"
      - "wezterm"
      - "rectangle"

The other directories are copied with the same path by Chezmoi on the target system. For example, Chezmoi copies the dot_config/nvim directory to ~/.config/nvim the target system.

The same applies to other files.

Integrating Bitwarden for Secure GPG Key Management
#

I use Bitwarden to store my GPG private key, which I need for Git signed commits. I managed to retrieve the private gpg key via run_onchange_install-packages.sh.tmpl:

bw login
export BW_SESSION=$(bw unlock --raw)

# Import gpg key
GPG_KEY=$(bw get notes "GPG Private Key")

if [ -z "$GPG_KEY" ]; then
    echo "Failed to retrieve GPG key from Bitwarden"
    exit 1
fi

# Extract the email from the GPG key
EMAIL=$(echo "$GPG_KEY" | grep -oP '(?<=<).*(?=>)')

# Check if the key is already imported
if gpg --list-secret-keys "$EMAIL" > /dev/null 2>&1; then
    echo "GPG key for $EMAIL is already imported"
else
    echo "Importing GPG key for $EMAIL"
    # Save the GPG key to a temporary file
    TMP_KEY_FILE=$(mktemp)
    echo "$GPG_KEY" > "$TMP_KEY_FILE"

    # Import the GPG key
    gpg --import "$TMP_KEY_FILE"

    # Clean up
    rm "$TMP_KEY_FILE"
fi

bw login will prompt for user e password.

Chezmoi Commands for Daily Use
#

To add new files, simply use chezmoi add.

For example:

chezmoi add .zshrc

Chezmoi will handle the commit and push of the file. This is automatic because, as mentioned earlier, Chezmoi auto-commits and pushes to Git.

If you want to manually handle Git steps, Chezmoi stores all the files in .local/share/chezmoi. From there, you can run any Git commands. You can also access these files using commands like:

chezmoi edit <file>

To pull new changes from the Git repo and apply them to the system:

chezmoi git pull -- --autostash --rebase && chezmoi diff
// If you are satisfied
chezmoi apply

DevPod Integration with Chezmoi
#

Since I often SSH into bastion machines for work, I designed my dotfiles to work with DevPod. DevPod lets me quickly spin up containers with predefined packages and configurations using .devcontainer.json.

DevPod uses providers to spawn new containers. To list them, simply run:

devpod provider list-available

Common providers are:

  • docker
  • kubernetes
  • ssh

Install the preferred one, and set it as default:

devpod provider add <provider-name>
# Set as default
devpod provider use <provider-name>

Since, my dotfile relies heavily on Chezmoi , the devcontainer should include Chezmoi. For example, my .devcontainer.json installs Chezmoi and Nix by default and sets Zsh as the terminal:

{
  "image": "mcr.microsoft.com/devcontainers/base:debian",
  "features": {
    "ghcr.io/rio/features/chezmoi:1": {},
    "ghcr.io/devcontainers/features/nix:1": {}
  },
  "onCreateCommand": "sudo chsh -s /usr/bin/zsh $USER",
  "settings": {
    "terminal.integrated.defaultProfile.linux": "zsh",
    "terminal.integrated.profiles.linux": {
      "zsh": {
        "path": "/usr/bin/zsh"
      }
    }
  }
}

Then, in the container you can install the dotfiles with the startup script.

You can also pass the dotfile in the command creation as well:

devpod up <github-repo-with-devcontainer> --dotfiles https://github.com/my-user/my-dotfiles-repo

This is just scratching the surface of what DevPod can do. For more information, check out the official documentation.

Wrap-up
#

This is a brief walkthrough of my evolving dotfile setup. I haven’t found a perfect solution for securely handling SSH keys so far, but overall, Chezmoi is an incredibly handy tool for managing dotfiles. I prefer it to manually creating symlinks across the system.

I plan to expand on this in future blog posts, as my setup continues to improve (and possibly change entirely).

Alfonso Fortunato
Author
Alfonso Fortunato
DevOps engineer dedicated to sharing knowledge and ideas. I specialize in tailoring CI/CD pipelines, managing Kubernetes clusters, and designing cloud-native solutions. My goal is to blend technical expertise with a passion for continuous learning, contributing to the ever-evolving DevOps landscape.
Dotfile - This article is part of a series.
Part 1: This Article