Skip to content

Set up a workstation

Use the review-first path for an existing machine. The one-line installer is intended for a new machine or for a configuration you already trust.

Before you begin

The full setup can:

  • install apt packages, Homebrew packages, and macOS applications;
  • change your login shell to fish;
  • initialize chezmoi and force-apply files from this repository;
  • install tmux and Neovim plugins;
  • install Rancher Desktop on macOS; and
  • change macOS defaults, Dock contents, and keyboard shortcuts.

Back up your existing configuration before continuing. At minimum, inspect config.yaml and the files under config/.

Platform requirements

Install available system updates and the Xcode Command Line Tools on a fresh machine:

sudo softwareupdate -i -a
xcode-select --install

Use an account with sudo access. The bootstrap installs the apt prerequisites, adds the Ansible PPA, and then installs Homebrew.

Apple Silicon macOS is the supported workstation target. Ubuntu 24.04 ARM64 is the continuously tested container and integration target. The setup may work on other Debian-family systems, but those systems are best effort. Other operating systems are rejected.

Clone the repository so you can inspect exactly what the setup will run. Pay particular attention to config.yaml and the main.yaml playbook:

git clone https://github.com/shmileee/dotfiles.git
cd dotfiles

less scripts/common/ansible/config.yaml
less scripts/common/ansible/main.yaml

When you are comfortable with the configuration, run the complete setup:

./scripts/setup.sh --all

The script uses the current checkout, so local changes to package lists, Ansible roles, or managed dotfiles are included.

Fast path: bootstrap a new machine

curl -fsSL oponomarov.com/d | sh -s -- --all

The short URL redirects to scripts/setup.sh on the master branch. The script downloads that branch into a unique temporary directory, runs the full setup, and removes the temporary checkout when it exits.

Download the script before running it

If you want the convenience of the bootstrap without piping directly into a shell:

curl -fsSL https://raw.githubusercontent.com/shmileee/dotfiles/master/scripts/setup.sh > setup.sh
less setup.sh
chmod +x setup.sh
./setup.sh --all

What --all does

  1. 01
    Validate the platform

    Continue only on macOS or Linux, then locate the current checkout or download one.

  2. 02
    Prepare Ubuntu

    On the tested Linux target, install the required apt packages and Ansible before the shared setup begins.

  3. 03
    Prepare Homebrew

    Install Homebrew if necessary, add it to the current process, and disable analytics.

  4. 04
    Run Ansible

    Install the required collections, prompt for a sudo password if needed, and run the local playbook.

Run one stage

Stage flags must be run from a repository checkout.

Command Purpose
./scripts/setup.sh --deps Install apt prerequisites on Linux; tested on Ubuntu 24.04 ARM64.
./scripts/setup.sh --brew Install Homebrew if it is missing.
./scripts/setup.sh --ansible Install Ansible collections and run every role.
./scripts/setup.sh --all Run the complete platform-specific sequence.
./scripts/setup.sh Same as --all.

If the prerequisites are already installed, use Ansible to preview supported changes:

./scripts/common/ansible.sh --all --check

Check mode has limits

Ansible check mode is useful for file and package changes, but command-based tasks cannot always predict their effects. Treat it as a preview, not a full simulation.

Customize the setup

Make changes in four places:

Area Source of truth Typical changes
Packages and applications scripts/common/ansible/config.yaml Homebrew packages, casks, Dock items, keyboard shortcuts
System behavior scripts/common/ansible/roles/ Installation logic and macOS defaults
Home-directory files config/ fish, Git, tmux, Neovim, Alacritty, OpenCode
Tool versions config/private_dot_config/mise/config.toml Language runtimes and developer tools

The dotfiles.checkout value in config.yaml points chezmoi at the current repository checkout. The role force-applies that checkout's config/ directory, so a fork or local changes can be tested without changing a separate repository or branch setting.

Ansible roles

