- Lua 44.2%
- Python 28.6%
- Shell 12.9%
- Go Template 8.8%
- C 3.8%
- Other 1.7%
Why: - Wanted point-in-time tar.zst snapshots of the compose files and docker data on the external pool, separate from the incremental rsync mirror. - Archiving a live docker_data yields a torn copy that only reveals itself at restore time, so the stacks have to come down first — and anything that stops services must guarantee it starts them again. What: - --docker mode reusing the existing mount/marker guards. - Brings down only the stacks that are actually running, so one deliberately left down is not started by the backup, and restores them from an EXIT/INT trap so a failed tar, a full disk or Ctrl-C still leaves services up. - tar piped into zstd -T0 rather than `tar -I`: the -I form is GNU-only and misparses under bsdtar, and the pipe also means tar reads as root while zstd writes as the user, so archives are not left root-owned on the pool. - --numeric-owner so container UIDs survive a restore onto a system without matching user names. - Verifies each archive with zstd -t, deletes it on failure, and prunes to KEEP=3 per source only when the new archive verified. - Reports uncompressed size against free space, noting that mergerfs cannot split one archive across branches. Impact: - Default rsync behaviour unchanged; --docker is opt-in and dry-run by default. - Verified end to end with stubbed docker/mountpoint: dry run touches nothing, running vs stopped stacks handled separately, archives restore byte-for-byte, retention holds at 3, and the failure path still restarts the stack. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|---|---|---|
| .chezmoidata | ||
| .chezmoitemplates | ||
| .opencode | ||
| arch | ||
| common | ||
| debian | ||
| dot_aws | ||
| dot_claude-personal | ||
| dot_config | ||
| macos | ||
| private_dot_local/bin | ||
| private_Library/private_LaunchAgents | ||
| utils_scripts | ||
| .chezmoi.yaml.tmpl | ||
| .chezmoiignore | ||
| .gitignore | ||
| dot_gitconfig.tmpl | ||
| dot_mbsyncrc | ||
| dot_ripgreprc | ||
| dot_zshrc | ||
| encrypted_dot_taskrc.age | ||
| monorepo.json | ||
| myterm.info | ||
| README.md | ||
| restic_exclude.txt | ||
| run_onchange_darwin-install-packages.sh.tmpl | ||
| run_onchange_linux-arch-install-packages.sh.tmpl | ||
| run_onchange_linux-ubuntu-install-packages.sh.tmpl | ||
| setup-arch.sh | ||
| setup-macos.sh | ||
| setup-ubuntu.sh | ||
| setup.sh | ||
| stylua.toml | ||
Dotfiles
Personal machine configuration managed with chezmoi.
Supports macOS, Arch Linux (including Arch Linux ARM and Omarchy), Ubuntu/Debian, and headless Linux servers. Derivatives are detected via ID_LIKE, so Manjaro, EndeavourOS, Linux Mint, Pop!_OS and Raspberry Pi OS route to their parent distro.
Fresh machine setup
1. Retrieve the age encryption keys
All secrets in this repo are encrypted with age using an SSH key pair stored in Bitwarden.
Before anything else, restore both keys from Bitwarden:
mkdir -p ~/.ssh
# Private key — Bitwarden item "id_chezmoi" (file attachment or secure note)
nvim ~/.ssh/id_chezmoi # paste private key, save
chmod 600 ~/.ssh/id_chezmoi
# Public key — Bitwarden item "id_chezmoi.pub" (file attachment or secure note)
nvim ~/.ssh/id_chezmoi.pub # paste public key, save
Why both? chezmoi uses the private key (
id_chezmoi) to decrypt.agefiles duringapply, and the public key (id_chezmoi.pub) as the recipient when encrypting new secrets withchezmoi add --encrypt.
Omittingid_chezmoi.pubwon't break decryption but will prevent adding new secrets.
The setup scripts run common/age-key.sh before chezmoi apply — it checks for the
private key and will prompt you if it's missing.
2. Run the platform bootstrap
# clone and enter the source directory
git clone https://github.com/huantrinh1802/dotfiles.git ~/.local/share/chezmoi
cd ~/.local/share/chezmoi
# auto-detects OS and runs the right script
./setup.sh
# Arch with Tailscale (optional)
./setup-arch.sh --tailscale-network 192.168.1
Each setup script runs chezmoi init after installing packages, which prompts for
machine configuration using chezmoi's built-in interactive prompts:
Device type [personal/work] (default "personal"):
Neovim profile [full/dev/minimal] (default "full"):
Default shell [fish/zsh/bash] (default "fish"):
Install development tools [Y/n]: y
Install AWS tools [y/N]: n
Install nice-to-have packages [Y/n]: y
Install GUI packages [Y/n]: y
Set up Docker [Y/n]: y
Values are stored in ~/.config/chezmoi/chezmoi.yaml and are only prompted once —
subsequent runs of chezmoi init reuse the saved values without re-prompting.
| Script | Platform | Steps |
|---|---|---|
setup.sh |
any | detects OS, delegates below |
setup-macos.sh |
macOS | Homebrew → chezmoi init → defaults → shell → age key → apply → Docker |
setup-arch.sh |
Arch | update → shell → packages → chezmoi init → age key → apply → Docker |
setup-ubuntu.sh |
Ubuntu/Debian | apt → shell → packages → chezmoi init → age key → apply → Docker → Bitwarden |
The first step of both Linux setup scripts prompts before touching the system.
setup-arch.sh offers three choices, defaulting to the first:
| Choice | Flag | Runs |
|---|---|---|
| Full upgrade | --system-update, --yes |
pacman -Syu |
| Refresh databases only | --refresh-db |
pacman -Sy |
| Skip entirely | --skip-system-update |
nothing |
Refresh-only is not the safe middle option. pacman -Sy leaves the sync
database ahead of the installed system, so every package installed afterwards
is a partial upgrade — a state Arch does not support, and one that can break
dynamically linked binaries. Use it when a stale database is stopping package
lookups, then run a full pacman -Syu as soon as the bootstrap finishes.
setup-ubuntu.sh asks a simple yes/no, defaulting to yes, and accepts
--skip-system-update / --system-update / --yes. It governs only
apt-get update — the package-list refresh — because nothing in the Ubuntu
path runs apt-get upgrade at all.
3. Enable Bitwarden SSH agent
The SSH agent built into the Bitwarden desktop app is used for all SSH operations on graphical machines.
- Open Bitwarden → Settings → SSH Agent → enable it.
- Add your SSH keys to the Bitwarden vault.
- Re-open a shell —
SSH_AUTH_SOCKis set automatically when the agent socket is live.
On macOS the socket is
~/Library/Containers/com.bitwarden.desktop/Data/.bitwarden-ssh-agent.sock.
On Linux it is~/.bitwarden-ssh-agent.sock.
SSH_AUTH_SOCKis only set when the socket file exists, so SSH won't fail if Bitwarden isn't running.
On servers, use a dedicated SSH key instead of the desktop Bitwarden agent:
# Generate an Ed25519 key, add to ssh-agent, print upload instructions
~/.local/share/chezmoi/common/ssh-keygen.sh --comment "myserver"
The script prints direct URLs to register the public key on GitHub, GitLab, and Bitbucket, and an ssh-copy-id one-liner for authorized_keys on remote hosts.
4. Apply dotfiles
chezmoi init https://github.com/huantrinh1802/dotfiles.git # if not already cloned
chezmoi diff # preview changes
chezmoi apply
Per-machine configuration
Machine-level flags are prompted for by chezmoi init (see .chezmoi.yaml.tmpl) and written to ~/.config/chezmoi/chezmoi.yaml. They can also be edited manually:
data:
device_type: "personal" # personal | work
nvim_profile: "full" # full | dev | minimal
default_shell: "fish" # fish | zsh | bash
install_development: true # languages, databases, build tools, IDEs
install_aws: false # awscli, sam, cloudformation-cli, aws/tap
install_nice_to_have: true # media tools, specialty CLIs, extras
install_gui: true # desktop apps and graphical dotfiles
setup_docker: true # install Docker and run post-install config
device_type
Selects the personal or work sub-bucket within every package category in
packages.yaml. The common sub-bucket is always installed.
install_gui
| Value | Effect |
|---|---|
true (default) |
Desktop packages and graphical dotfiles are installed |
false |
CLI-only: gui package tier skipped, Bitwarden desktop not installed, graphical dotfiles (wezterm, autostart) excluded via .chezmoiignore |
nvim_profile
Controls which Neovim plugin groups are loaded via lua/user/lazy.lua.tmpl:
| Profile | Plugin groups loaded |
|---|---|
full |
plugins + technical_plugins + nth_plugins + firenvim_plugins |
dev |
plugins + technical_plugins |
minimal |
plugins only |
install_nice_to_have
When true, the package install hooks also install the nice_to_have /
extra tier — media tools, macOS extras, specialty CLIs, and optional
tap-based utilities.
setup_docker
When true (default), Docker runtime packages are installed and post-install configuration runs:
- macOS — installs
colima,docker,docker-compose,lazydocker; starts colima as a background launchd service - Linux — installs
docker+docker-compose; enablesdocker.servicevia systemd; adds the current user to thedockergroup
Set to false for headless servers or minimal installs that don't need Docker.
What's managed
| Area | Tools |
|---|---|
| Shell | fish, aliases, environment, env-selector |
| Editor | Neovim + lazy.nvim (profile-based) |
| Terminal | tmux, Zellij, WezTerm |
| VCS | git, jj, lazygit |
| File manager | yazi |
| Prompt & shell tools | starship, atuin, fzf, zoxide, thefuck |
| Containers | Docker (colima on macOS), lazydocker |
| aerc, neomutt, isync/mbsync, msmtp, notmuch | |
| Calendar / contacts | khal, khard, vdirsyncer |
| Secrets | Bitwarden (desktop + CLI), pass, age |
| macOS bar / WM | sketchybar, aerospace |
| AWS | CLI, credential templates |
| AI tools | opencode, aichat |
Package management
Packages are declared in .chezmoidata/packages.yaml under essential, docker, and extra tiers per platform:
packages:
darwin:
taps: [...]
brews:
essential: [...] # always installed
docker: [...] # installed when setup_docker = true
extra: [...] # installed when install_nice_to_have = true
casks:
essential: [...]
extra: [...]
linux:
arch:
pacman: { essential: [...], docker: [...], extra: [...] }
aur: { essential: [...], docker: [...], extra: [...] }
ubuntu:
apt: { essential: [...], docker: [...], extra: [...] }
brew: { essential: [...], docker: [...], extra: [...] } # Linuxbrew
The install hooks are triggered by chezmoi whenever packages.yaml changes:
run_onchange_darwin-install-packages.sh.tmpl— macOS (brew bundle)run_onchange_linux-arch-install-packages.sh.tmpl— Arch (pacman + yay)run_onchange_linux-ubuntu-install-packages.sh.tmpl— Ubuntu (apt + Linuxbrew)
Arch uses pacman + AUR only (no Linuxbrew — AUR already has bleeding-edge versions).
Ubuntu uses apt for bootstrap deps and Linuxbrew for developer tooling.
To regenerate the macOS brew list from the current Brewfile:
python3 utils_scripts/brewfile_generator.py
Encryption
Files with an .age suffix are encrypted at rest and decrypted automatically by chezmoi during apply.
dot_config/aerc/encrypted_aerc.conf.age
dot_config/mutt/encrypted_muttrc.age
dot_config/vdirsyncer/encrypted_config.age
Chezmoi is configured to use age with:
- Identity:
~/.ssh/id_chezmoi(private key — retrieve from Bitwarden) - Recipients:
~/.ssh/id_chezmoi.pub(public key — retrieve from Bitwarden)
Neither key is committed to the repo. Both must be in place before chezmoi apply.
Secrets
No plaintext credentials are committed. Templates pull secrets from:
- Bitwarden CLI (
bw get) — runtime lookups duringchezmoi apply - pass — GPG-encrypted local store (seeded via
common/pass.sh) - age — file-level encryption for sensitive config files
AWS credentials are resolved via .chezmoitemplates/aws_credentials.tmpl.
To seed the local pass store from Bitwarden after first apply:
~/.local/share/chezmoi/common/pass.sh
Repository layout
.
├── .chezmoidata/ # Template data (packages.yaml)
├── .chezmoitemplates/ # Reusable chezmoi template snippets
├── .chezmoiignore # Platform/GUI conditional excludes
├── arch/ # Arch Linux setup scripts
├── common/ # Cross-platform scripts
│ ├── startup.sh # Shell install, plugin manager, chsh
│ ├── age-key.sh # Check/restore chezmoi age key before apply
│ ├── docker-setup.sh # Post-install Docker config (systemd + group on Linux, colima on macOS)
│ ├── pass.sh # Seed pass store from Bitwarden
│ └── ssh-keygen.sh # Generate SSH key with upload instructions
├── debian/ # Ubuntu/Debian setup scripts
├── macos/ # macOS setup scripts and defaults
├── dot_claude-personal/ # Claude Code personal config (agents, commands)
├── dot_config/ # Files deployed to ~/.config/
├── dot_*/ # Other files deployed to ~/
├── run_onchange_*.tmpl # Chezmoi install hooks (run on data change)
├── setup.sh # Master bootstrap (auto-detects OS)
├── setup-{macos,arch,ubuntu}.sh
└── utils_scripts/ # Repo maintenance utilities
Notes
- Commits should be atomic and grouped by tool or concern.
- Do not commit plaintext secrets, API tokens, or private keys.
- macOS-only configs (
sketchybar,aerospace,yabai,skhd) are excluded on Linux via.chezmoiignore. - Graphical dotfiles (
wezterm,autostart) are excluded wheninstall_gui = false. - Encrypted files are committed as
.ageciphertext only.