Hand off from an Ubuntu autoinstall to Dotfiles Pro
A Subiquity autoinstall turns a blank laptop into a workstation: disk layout, encryption, the user account, apt packages, snaps. Many teams then bolt a long late-commands block of curl | bash installers onto the end, oh-my-zsh, Docker, Homebrew, a version manager, a dozen CLIs.
That block solves day 0 and never runs again. But installed is the part of a machine that never breaks, what breaks is configured (the team-fleet post makes the full case). This guide shrinks late-commands to a handoff and lets Dotfiles Pro own day 2.
The dividing line
| Stays in the autoinstall (day 0) | Moves to Dotfiles Pro roles (day 2) |
|---|---|
| Partitioning, LVM, disk encryption | Shell, aliases, prompt |
| The user account, locale, keyboard, network | Git identity, SSH keys, cloned repositories |
| The base apt packages the first login needs | Dev tooling (Docker, kubectl, language runtimes) |
| Anything that must exist before first login | CAs, internal CLIs, the company dev environment |
Rule of thumb: if it must exist before anyone logs in, the installer owns it; if it is configuration that can apply in a user session, a role owns it.
The thin late-commands
The whole handoff is making sure Ansible and git exist:
---
# autoinstall.yaml (excerpt)
late-commands:
- curtin in-target -- apt-get install -y ansible ansible-mitogen gitEverything else the old block did becomes a role that runs at first login, and on every apply after.
First login: apply from the personal repo
The developer runs the installer pointed at their personal repo. Ansible is already present, so this clones the repo and installs the collections:
curl -LsSf https://dotfiles.pro/install.sh | sh -s -- git@gitlab.example.com:alice/mydotfiles.gitThen apply the host with dotpro apply (or DOTPRO_APPLY=1 to bootstrap and apply at once). From then on, reinstall and update are the same code path, dotpro apply.
The SSH chicken-and-egg
Cloning a private repo needs an SSH key a freshly imaged machine doesn't have yet. That's why the clone happens at first login, a human is present to generate and register the key, not in late-commands. Three ways through it:
- Run the installer at first login (recommended), register the SSH key, then run the command above.
- Clone over HTTPS with a token, then flip the remote to SSH afterwards.
- Keep the company baseline public/HTTPS so an image-time apply needs no secrets; identity and private repos still wait for first login.
A bootstrap that generates the key and pauses to register it is on the team-provisioning roadmap.
Applying a baseline at image time
With no repo argument the installer scaffolds a starter; dotpro apply (or DOTPRO_APPLY=1) then applies a public base. You can also point it at a public/HTTPS company baseline. Today the installer runs interactively (--ask-become-pass) and limits to the hostname, so a fully non-interactive image-time apply (CI/Docker detection, root-vs-user) is roadmap, not shipped. For now, prefer the first-login handoff above and keep late-commands to installing Ansible.
Turn the curl|bash block into roles
This is the migration. Each imperative installer becomes an idempotent role, so a reinstall and a config update stop being two different things.
Before, a representative slice of a real late-commands block:
---
late-commands:
- curtin in-target -- bash -c 'wget -qO- https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh |
bash'
- curtin in-target -- bash -c 'curl -fsSL https://download.docker.com/linux/ubuntu/... | sh'
- curtin in-target -- npm install -g @anthropic-ai/claude-code
- curtin in-target -- bash -c '/bin/bash -c "$(curl -fsSL .../Homebrew/install.sh)"'
# …and a dozen moreAfter, one line, plus roles in the playbook:
---
# autoinstall.yaml (excerpt)
late-commands:
- curtin in-target -- apt-get install -y ansible ansible-mitogen gitWhere each installer lands:
Day-0 late-command | Day-2 role |
|---|---|
| Docker apt repo + compose plugin | dotfilespro.roles.docker, shipped |
Claude Code (npm install -g) | dotfilespro.roles.claude, shipped |
| oh-my-zsh installer | dotfilespro.roles.zsh via shell: zsh, shipped; oh-my-zsh itself (roadmap) |
| nvm / Node, npm globals | node role (roadmap) |
| Homebrew | homebrew role (roadmap) |
| 1Password CLI apt repo | 1password role (roadmap) |
| Warp terminal apt repo | warp role (roadmap) |
| Backblaze B2 CLI venv | b2 role (roadmap) |
| mkcert CA first-boot unit | mkcert role (roadmap) |
fs.inotify.max_user_watches bump | ubuntu sysctl var (roadmap) |
| snaps (Slack, PhpStorm, …) | ubuntu_snaps var (roadmap) |
| MageBox / shop-platform dev env | company.roles.devstack, your collection |
Anything generic is worth contributing upstream; anything only your company needs goes in your company collection.
Adopt without a big bang
Roles are idempotent, so they run safely next to the existing installer, it's fine that late-commands already installed Docker; the role just applies it.
The realistic path is bottom-up: one developer pilots a personal repo with two or three roles, and each late-commands line retires only when its role replaces it. Every step is reversible, and the old block stays the fallback until it's empty.
Gotchas
zsh. If the image sets everyone's shell to zsh, set
shell: zshin the host'shost_vars. Theunixrole then wires~/.profile.d/into~/.zprofileand~/.zshrcand runs thezshrole. oh-my-zsh itself is still roadmap.become password. The installer uses
--ask-become-pass, which prompts, fine at first login, blocking in a non-interactive autoinstall. Keeplate-commandsto the apt line and let the human apply after login.
Next steps
- Set up a company dotfiles collection the
company.roleslayer the table above points to - Twenty laptops, one installer drift, the seam, rollout, onboarding
- Getting started the installer and first run in detail
- Team provisioning roadmap the bootstrap script, late-commands migration, and apply timer