Setup dual screen para gerenciamento de dotfiles com GNU Stow e Ansible no Linux

Automatizei Meus Dotfiles com GNU Stow e Ansible — E Agora Formato PC Sem Medo

Na última vez que formatei o notebook, jurei que ia ser a última vez que eu perdia um dia inteiro reconfigurando tudo. Vim do Ubuntu pro Fedora — motives à parte — e quando abri o terminal pensei: “qual era minha configuração do Neovim mesmo?”

Não sabia. O Git estava sem chave SSH, o alias gs pra git status não existia, meu Tmux tava pelado, e o Zsh nem tinha sido instalado. Perdi 6 horas só pra deixar o ambiente minimamente usável. Seis horas que eu poderia ter escrito código, estudado, ou simplesmente assistido YouTube sem culpa.

Foi nesse dia que eu decidi: nunca mais. Comecei a pesquisar como as pessoas que levam a sério gerenciam seus dotfiles, e descobri um combo que mudou minha relação com formatar PC: GNU Stow + Ansible.

Neste artigo, vou te mostrar como automatizar seus dotfiles com GNU Stow e Ansible — do zero até um repositório Git que replica seu ambiente inteiro em qualquer máquina Linux em menos de 15 minutos. Sem mágica, sem ferramentas obscuras. Apenas organização e um pouquinho de script.

Desenvolvedor configurando dotfiles automatizados no terminal Linux
O momento exato em que você percebe que configurar tudo manualmente não é inteligente — é teimosia.

O Que São Dotfiles (E Por Que Você Deveria Se Importar)

Dotfiles são aqueles arquivos de configuração escondidos na sua home que começam com ponto: .bashrc, .vimrc, .gitconfig, .tmux.conf. Eles guardam todas as suas personalizações — aliases, temas, keybindings, variáveis de ambiente.

O problema? Eles estão espalhados por toda a árvore de diretórios do sistema. Seu .bashrc tá em ~/, seu init.vim tá em ~/.config/nvim/, seu .ssh/config tá em ~/.ssh/. Não é só copiar a home e pronto.

E tem mais: se você mexe em duas máquinas (notebook + servidor, ou trabalho + pessoal), manter tudo sincronizado manualmente é um parto que ninguém merece. Já falei aqui sobre como shell scripts me economizaram 30 horas por semana — gerenciar dotfiles é a extensão natural disso.

O Problema dos Symlinks Manuais

A primeira tentativa de todo mundo é criar symlinks manualmente:

# A abordagem ingênua
ln -s ~/dotfiles/.bashrc ~/.bashrc
ln -s ~/dotfiles/.vimrc ~/.vimrc
ln -s ~/dotfiles/.config/nvim/init.vim ~/.config/nvim/init.vim
ln -s ~/dotfiles/.tmux.conf ~/.tmux.conf
# ... e por aí vai

Funciona? Sim. Escala? Nem um pouco. Problemas reais que eu enfrentei:

  • Esqueci de criar o diretório pai antes do symlink → erro silencioso, e passei dias sem perceber que o Tmux não estava lendo meu config
  • Symlinks quebrados ao mudar o caminho do repositório → .gitconfig apontando pro vazio
  • Conflitos ao rodar o mesmo script em máquina que já tinha configuração diferente → sobrescrevi sem querer
  • Nenhuma idempotência — rodar duas vezes criava links redundantes ou falhava

Eu precisava de algo que gerenciasse os symlinks de forma limpa, previsível e reversível. Entrei num buraco de pesquisa e saí com o GNU Stow.

GNU Stow: O Gerenciador de Symlinks Que Você Não Conhecia

O GNU Stow é um gerenciador de symlinks que existe desde 1993. Sim, 1993. Ele resolve exatamente o problema de “tenho arquivos organizados numa pasta e quero que apareçam espalhados em outra”.

A ideia é genial na simplicidade:

  1. Você organiza seus dotfiles em pacotes dentro de um diretório (~/dotfiles)
  2. Cada pacote espelha a estrutura de diretórios relativo à home
  3. Você roda stow <pacote> e ele cria os symlinks automaticamente

Estrutura de Diretórios

~/dotfiles/
├── bash/
│   └── .bashrc
│   └── .bash_aliases
├── git/
│   └── .gitconfig
│   └── .gitignore_global
├── nvim/
│   └── .config/
│       └── nvim/
│           └── init.lua
│           └── lua/
│               └── plugins.lua
├── tmux/
│   └── .tmux.conf
└── zsh/
    └── .zshrc
    └── .p10k.zsh

Quando você roda stow bash a partir de ~/dotfiles, o Stow cria:

~/.bashrc → ~/dotfiles/bash/.bashrc
~/.bash_aliases → ~/dotfiles/bash/.bash_aliases

