
September 12, 2026
10 min read
By Kokil Thapa | Last reviewed: September 2026
You rebuild a laptop or SSH into a fresh Ubuntu box far more often than frameworks change. Without a practical dotfiles setup, you lose aliases, Git defaults, SSH host blocks, and editor keymaps. You re-type the same fixes for the tenth time. I maintain several production servers and local dev machines with the same Git-based dotfiles repo. This guide shows the structure I actually use—not a perfect abstraction, but one that survives real client work, Ubuntu server provisioning, and late-night deploys.
What is a practical dotfiles setup and why does it matter?
Dotfiles are hidden config files in your home directory: .bashrc, .gitconfig, .ssh/config, and editor settings. A practical dotfiles setup treats them like application code. You commit, review, and replay them.
The payoff is speed and consistency. Your ll alias, Git identity, and SSH jump-host rules appear on a new VPS in five minutes. Your local PHP 8.4 path and Deployer shortcuts match the machine you used yesterday. Small teams benefit too. One shared baseline reduces "works on my laptop" friction during Linux server administration work.
What dotfiles are not: a second job. Skip exotic frameworks until plain Git plus Stow feels boring. Fancy tools help when you manage ten machines with different OS versions. Most freelancers and small agencies need a repo, a symlink tool, and a bootstrap script.
Start with pain you feel weekly. For me that is Git ergonomics, SSH host entries, and shell paths for PHP and Composer. Editor config comes next. Window manager themes can wait.
How do you organize dotfiles in a Git repository?
Keep the repo at ~/.dotfiles or ~/dotfiles. Use a flat top level with one folder per config bundle. Each bundle mirrors the path Stow will create under your home directory.
A layout that scales on Ubuntu 22/24 and macOS looks like this:
dotfiles/
├── README.md
├── install.sh
├── bash/
│ └── .bashrc
│ └── .bash_profile
├── git/
│ └── .gitconfig
│ └── .gitignore_global
├── ssh/
│ └── .ssh/
│ └── config
├── nvim/
│ └── .config/nvim/init.lua
└── deployer/
└── .deployer/ The bash/ folder holds files that Stow links into ~/. The nvim/ folder holds .config/nvim/ because Neovim expects XDG paths. Match real paths, not clever nesting.
Initialize the repository
- Clone or create the repo:
git init ~/.dotfiles. - Add a root
.gitignorefor OS junk:.DS_Store,*~,.cache/. - Commit one bundle at a time so rollbacks stay obvious.
- Push to a private remote on GitHub or GitLab. Treat it like credentials-adjacent infrastructure.
Document assumptions in README.md: target OS, shell, and packages the bootstrap script installs. Future you—or a contractor on a Laravel Livewire booking project—should not guess.
Tag stable snapshots after major changes: git tag v2026.09. Roll back a bad .bashrc edit without archaeology.
How do you symlink dotfiles safely with GNU Stow?
Manual symlinks rot fast. You forget which file is canonical. GNU Stow treats each bundle as a mini tree and links files into your home directory. It is package-manager logic applied to dotfiles.
Install Stow on Ubuntu:
sudo apt update
sudo apt install -y stow From inside the repo, stow one bundle:
cd ~/.dotfiles
stow --verbose bash
stow --verbose git
stow --verbose ssh
stow --verbose nvim Stow creates symlinks like ~/.bashrc -> ../.dotfiles/bash/.bashrc. Edit the repo file; the live config updates instantly. Unstow before removing a bundle:
stow --delete bash Never stow over files you have not inspected. Stow refuses some conflicts; others silently nest. Before first stow on a new machine, back up existing configs:
mkdir -p ~/dotfiles-backup-$(date +%F)
cp -a ~/.bashrc ~/.gitconfig ~/dotfiles-backup-$(date +%F)/ 2>/dev/null || true Alternative tools exist—chezmoi, yadm, bare-repo methods—but Stow stays readable for the next developer. That matters on shared Ansible-driven server setups where dotfiles complement playbooks rather than replace them.
Which dotfiles should you version first on Ubuntu?
Prioritize files that save time on every session. Skip anything that embeds secrets or machine-specific hardware paths until you have a split strategy.
| File / bundle | Track in Git? | Notes |
|---|---|---|
.bashrc / .zshrc | Yes | Aliases, PATH, history settings; keep portable |
.gitconfig | Yes | Name, email, aliases; use conditional includes per directory |
.ssh/config | Yes (no keys) | Host blocks, IdentityFile paths, ProxyJump chains |
.ssh/id_* | Never | Generate per machine; add pubkey to Git hosting manually |
.env / API tokens | Never | Use .env.local outside the repo or a secret manager |
.config/nvim/ | Yes | See Neovim setup for DevOps engineers for a lean baseline |
.deployer/ | Partial | Shared recipes yes; host passwords never |
.mysql_history | Never | May contain query data; exclude via global gitignore |
Example portable .bashrc excerpt
# ~/.dotfiles/bash/.bashrc
export EDITOR="nvim"
export PATH="$HOME/.config/composer/vendor/bin:$PATH"
alias ll='ls -lah'
alias gs='git status -sb'
alias dep='vendor/bin/dep'
# PHP version switch — override in ~/.bashrc.local
export PHP_BIN="${PHP_BIN:-php8.4}" Load local overrides from an untracked file:
# at end of .bashrc
[[ -f ~/.bashrc.local ]] && source ~/.bashrc.local That pattern keeps the repo portable. Your laptop can set PHP_BIN=php8.5 while a server stays on 8.4.
Example .gitconfig with conditional identity
[user]
name = Kokil Thapa
email = you@personal.example
[includeIf "gitdir:~/work/"]
path = ~/.gitconfig-work
[alias]
co = checkout
br = branch
lg = log --oneline --graph --decorate -20
[init]
defaultBranch = main Store .gitconfig-work in a separate stow bundle if client email must differ. That avoids wrong-commit-author mistakes on law-firm repos and eCommerce projects alike.
SSH config without private keys
# ~/.dotfiles/ssh/.ssh/config
Host gitlab-prod
HostName gitlab.example.com
User git
IdentityFile ~/.ssh/id_ed25519_gitlab
IdentitiesOnly yes
Host client-vps
HostName 203.0.113.10
User deploy
ForwardAgent no Pair this with a hardened Ubuntu SSH server setup on the remote side. Keys stay generated on each host. Only the layout is shared.
How do you bootstrap a new machine from dotfiles?
The bootstrap script is the product. Cloning the repo is step one. install.sh should install packages, run Stow, and verify symlinks—idempotently.
Sample install.sh skeleton:
#!/usr/bin/env bash
set -euo pipefail
DOTFILES_DIR="$(cd "$(dirname "$0")" && pwd)"
BUNDLES=(bash git ssh nvim)
if command -v apt-get &>/dev/null; then
sudo apt-get update
sudo apt-get install -y stow git curl
fi
for bundle in "${BUNDLES[@]}"; do
stow --dir="$DOTFILES_DIR" --target="$HOME" --restow "$bundle"
done
echo "Dotfiles linked. Open a new shell or: source ~/.bashrc" Run with bash install.sh the first time. chmod +x once committed. Add optional flags: --minimal for servers without Neovim, --work to include the work Git bundle.
On production VPS hosts I keep bootstrap minimal. Shell, Git, SSH, and Deployer aliases only—no editor plugins. Full dev bundles stay on laptops. That mirrors how I split CI runners versus local builds in YAML pipeline workflows.
For teams, combine dotfiles with Ansible when you need package versions pinned system-wide. Dotfiles own user-level prefs; playbooks own php8.4-fpm packages and firewall rules. The overlap is intentional, not redundant.
How do you keep secrets out of your dotfiles repo?
One leaked API key in a public dotfiles fork becomes a billing incident. Private repos help. They are not sufficient. Assume the repo may become public someday.
- Never commit private keys,
.envfiles, or password manager exports. - Use
*.localfiles loaded from untracked paths. - Add a repo
.gitignoreplus a global gitignore for.ssh/id_*and.env*. - Run
git log -pbefore making a repo public; history keeps secrets forever. - Use a password generator for service accounts; store vault copies outside Git.
For Deployer host definitions, I commit structure and usernames. Passwords live in .env on the deploy runner or in GitLab CI variables. Same pattern as Laravel apps: tracked config templates, untracked secrets. Reference the official gitignore documentation for pattern syntax.
If you already leaked a key, rotate it immediately. Removing the file from HEAD is not enough. Use git filter-repo or BFG only when you understand rewrite impact on clones.
Encrypt sensitive dotfiles only when you must sync them across machines. Tools like SOPS or age work. They add ops overhead. For most PHP/Laravel freelancers, untracked .local files plus CI secrets is simpler and safer.
Key Takeaways
- Store dotfiles in a private Git repo with one Stow bundle per config domain: bash, git, ssh, editor.
- Use GNU Stow instead of hand-maintained symlinks; always back up existing files before first stow.
- Keep secrets and private keys out of Git; load
*.localoverrides from untracked paths. - Write an idempotent
install.shthat installs Stow, restows bundles, and documents OS assumptions. - Split minimal server bundles from full laptop bundles to reduce attack surface on production VPS hosts.
- Tag repo snapshots so you can roll back a bad shell change without rebuilding from memory.
People Also Ask
Should dotfiles be a public or private Git repository?
Use a private repository by default. Shell and SSH configs reveal hostnames, internal URLs, and tooling choices. Public dotfiles are fine for generic editor themes with zero infrastructure hints. If you publish, audit history with git log -p first.
GNU Stow vs bare Git repo for dotfiles—which is simpler?
Stow is simpler for mixed paths like .config/nvim alongside .bashrc. Bare-repo methods keep everything in ~/.cfg with alias tricks. They work but confuse newcomers. Stow reads clearly in a README and pairs well with Ansible on cloud VPS provisioning.
How often should you update dotfiles?
Commit when a change survives one week of daily use. Batch tiny tweaks monthly. Pull before bootstrap on a new machine. After OS upgrades, run stow --restow on each bundle to fix broken symlinks.
Do dotfiles replace infrastructure-as-code tools?
No. Dotfiles manage user-level preferences on a host. Terraform, Ansible, and cloud-init manage servers, networks, and packages. They complement each other. See infrastructure as code with Terraform for the server layer; dotfiles finish the developer experience on top.
Ship a repeatable environment you trust
A practical dotfiles setup pays off the third time you provision a machine—not the first afternoon you build it. Start with shell, Git, and SSH. Add editor config when those feel stable. Keep secrets local, bootstrap with Stow, and document what your script expects.
If you want help standardizing developer machines alongside production servers—Deployer pipelines, GitLab CI, backups, and monitoring—I handle that end to end. See support and maintenance services, review Notary Kathmandu and sister sites on shared deploy infrastructure, or contact us to talk through your team setup.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

