Tutorial: Deploy to a Fleet¶
Roll out .dotfiles across multiple workstations and verify they're in sync.
Scenario¶
You have three hosts:
- macbook-t2 — your MacBook (current host, already installed)
- surface-pro — your Linux laptop (new install)
- geekom-a9 — your desktop NUC (new install)
Goal: all three running identical .dotfiles with per-host customization, with auditable cross-host attestation.
Step 1: Per-Host Install¶
On each new host:
bash -c "$(curl -fsSL https://raw.githubusercontent.com/sebastienrousseau/dotfiles/main/install.sh)"
During chezmoi init, answer with the host's preset:
- Surface Pro →
machine = "surface-pro" - Geekom A9 →
machine = "geekom-a9"
Step 2: Exchange Signing Keys¶
Each host has its own SSH ED25519 key. To verify commits and attestations across the fleet, publish each host's public key to every other host.
On each host, copy the public key:
Collect all three keys. Append them to ~/.ssh/allowed_signers on every host:
user@macbook-t2 ssh-ed25519 AAAAC3N...jkl5
user@surface-pro ssh-ed25519 AAAAC3N...abc7
user@geekom-a9 ssh-ed25519 AAAAC3N...def9
Optionally commit allowed_signers as a template so all hosts receive updates via dot update:
# On macbook-t2
chezmoi add ~/.ssh/allowed_signers
git add -A && git commit -sS -m "feat(ssh): add fleet signing keys"
git push
Now every other host gets the updated list on dot update.
Step 3: Share the Age Key (for Shared Secrets)¶
If you want shared secrets decrypted on all three hosts, each host must have an Age key recipient in .sops.yaml.
Option A: Share one key across all hosts (simpler, less granular). Option B: Each host has its own key; all three are listed as recipients (better operational security).
For Option B:
# On macbook-t2 (already set up)
grep "public key:" ~/.config/age/keys.txt
# On surface-pro
age-keygen -o ~/.config/age/keys.txt
grep "public key:" ~/.config/age/keys.txt
# On geekom-a9
age-keygen -o ~/.config/age/keys.txt
grep "public key:" ~/.config/age/keys.txt
Add all three public keys to .sops.yaml:
creation_rules:
- path_regex: \.sops\.yaml$
age: >-
age1aaa...111, # macbook-t2
age1bbb...222, # surface-pro
age1ccc...333 # geekom-a9
Re-encrypt existing secrets for the new recipients:
# On macbook-t2
find . -name '*.sops.yaml' -exec sops updatekeys {} \;
git add .sops.yaml dot_config/*.sops.yaml
git commit -sS -m "chore(secrets): add fleet recipients"
git push
Other hosts on dot update decrypt with their own private key automatically.
Step 4: Verify Fleet Alignment¶
From any host:
Expected output:
Fleet Attestation — v0.2.501
Host Git SHA Policy Tools OK Drift Verified
-------------- ---------- --------- ---------- ------- --------
macbook-t2 abc123d 0x7f2a… ✓ 0 ✓
surface-pro abc123d 0x7f2a… ✓ 0 ✓
geekom-a9 abc123d 0x7f2a… ✓ 0 ✓
-------------- ---------- --------- ---------- ------- --------
FLEET STATUS: 3/3 aligned
If any row shows drift or missing verification:
# Check which host is out of sync
dot fleet diff
# SSH into the affected host and fix
ssh surface-pro
dot doctor && dot heal
dot update
Step 5: Per-Host Customization¶
Each host has its own ~/.config/chezmoi/chezmoi.toml — use it for things that shouldn't propagate:
# surface-pro's chezmoi.toml
[data]
machine = "surface-pro"
theme = "monterey-dark" # per-host default
default_shell = "fish"
terminal_font_size = 11 # smaller for HiDPI
# geekom-a9's chezmoi.toml
[data]
machine = "geekom-a9"
theme = "valley-dark" # different default
default_shell = "zsh"
terminal_font_size = 14 # larger for external 1440p
Both hosts render from the same templates, producing correct platform-appropriate output.
Step 6: Fleet-Wide Updates¶
When you push a commit to main, each host pulls independently:
dot update does:
git pull --rebase --autostashin~/.dotfileschezmoi applywith progress outputdot healif apply detected drift- Reports changes applied
For simultaneous update across the fleet (requires SSH):
Step 7: Secrets Rotation¶
When a secret is compromised (or as part of routine rotation):
# 1. Edit the secret on any host
sops dot_config/credentials.sops.yaml
# Change the compromised value, save
# 2. Commit and push
git commit -sS -am "chore(secrets): rotate stripe key"
git push
# 3. All fleet hosts pick up the new value on next update
# On each host:
dot update
Since hosts all have the Age private key for themselves, decryption is automatic.
Step 8: Decommission a Host¶
When retiring a host:
# 1. Remove the host's public key from allowed_signers
# Edit dot_ssh/allowed_signers.tmpl (on any host)
# 2. Remove the host's Age public key from .sops.yaml
# 3. Re-encrypt all SOPS files (remove the decommissioned recipient)
find . -name '*.sops.yaml' -exec sops updatekeys {} \;
# 4. Commit
git commit -sS -am "chore(fleet): decommission geekom-a9"
git push
# 5. On the decommissioned host: wipe the dotfiles state
rm -rf ~/.dotfiles ~/.local/bin/dot ~/.config/chezmoi ~/.config/age
Troubleshooting¶
"Fleet host unreachable"¶
dot fleet couldn't SSH to a host. Check:
Drift on One Host¶
Run dot heal on that host:
If drift persists:
Policy Hash Mismatch¶
One host has an older policy than the others. Usually means that host missed an update:
Attestation Signature Failed¶
The host's SSH key isn't in ~/.ssh/allowed_signers on the verifying host. Fix by redistributing allowed_signers.
Summary¶
A three-host fleet:
- Installs from one repository
- Has independent signing keys (each host verifies commits from others)
- Can share secrets across specific hosts via SOPS recipients
- Is auditable via
dot fleet attest - Updates independently with
dot update(or in parallel withdot fleet sync)