Quando precisa remover? stow -D bash. Pronto, symlinks removidos, arquivos originais intactos.

Instalação e Configuração Inicial

# Ubuntu/Debian
sudo apt install stow

# Fedora
sudo dnf install stow

# macOS (via Homebrew)
brew install stow

# Arch (porque alguém vai perguntar)
sudo pacman -S stow

O fluxo completo pra adicionar um dotfile existente:

# 1. Criar a estrutura do pacote
mkdir -p ~/dotfiles/git

# 2. Mover o arquivo original
mv ~/.gitconfig ~/dotfiles/git/.gitconfig

# 3. Criar o symlink
cd ~/dotfiles
stow git

# 4. Verificar
ls -la ~/.gitconfig
# lrwxrwxrwx 1 user user 22 Apr 17 .gitconfig -> dotfiles/git/.gitconfig

Um comando. Um symlink limpo. Reversível. Idempotente. É o tipo de coisa que faz você se perguntar por que não fazia isso antes.

Quando o Stow Não Chega Sozinho: Entra o Ansible

O Stow resolve a parte de symlinks dos arquivos de configuração. Mas e o resto?

  • Quem instala os pacotes que seus dotfiles assumem que existem? (Neovim, Tmux, Zsh, ripgrep, fd, bat…)
  • Quem configura os repositórios extras? (PPA do Neovim, rpmfusion, Homebrew…)
  • Quem instala plugins do Vim/Tmux automaticamente?
  • Quem garante que o SSH key existe e está no agent?

É aqui que o Ansible entra como o orquestrador. Enquanto o Stow cuida dos dotfiles, o Ansible cuida de tudo ao redor.

Código colorido no terminal Linux mostrando configuração de dotfiles e Ansible playbook
O momento em que você percebe que playbooks são só YAML com superpoderes.

Montando o Playbook Ansible

A estrutura completa do meu repositório de dotfiles ficou assim:

~/dotfiles/
├── ansible/
│   ├── playbook.yml
│   ├── inventory.ini
│   └── tasks/
│       ├── packages.yml
│       ├── stow.yml
│       ├── ssh.yml
│       └── vim-plug.yml
├── bash/
│   └── .bashrc
├── git/
│   └── .gitconfig
│   └── .gitignore_global
├── nvim/
│   └── .config/nvim/
├── tmux/
│   └── .tmux.conf
├── zsh/
│   └── .zshrc
├── install.sh    # ponto de entrada
└── README.md

O Playbook Principal

---
# ansible/playbook.yml
- name: Configurar ambiente de desenvolvimento
  hosts: localhost
  connection: local
  vars:
    stow_packages:
      - bash
      - git
      - nvim
      - tmux
      - zsh
    fedora_packages:
      - neovim
      - tmux
      - zsh
      - git
      - ripgrep
      - fd-find
      - bat
      - curl
      - stow
      - util-linux-user  # para chsh
    ubuntu_packages:
      - neovim
      - tmux
      - zsh
      - git
      - ripgrep
      - fd-find
      - bat
      - curl
      - stow

  tasks:
    - name: Detectar distribuição
      ansible.builtin.setup:
        filter: ansible_distribution

    - name: Instalar pacotes (Fedora)
      become: true
      ansible.builtin.dnf:
        name: "{{ fedora_packages }}"
        state: present
      when: ansible_distribution == "Fedora"

    - name: Instalar pacotes (Ubuntu/Debian)
      become: true
      ansible.builtin.apt:
        name: "{{ ubuntu_packages }}"
        state: present
        update_cache: true
      when: ansible_distribution in ["Ubuntu", "Debian"]

    - name: Stow dotfiles
      ansible.builtin.command:
        cmd: "stow -t {{ ansible_env.HOME }} {{ item }}"
        chdir: "{{ playbook_dir }}/.."
      loop: "{{ stow_packages }}"
      changed_when: false

    - name: Instalar Vim-Plug (Neovim)
      ansible.builtin.shell: |
        curl -fLo "{{ ansible_env.HOME }}/.local/share/nvim/site/autoload/plug.vim" --create-dirs \
          https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim
      args:
        creates: "{{ ansible_env.HOME }}/.local/share/nvim/site/autoload/plug.vim"

    - name: Instalar plugins do Neovim
      ansible.builtin.command:
        cmd: nvim --headless +PlugInstall +qall
      changed_when: false
      ignore_errors: true

    - name: Instalar TPM (Tmux Plugin Manager)
      ansible.builtin.git:
        repo: "https://github.com/tmux-plugins/tpm"
        dest: "{{ ansible_env.HOME }}/.tmux/plugins/tpm"
        depth: 1

    - name: Definir Zsh como shell padrão
      become: true
      ansible.builtin.user:
        name: "{{ ansible_user_id }}"
        shell: /bin/zsh
      when: ansible_user_id != "root"

    - name: Gerar chave SSH se não existir
      ansible.builtin.command:
        cmd: 'ssh-keygen -t ed25519 -C "{{ ssh_email }}" -f "{{ ansible_env.HOME }}/.ssh/id_ed25519" -N ""'
      args:
        creates: "{{ ansible_env.HOME }}/.ssh/id_ed25519"

