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
, andasdf
. 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:
💡 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.