A couple of months ago, I shared my dotfiles setup using GNU Stow. It worked fine for a while, but once I started switching between Arch Linux, macOS, and Ubuntu running inside a devcontainer, I noticed it just was not holding up. It felt too fragile.
I wanted something that did not care which operating system I was on. I wanted to be able to spin up my dotfiles and all my packages anywhere, whether that was my laptop, my desktop, or some random devcontainer in the cloud.
Over the years, my list of installed packages had grown a lot. I wanted a way to keep my host system clean but still have all the tools I needed available for each project.
In the following post, I will describe my new reproducible development environment and how it aligns with my new dotfiles approach. If you would like to follow along, check out my example-devcontainer project as well as my dotfiles repository.
DevContainers
A devcontainer is basically a self-contained development environment. You put everything your project needs inside it, including tools, libraries, and runtimes, and it runs in isolation from your machine. This keeps your host system clean and avoids the “it works on my machine” problem.
You can run devcontainers locally, remotely, or even in the cloud. That flexibility makes them perfect for this type of setup.
DevPod
DevPod takes the idea of a devcontainer and makes it simple to reproduce the same environment anywhere. It links your container with your IDE, whether you use VS Code, Neovim, or something else, and it also supports dotfiles right out of the box which is a big advantage for me.
It works on Windows, macOS, and Linux so I can use it anywhere without having to rethink my setup.
After installing DevPod, the first thing I do is add the Docker provider:
devpod provider add docker
A provider tells DevPod how to run the workspace. The workspace is the actual container with all the project files and tools.
Here is my devcontainer.json from one of my example projects:
{
"build": {
"context": "..",
"dockerfile": "Dockerfile"
},
"postCreateCommand": "scripts/setup"
}
This tells DevPod to build the container from a Dockerfile in the parent directory and then run scripts/setup after it is created.
Dockerfile
FROM mcr.microsoft.com/devcontainers/base:ubuntu-24.04
COPY --from=jdxcode/mise /usr/local/bin/mise /usr/local/bin/
RUN echo 'eval "$(mise activate bash)"' >> ~/.bashrc && \
echo 'eval "$(mise activate zsh)"' >> ~/.zshrc
I start with the official Ubuntu 24.04 devcontainer image. Then I copy the mise binary from the jdxcode/mise image into /usr/local/bin. Finally, I add commands so mise is activated automatically in both Bash and Zsh.
The drawback is that any time I change this Dockerfile I have to rebuild the whole container. That is fine for larger updates, but not ideal if I just want to quickly test a new CLI tool or try a different programming language version.
This made me look for a way to manage tools inside the devcontainer without baking them directly into the image.
Mise
Mise is a cross-platform package manager that does not depend on any specific programming language. If you have used pyenv or nvm you will find it familiar, but Mise works for everything.
It can install multiple versions of the same tool, manage environment variables, and define tasks similar to a Makefile. Everything is configured in a single TOML file.
Here is an example mise.toml:
[tools]
kubectl = "latest"
go = ["1.21.5", "1.22.0"]
aws-cli = "2.15.0"
1password-cli = "latest"
This setup installs the latest versions of these tools, but you could also lock them to specific versions or install more than one version if needed.
Before Mise applies a configuration, it needs to trust it. I keep that process in a small script so I do not have to type the commands each time:
#!/bin/bash
/usr/local/bin/mise trust /workspaces/example-devcontainer/mise.toml && \
/usr/local/bin/mise install
Chezmoi
For dotfiles, I now use Chezmoi. Unlike GNU Stow, which only creates symlinks, Chezmoi manages your files with a state file similar to Terraform. You can preview changes before applying them and then update everything with chezmoi apply. Your dotfiles repository becomes the single source of truth.
Here is my DevPod setup script:
#!/bin/bash
set -euo pipefail
if command -v zsh >/dev/null; then
sudo chsh -s "$(command -v zsh)" "$USER"
fi
if ! command -v chezmoi >/dev/null; then
sh -c "$(curl -fsLs get.chezmoi.io)" -- init --apply https://github.com/dankaiser1808/dotfiles.git
fi
if [ ! -d "$HOME/.zsh" ]; then
mkdir -p "$HOME/.zsh"
fi
# zsh auto-suggestions
git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions
# zsh syntax highlighting
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting
# zsh fzf-tab
git clone https://github.com/Aloxaf/fzf-tab ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/fzf-tab
This script changes my default shell to Zsh if it is installed, installs Chezmoi if missing, applies my dotfiles, and installs some Zsh plugins that I like to have everywhere.
Installing Mise with Chezmoi
Chezmoi can detect the operating system and architecture so I can install the correct Mise binary automatically:
[".local/bin/mise"]
type = "file"
executable = true
url = "https://mise.jdx.dev/mise-latest-{{.chezmoi.os}}-{{.chezmoi.arch}}"
I also have a script that installs packages only when the configuration changes:
#!/bin/bash
# packages hash: {{ include "dot_config/mise/config.toml" | sha256sum }}
$HOME/.local/bin/mise trust $HOME/.config/mise/config.toml && $HOME/.local/bin/mise install --verbose
By naming this file .chezmoiscripts/run_onchange_after_install_packages.sh.tmpl and including the configuration hash, it only runs when something has actually changed.
Why I like this setup
This way my host stays clean, my environments are consistent across every operating system I use, and I can change tools or versions whenever I need without touching the base image. Everything from my dotfiles to my packages is stored in version control, and I can apply the entire setup with just a few commands.
In my next post I will share how I run DevPod devcontainers in a Kubernetes cluster.
As always, thanks for reading!