O Script de Entrada

#!/bin/bash
# install.sh — Clone e execute
set -euo pipefail

DOTFILES_DIR="$HOME/dotfiles"

if [ ! -d "$DOTFILES_DIR" ]; then
  echo "❌ Clone o repositório primeiro:"
  echo "   git clone git@github.com:SEU_USER/dotfiles.git $DOTFILES_DIR"
  exit 1
fi

# Instalar Ansible se não existir
if ! command -v ansible &>/dev/null; then
  echo "📦 Instalando Ansible..."
  if command -v dnf &>/dev/null; then
    sudo dnf install -y ansible
  elif command -v apt &>/dev/null; then
    sudo apt update && sudo apt install -y ansible
  else
    pip3 install --user ansible
  fi
fi

# Rodar o playbook
echo "🚀 Configurando ambiente..."
cd "$DOTFILES_DIR"
ansible-playbook ansible/playbook.yml \
  --extra-vars "ssh_email=${1:-$(git config user.email)}" \
  -v

echo ""
echo "✅ Ambiente configurado! Reinicie o terminal ou execute:"
echo "   source ~/.zshrc"

Gerenciando Configurações Específicas Por Máquina

Um problema real: seu notebook e seu servidor não precisam das mesmas coisas. Você quer Oh My Zsh com Powerlevel10k no notebook, mas no servidor quer só o básico funcional.

A solução que funcionou pra mim é usar includes condicionais no .gitconfig e variáveis no Ansible:

# ~/.gitconfig (base)
[user]
  name = Seu Nome
[includeIf "gitdir:~/work/"]
  path = ~/.gitconfig-work
[includeIf "gitdir:~/pessoal/"]
  path = ~/.gitconfig-pessoal
[core]
  editor = nvim
  autocrlf = input
[alias]
  s = status
  l = log --oneline --graph --decorate -15
  co = checkout
  br = branch

E no Ansible, você pode usar group_vars pra definir o que vai em cada tipo de máquina:

~/dotfiles/
├── ansible/
│   ├── group_vars/
│   │   ├── desktop.yml   # GUI, fontes, tema, barra de status
│   │   └── server.yml    # mínimo indispensável
│   ├── host_vars/
│   │   └── vps-production.yml
│   └── inventory.ini
# ansible/inventory.ini
[desktop]
localhost

[server]
vps-production ansible_host=seu.server.com

[desktop:vars]
stow_packages=bash,git,nvim,tmux,zsh,alacritty

[server:vars]
stow_packages=bash,git,nvim,tmux

Assim, ao rodar ansible-playbook playbook.yml --limit server, só os dotfiles essenciais são implantados. Sem bloat.

O Repositório Git: Versão, Backup e Portabilidade

O repositório de dotfiles no GitHub é sua seguro contra amnésia digital. Cada mudança é um commit. Cada máquina é um clone. E se algo quebrar, git diff te diz exatamente o que mudou.

Mas tem um detalhe crucial: o que NÃO versionar.

# .gitignore do repositório de dotfiles
# Segredos — NUNCA comitar
.env
.netrc
.ssh/id_*
.ssh/known_hosts
.config/gh/hosts.yml

# Machine-specific
.local/
.cache/

# Regeneráveis
.tmux/plugins/
.config/nvim/plugged/
.config/nvim/autoload/

O Ansible cuida de regenerar o que for necessário (plugins, autoloads). O Git guarda a intenção; o Ansible executa.

Se quiser ir além, o mesmo princípio de versionamento com Git que uso pro Obsidian se aplica aqui: branch por máquina, merge quando quiser sincronizar, revert quando algo quebrar.

Box Perrengue: O Dia Que Eu Commitei Minha SSH Key

🚨 Log de Erros #47 — O perrengue que me ensinou .gitignore

Primeiro commit do repositório de dotfiles. Empolgado, mandei git add . e git push sem revisar. Commitei tudo. Inclusive a pasta .ssh/ inteira com minha chave privada.

Não percebi até receber um alerta do GitHub: “Pushed SSH private key detected”. Eles literalmente me notificaram que eu tinha vazado minha própria chave. Cortesia da GitHub, que faz esse scan automático.

Lições aprendidas:

