From Tooling Fatigue to 'Mise en Place': Consolidating My Dev Stack
Ditching the manager soup of tfenv, nvm, and pyenv for a single, declarative source of truth.
The Hook: Too Many Managers, Not Enough Magic
I’ve spent years building cloud-native systems, backend services, and frontend applications, but recently, my local development environment started feeling like a house of cards.
It wasn’t the code; it was the “manager soup.” To get a single project running, I was juggling tfenv for Terraform, tgenv for Terragrunt, nvm for Node, pyenv for Python, and asdf for… everything else. Each one has its own syntax, its own way of “shimming” binaries, and its own hidden version file.
The breaking point? Refreshing my development machine. I realized I had to manually reinstall and reconfigure half a dozen meta-tools before I could even run go mod tidy. It was death by a thousand “command not found” errors. I wanted Mise en Place—everything in its place—not a junk drawer of version managers. That’s when I went “Nuclear” and moved everything to mise.
The Migration: The “Nuclear” & Legacy Support
Transitioning didn’t require me to break everything on day one. One of the best features of mise is its ability to ingest legacy files like .nvmrc or .terraform-version out of the box. This allowed me to do a “Nuclear” migration of the underlying tools while slowly consolidating the configuration.
Eventually, I moved everything into a single mise.toml. I also took the opportunity to consolidate my environment variable management. While I still use .env files to keep secrets and sensitive values out of Git, I now load them directly in mise using the _.file option.
[env]
# Loads your existing secrets safely without extra shell hooks
"_.file" = ".env"
APP_PORT = "8080"
The “Dev” Task: Orchestrating the Chaos
One of the biggest wins in this migration was moving away from a separate justfile. While just is a solid command runner, mise treats tasks as first-class citizens that understand your environment and dependencies.
In my previous setup, spinning up a full environment meant opening three terminal tabs or writing a brittle bash script. With mise, I’ve consolidated my Go backend, Python/Django services, and Astro frontend into a single, concurrent workflow.
The Dependency Tree
The real power is in the depends_on and parallel execution. I can ensure my Docker containers (Postgres, Redis, etc.) are healthy before the compilers even touch my code.
[tasks.docker]
description = "Spin up infrastructure"
run = "docker-compose up -d"
[tasks.backend]
description = "Start the Go monoservice"
depends_on = ["docker"]
run = "go run main.go"
[tasks.frontend]
description = "Start the Astro dev server"
run = "npm run dev"
[tasks.dev]
description = "Start the full stack"
# mise runs these concurrently by default
depends_on = ["backend", "frontend"]
Why this is cleaner:
- One Command: I just type
mise run dev. - Context Awareness:
miseensures that whentasks.backendruns, it’s using the exact version of Go defined in the samemise.toml. - Environment Injection: Every task automatically has access to my secrets and config without me having to prefix commands with
dotenvor source files manually.
A Note on CI/CD
While mise is a local powerhouse, I still rely on native GitHub Actions for CI workflows. Why? GitHub API rate limits. Automatically fetching toolchains in a high-velocity CI environment can lead to flaky builds, so I keep the “magic” for my local machine and keep the CI “boring” and stable.
Standing on the Shoulders of Giants
Before I wrap up, it’s worth noting that while I’ve consolidated my workflow, the tools I moved away from—tfenv, nvm, pyenv, just, direnv and asdf—remain world-class at what they do. The creators and contributors of these projects solved massive, fundamental problems for the engineering community. They provided the blueprint for modern environment management and have served me reliably for years.
My move to mise isn’t about these tools being “lesser”; it’s about a shift in how I prefer to handle context switching. Mise offers a unified, declarative approach that fits my current mental model for managing polyglot environments in a single file.
If you are just starting to explore tool management, I’d actually encourage you to try both paths. Spend some time with the specialized managers and the “all-in-one” approach of mise. Tooling is a deeply personal part of a developer’s DX; finding the one that removes the most friction for you is what matters most.
The Final Result: A Unified mise.toml
To wrap things up, here is what a consolidated mise.toml looks like when you bring the runtimes, environment variables, and tasks together for a project.
[tools]
pre-commit = "latest"
python = "3.14"
semgrep = "latest"
uv = "latest"
[env]
PYTHONDONTWRITEBYTECODE = "1"
PYTHONUNBUFFERED = "1"
_.file = ".env"
# Mise handles the creation and activation of your Python virtual environment automatically. No more source .venv/bin/activate every time you change directories.
_.python.venv = { path = ".venv", create = true }
# This adds the virtual environment's bin directory to the PATH for all tasks and your current shell session
_.path = [ ".venv/bin" ]
[tasks.install]
run = "uv sync"
description = "Install dependencies"
[tasks.serve]
run = "uv run python manage.py runserver"
description = "Run django development server"
[tasks."celery:worker"]
run = "watchmedo auto-restart --recursive --directory=./ --patterns='**/*.py' -- uv run celery -A config worker -l info"
description = "Run celery worker"
[tasks."celery:beat"]
run = "watchmedo auto-restart --recursive --directory=./ --patterns='**/*.py' -- uv run celery -A config beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler"
description = "Run celery beat"
[tasks."test:unit"]
run = "uv run pytest"
env = { DJANGO_ENV = "testing" }
description = "Run tests"
[tasks."test:e2e"]
run = "uv run pytest e2e"
env = { DJANGO_ENV = "testing" }
description = "Run E2E tests"
[tasks.test]
run = [{ task = "test:unit" }, { task = "test:e2e" }]
description = "Run all tests (unit + E2E)"
[tasks."docker:up"]
run = "docker compose -f compose.dev.yml up -d"
description = "Start docker stack"
[tasks."db:migrate"]
run = "uv run python manage.py migrate"
description = "Run database migrations"
[tasks.dev]
run = [
{ task = "docker:up" },
{ task = "db:migrate" },
{ tasks = [
"serve",
"celery:worker",
"celery:beat",
] },
]
description = "Run development environment (Docker + Django + Celery)"
Lessons Learned: The Clarity of a Clean Stack
Moving to mise wasn’t just about deleting a few hidden files. It was about reducing the “background radiation” of local development. After a few months with this setup, here are my takeaways:
The Single Source of Truth is Real: There is a massive cognitive win when you stop asking, “Wait, which tool is managing Python right now?” Having your runtimes, environment variables, and tasks in one mise.toml means the answer is always in one place.
Don’t Fight the CI/CD Reality: Know where the tool ends and the platform begins. Sticking to native GHA for CI avoids those pesky API rate limits and keeps your pipelines reproducible without extra dependencies.
Cleanup is Cathartic: While mise reads legacy files, don’t let them linger forever. Consolidating into one file tree is the ultimate goal for a clean, professional repository.
The “One Tool” Mindset: I didn’t realize how much I tolerated tooling friction until it was gone. Now, my environment feels like it was designed, not just accumulated.
If you’re feeling the weight of “manager soup,” give mise a shot. Your future self (and your refreshed dev machine) will thank you.
Tags
References
- mise
A unified tool manager and task runner for local development environments.
- tfenv
Terraform version manager.
- tgenv
Terragrunt version manager.
- nvm
Node.js version manager.
- pyenv
Python version manager.
- asdf
A version manager that supports multiple languages and tools.
- direnv
A tool for managing environment variables per project.
Feedback
Thoughts or corrections? Drop me a line at feedback@buckbrady.com or find me on GitHub at voidrot.
Content disclaimer
A note on AI: To assist with editorial review and visual elements, AI tools may be utilized in the creation of these posts. Technical insights and opinions remain entirely my own.