Role Responsibility
common Install shared command-line tools and platform-specific packages and applications
fonts Install developer fonts on macOS or the Ubuntu integration environment
dotfiles Install chezmoi and apply the current checkout
fish Install fish, make it the login shell, and synchronize Fisher plugins
mise Install the tools declared in the mise configuration
neovim Install LazyVim and its plugins in headless mode
docker Install Rancher Desktop on macOS
tmux Install tmux, TPM, and declared plugins
system_defaults Apply macOS preferences, Dock items, and keyboard settings

Routine work with mise

The initial setup installs mise and the tools pinned by this repository. After that bootstrap, run routine workflows from the repository checkout with mise run. List the available tasks and their descriptions at any time:

mise tasks
Command Purpose
mise run reconcile Install the required Ansible collections and reconcile the machine.
mise run reconcile:check Preview the reconciliation using Ansible check mode.
mise run status Show differences between the chezmoi source and files in the home directory.
mise run import Import all modified, non-template managed files into config/.
mise run import ~/.config/nvim Import one managed file or directory.
mise run docs Serve the documentation at http://localhost:8000 and rebuild it on changes.
mise run docs:build Run the strict documentation build used by CI.

Bootstrap before using tasks

mise is the task runner, but it is also installed by Ansible. On a new machine, run ./scripts/setup.sh --all first. The mise tasks are the post-bootstrap interface, not a replacement for initial setup.

Task execution installs any missing tools declared in mise.toml automatically.

Import local dotfile changes

When a managed file was edited directly in the home directory, inspect the differences before copying them back into the repository:

mise run status
mise run import ~/.config/nvim
git diff -- config

Omit the path to import every modified managed file. The task uses chezmoi re-add, which deliberately does not overwrite template source files. For a rendered file backed by config/**/*.tmpl, reconcile the local change with its template explicitly. Always review the resulting Git diff before committing.

Preview the documentation

Start the local Zensical server with:

mise run docs

Open http://localhost:8000. The preview rebuilds when documentation, configuration, templates, CSS, or JavaScript change. Before committing a docs change, run the same strict build as CI:

mise run docs:build

Reapply after an update

After the initial setup has installed mise, pull the latest changes, review them, and rerun the Ansible stage:

git pull --ff-only
git diff HEAD@{1} -- scripts/common/ansible config
mise run reconcile

The roles are designed to be rerun, and a repeated run should leave converged state unchanged. Setup may still refresh Homebrew metadata and update managed formulae or versioned casks, so review changes before rerunning after a long gap.

There is no repository-wide upgrade command. Use brew upgrade for an intentional full Homebrew upgrade, let Renovate propose changes to versions declared in the repository, and use Lazy, Fisher, or TPM for intentional plugin updates. Review and commit any resulting lockfile, manifest, or Ansible pin changes. The checked-out chezmoi source remains authoritative and is force-applied during setup.

Try the Linux path in Docker

Run the published image:

docker run --rm -it shmileee/dotfiles

Or build the current checkout:

docker buildx build --platform linux/arm64 -t dotfiles --progress plain .

The image uses Ubuntu 24.04 ARM64 and runs the full Ansible installation as the non-root linuxbrew user. Its smoke test verifies the installed tools and a second, idempotent provisioning pass. The docker role is intentionally skipped inside the container.

Validate changes locally

Run the same static checks and Ansible syntax validation used by CI:

mise install
mise exec -- scripts/common/ansible.sh --install
mise exec -- prek run --all-files
mise exec -- env ANSIBLE_CONFIG=scripts/common/ansible/ansible.cfg \
  ansible-playbook --inventory '127.0.0.1,' \
  --syntax-check scripts/common/ansible/main.yaml

If the prerequisites are installed, preview the playbook too:

mise run reconcile:check

Why Ansible and chezmoi?

Ansible owns machine state: packages, applications, services, shell setup, and operating-system preferences. Its roles make the order and platform conditions explicit, and repeated runs provide a practical convergence check.

chezmoi owns files in the home directory. It renders templates using facts such as the operating system and architecture, which keeps one source tree useful across macOS and the Ubuntu integration environment. Keeping these responsibilities separate makes it clear whether a change belongs to the machine or to the user's configuration.