1. git add . é um atalho pro desastre. Sempre use git add -p pra revisar.
2. SSH keys no .gitignore antes de qualquer commit.
3. Se vazar, revogue a chave imediatamente. Eu demorei 2 horas pra perceber e já tinha sido clonada por um scanner antes de eu deletar. Felizmente, a GitHub bloqueou o push com a chave antes que ficasse pública de verdade.

Dica: Adicione gitignore templates ao seu repo. Eu uso um .gitignore global no repo com tudo que é sensível. E, se quiser ir paranoico (recomendo), use as mesmas práticas de segurança que aplico em servidores.

O Workflow Completo: Do Zero ao Ambiente Pronto

Resumindo, aqui está o fluxo completo que uso hoje:

Na máquina nova:

# 1. Clonar o repo
git clone git@github.com:SEU_USER/dotfiles.git ~/dotfiles

# 2. Rodar o installer
cd ~/dotfiles && chmod +x install.sh
./install.sh seu@email.com

# 3. Aguardar ~3 minutos (pacotes + stow + plugins)

# 4. Reiniciar o terminal
source ~/.zshrc

Quando quero alterar uma config:

# 1. Editar o arquivo normalmente
nvim ~/.tmux.conf  # (que é um symlink pro repo)

# 2. Commitar a mudança
cd ~/dotfiles
git add tmux/.tmux.conf
git commit -m "tmux: habilitar mouse scroll"
git push

# 3. Nas outras máquinas, só pull
cd ~/dotfiles && git pull

Quando formato o PC:

# Install Fedora/Ubuntu
# Instalar Git
# Clonar dotfiles
# Rodar install.sh
# Ir tomar café enquanto Ansible faz tudo
# Voltar com ambiente idêntico

Simples assim. De 6 horas manuais pra 15 minutos automatizados. E cada segundo foi ganho porque parei de ser teimoso e comecei a ser metódico.

Stow vs. Alternativas: Chega de Comparação superficial

Existem outras ferramentas pra gerenciar dotfiles. Vou ser direto sobre cada uma:

  • Chezmoidi: Unifica tudo em um lugar. Bom, mas é uma ferramenta a mais pra aprender e maintain. O Stow já vem no package manager de qualquer distro.
  • YADM (Yet Another Dotfiles Manager): Usa Git diretamente com worktree na home. Inteligente, mas conflitos de Git na home são um pesadelo pra debugar.
  • Bare Git Repository: A mesma ideia do YADM, mas manual. Funciona até o dia que git status na home te mostra 500 untracked files.
  • GNU Stow: Faz UMA coisa e faz bem. Symlinks limpos, reversíveis, sem mágica. Se você já sabe usar symlinks, já sabe usar Stow.

O Ansible não é concorrente do Stow — é complementar. O Stow gerencia os arquivos; o Ansible gerencia o ambiente. Juntos, cobrem tudo. E, como mostrei em automações com n8n, automatizar é sobre combinar as ferramentas certas, não sobre encontrar A Ferramenta Única.

Dicas Que Eu Queria Ter Sabido No Começo

  1. Comece pequeno. Não tente stowar tudo de uma vez. Comece com .bashrc e .gitconfig. Adicione o resto aos poucos.
  2. Teste numa máquina virtual antes. Clone o repo, rode o playbook. Se algo quebrar, é VM — ninguém se machuca.
  3. Use tags no Ansible pra poder rodar partes específicas: ansible-playbook playbook.yml --tags "stow"
  4. Versionamento semântico funciona pra dotfiles também. Quando mudo algo grande no Neovim, faço um tag: git tag nvim-v2. Se der ruim, git checkout nvim-v2.
  5. Não versione segredos. Nunca. Sério. Use variáveis de ambiente ou um password manager. Se quiser saber como montar um sistema de senhas que funciona, contei tudo neste post sobre Bitwarden e 2FA.
  6. Documente no README. Seu eu do futuro vai agradecer. Anote o que cada pacote faz e quais dependências ele assume.

E Agora, Qual Automação Você Quer Ver?

Gerenciar dotfiles é só a ponta do iceberg. Uma vez que você tem um ambiente versionado e automatizado, as possibilidades explodem: containers com Docker que herdam suas configs, scripts de provisioning pra novas VMs, CI/CD pra validar seus dotfiles antes de deploy…

Me conta nos comentários: qual parte do seu ambiente você mais sofre pra reconfigurar quando formata? Qual automação você quer ver aqui no Lab da Garra? Tô coletando ideias pra próximos posts — e prometo que vou automatizar todas.

E se você já usa Stow, Ansible, ou qualquer outra abordagem pra seus dotfiles, compartilha aí. A melhor configuração é aquela que funciona pra você — mas a segunda melhor é aquela que alguém já debugou antes de você. 😉

Posts Similares