In DevOps, continuous improvement is a core mindset. Tools evolve. Workflows change. And as DevOps practitioners, our setups should reflect that evolution. This post is part of that journey—an update to my personal toolchain that replaces scattered configuration with a structured and scalable approach using using chezmoi and mise.

This post is an update to a previous article that has since been archived. That version outlined a more manual process for environment setup using scripts, direnv, and asdf. While functional, I’ve learned better ways since then.

The Evolution of My DevOps Toolset

Originally, I relied only on a set of manual scripts to configure new environments. It worked—until it didn’t scale. As needs grew, so did the complexity, and it became clear that something more maintainable and flexible was required.

So I took a step back, reflected on what could be improved, and embraced two tools that helped transform my workflow:

🛠 Incorporating Custom Bash Scripts to chezmoi

chezmoi is now the foundation of my environment setup. Rather than discarding the custom scripts I had carefully crafted over time, I adapted and organized them within chezmoi, turning a scattered toolchain into a structured, modular system.

It brings:

  • Declarative dotfile management with templates
  • Environment linking made simple
  • Clean repo structure for long-term maintenance
  • Orchestrated script execution

Beyond the basics, my dotfiles setup leverages some of chezmoi’s more advanced features to make it truly robust and adaptable.

Templates allow me to inject logic into configuration files using Go’s templating language—things like conditional blocks based on hostname, username, or operating system, and reusable snippets that keep everything clean and DRY. I also use .chezmoignore, which supports templating too, to dynamically exclude files depending on the environment—this helps me separate work from personal configurations seamlessly. Below are example configurations you can use as a reference for your own setup.

Filename: .chezmoi.toml.tmpl

{{- $name := promptStringOnce . "name" "User name. Ex: manuel.chichi" -}}
{{- $email := promptStringOnce . "email" "Email address. Ex: [email protected]" -}}
{{- $work := promptBoolOnce . "work" "It's a work related computer? y/N" -}}

[data]
  name = {{ $name | quote }}
  email = {{ $email | quote }}
  work = {{ $work }}

Filename: dot_gitconfig.tmpl

[user]
  name = {{ .name | quote }}
  email = {{ .email | quote }}
[include]
  path = ~/.gitconfig.personal

Filename: .chezmoignore

{{- if ne .work true }}
.zshrc.work
{{- end }}

Security is built-in thanks to encryption with age. Sensitive files are added to the repo using chezmoi add --encrypt, which ensures they’re versioned but only decrypted when applied. It’s a safe and convenient way to manage secrets without compromising security. What’s especially convenient is that chezmoi includes a built-in age binary, so there’s no need to install additional tools—everything just works out of the box. This makes managing secrets both safe and hassle-free.

To automate certain setup tasks, I rely on chezmoi’s script hooks. Scripts prefixed with run_once_ execute only on the first apply, which is ideal for things like installing dependencies or setting up initial configs. Meanwhile, run_onchange_ scripts trigger only when the associated file changes.

🧰 From asdf + direnv to mise

While asdf and direnv are solid tools, mise combines their best features into one modern developer toolchain manager. It handles runtime versions (Node, Python, etc.) and environment loading—but without requiring two separate tools.

What I love about mise:

  • Unified version and environment manager
  • Blazing fast setup with its caching and lazy-loading
  • Fewer moving parts, less overhead

With mise, I can specify exact versions of programming languages and tools required for each project. By defining these in a .mise.toml file, I ensure consistency across different environments and team members. For instance, setting up a project to use Node.js version 18 is as simple as adding the following to the configuration:

[tools]
node = "18"

This approach eliminates the need for global installations and reduces version conflicts.

Mise also offers a task runner feature, allowing me to define and execute custom tasks within the same configuration file. This is particularly useful for automating repetitive tasks like running tests, building projects, or deploying applications. For example, I can define a task to run tests as follows:

[tasks]
test = "npm run test"

Executing mise run test will then run the specified command.

Additionally, mise can manage environment variables on a per-project basis, ensuring that sensitive information like API keys or database credentials are appropriately set and isolated.

[env]
DATABASE_URL = "postgres://user:pass@localhost:5432/db"
API_KEY = "my-api-key"
NODE_ENV = "development"

With this setup, any tool or command you run via mise run or any shell initialized with mise exec will have these environment variables automatically set.

By consolidating version management, task automation, and environment configuration into a single tool, mise simplifies the setup and maintenance of development environments, making them more predictable and easier to manage.

📦 It’s All in the Dotfiles

All of this lives in a single dotfile repository:

👉 dotfiles repo

💡 DevOps = Continuous Improvement

DevOps isn’t just about automation—it’s about iteration. This setup reflects that principle. I’m not afraid to retire old tools if there’s a better way.

If you’ve been using scripts or juggling too many small tools, consider consolidating. Use tools that embrace modern workflows and help you grow with them, not around them.