I've had the same dotfiles repository since 2019. Seven years of shell configs, vim tweaks, and git aliases that I copy to every machine I touch. If you're not managing your dotfiles in git, you're doing it wrong, and I'll die on this hill.
The idea is simple: put every config file you care about in one repo, symlink them into place, and never manually configure a machine again. Here's how I do it.

Why Dotfiles Matter
Every time I SSH into a fresh server and don't have my aliases, I feel physically uncomfortable. Where's my `gs` for `git status`? Where's my `lla` for `ls -la`? It's like walking into someone else's kitchen and not knowing where the knives are.
Dotfiles are your digital muscle memory. They're the reason you type `dc` instead of `docker-compose` and `gco` instead of `git checkout`. Without them, every new machine is a productivity tax.

The Repository Structure
Here's what my dotfiles repo looks like.
dotfiles/
├── bash/
│ ├── .bashrc
│ ├── .bash_aliases
│ └── .bash_prompt
├── git/
│ ├── .gitconfig
│ └── .gitignore_global
├── vim/
│ └── .vimrc
├── tmux/
│ └── .tmux.conf
├── ssh/
│ └── config
├── install.sh
└── README.mdEach directory holds one tool's config. One tool, one directory. No interleaving, no guessing which file does what.
The Install Script
This is the only file that matters. It creates symlinks from the repo to where the system expects configs. Here's mine.
#!/bin/bash
DOTFILES_DIR="$HOME/dotfiles"
BACKUP_DIR="$HOME/dotfiles_backup"
# Backup existing files
mkdir -p "$BACKUP_DIR"
# Files to symlink
files=(
"bash/.bashrc"
"bash/.bash_aliases"
"bash/.bash_prompt"
"git/.gitconfig"
"git/.gitignore_global"
"vim/.vimrc"
"tmux/.tmux.conf"
"ssh/config"
)
for file in "\${files[@]}"; do
target="$HOME/.$(basename "$file")"
# Special case: ssh config
if [[ "$file" == "ssh/config" ]]; then
target="$HOME/.ssh/config"
mkdir -p "$HOME/.ssh"
fi
if [ -f "$target" ] || [ -L "$target" ]; then
echo "Backing up $target"
mv "$target" "$BACKUP_DIR/"
fi
echo "Linking $file -> $target"
ln -s "$DOTFILES_DIR/$file" "$target"
done
echo "Done. Restart your shell or source ~/.bashrc"
echo "Backup of replaced files: $BACKUP_DIR"Run it once on a new machine and everything lands where it should. No manual copying, no `scp` commands, no "wait which file was that again?" moments.

The Git Config That Saves Time
My `.gitconfig` has aliases I can't live without.
[user]
name = Davide Andreazzini
email = davide@davideandreazzini.co.uk
[alias]
s = status
co = checkout
br = branch
cm = commit -m
ca = commit -am
l = log --oneline -20
lg = log --oneline --graph --all
d = diff
ds = diff --staged
unstage = reset HEAD --
[core]
editor = vim
excludesfile = ~/.gitignore_global
[push]
autoSetupRemote = true
[pull]
rebase = true
[init]
defaultBranch = mainThose six two-letter aliases alone save me hundreds of keystrokes a day. The `unstage` alias is the one I always forget other people don't have.
Bash Aliases Worth Stealing
Here are the aliases I've carried across every machine for years.
# Navigation
alias ..='cd ..'
alias ...='cd ../..'
alias ll='ls -alF'
alias la='ls -A'
alias l='ls -CF'
# Git shortcuts ( redundant with .gitconfig but bash is faster )
alias gs='git status'
alias gl='git log --oneline -10'
alias gd='git diff'
alias gc='git commit -m'
alias gp='git push'
# Docker ( because docker-compose is too many characters )
alias dc='docker-compose'
alias dps='docker ps'
alias dimg='docker images'
alias dex='docker exec -it'
# Safety nets
alias rm='rm -i'
alias cp='cp -i'
alias mv='mv -i'
alias grep='grep --color=auto'
# Quick edits
alias vb='vim ~/.bashrc'
alias vv='vim ~/.vimrc'
alias vt='vim ~/.tmux.conf'
alias vg='vim ~/.gitconfig'
# Reload
alias sb='source ~/.bashrc && echo "bashrc reloaded"'The safety net aliases ( `rm -i`, `cp -i`, `mv -i` ) have saved me more times than I'll admit. The `sb` alias is the one I type most after editing my bashrc.
The .bash_prompt That Actually Helps
A good prompt shows you what you need without clutter. Mine shows git branch, dirty state, and the exit code of the last command.
# Git branch in prompt
parse_git_branch() {
git branch 2>/dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/ [\1]/'
}
# Dirty state indicator
parse_git_dirty() {
if [ -n "$(git status --porcelain 2>/dev/null)" ]; then
echo "*"
fi
}
# Color definitions
RED='\[\033[0;31m\]'
GREEN='\[\033[0;32m\]'
YELLOW='\[\033[0;33m\]'
BLUE='\[\033[0;34m\]'
RESET='\[\033[0m\]'
# Exit code display
exit_code() {
local ec=$?
if [ $ec -ne 0 ]; then
echo "${RED}[$ec]${RESET} "
fi
}
PS1="\$(exit_code)\u@\h:\${BLUE}\w\${RESET}\${YELLOW}\$(parse_git_branch)\$(parse_git_dirty)\${RESET}\$ " The red exit code is the most useful part. When something fails silently, the prompt tells you immediately.
Keeping It All In Sync
The repo lives on GitHub. When I change something on my laptop, I push. When I set up a new server, I clone and run `install.sh`. That's the entire workflow.
# On a new machine
git clone git@github.com:davideandreazzini/dotfiles.git ~/dotfiles
cd ~/dotfiles
chmod +x install.sh
./install.shIf I modify a config on any machine, I commit and push from that machine. On the others, `git pull` in `~/dotfiles` and the symlinks pick up the changes instantly. No syncing tools, no Ansible, no complicated orchestration. Just git.
One thing I learned the hard way: never put secrets in your dotfiles repo. API keys, SSH private keys, tokens — those go in a separate private repo or in environment-specific files that the install script skips.
Conclusion
Dotfiles are one of those things where the initial investment pays off forever. A weekend setting up the repo, and every new machine after that takes sixty seconds. If you don't have a dotfiles repo yet, start one today.
Mine are at github.com/davideandreazzini/dotfiles. Fork it, steal the aliases, make it yours.