Using mise (mise-en-place) in DevOps: Consistent Tool Version Management for kubectl, Terraform, Node, and Python

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

The Version Mismatch Problem That Wastes Your Afternoons

Your colleague pushed a Terraform plan that works perfectly on their machine. You pull it, run terraform apply, and hit a cryptic error. Three hours later, you discover they’re on Terraform 1.7 and you’re on 1.5. Sound familiar?

This is one of the most common, most annoying, and most avoidable pain points in DevOps. The root cause is simple: teams lack a shared, enforced contract for which version of each tool everyone — and every pipeline — should use. .nvmrc helps for Node.js. pyenv helps for Python. But what about kubectl? Terraform? Helm? You end up with a zoo of version managers, each with its own syntax, activation mechanism, and CI integration story.

mise (pronounced “meez”, short for mise en place) solves this with a single config file and a single tool. I’ve applied this approach in production across multiple teams and the results have been consistently stable — no more “works on my machine” Terraform surprises.

Quick Start: Up and Running in 5 Minutes

Install mise

On Linux and macOS, the fastest way is the official installer:

# Linux / macOS
curl https://mise.run | sh

# Or via Homebrew (macOS)
brew install mise

Then activate it in your shell. Add this to your ~/.bashrc, ~/.zshrc, or equivalent:

# For bash
echo 'eval "$(mise activate bash)"' >> ~/.bashrc
source ~/.bashrc

# For zsh
echo 'eval "$(mise activate zsh)"' >> ~/.zshrc
source ~/.zshrc

Define your tools in a config file

Navigate to your project root and create a .mise.toml file:

[tools]
terraform = "1.8.5"
kubectl = "1.30.2"
node = "20.14.0"
python = "3.12.3"
helm = "3.15.2"

Then install everything declared in that file with one command:

mise install

mise downloads, caches, and activates all listed versions. From that point on, any terminal session inside the project directory automatically uses these exact versions — no manual switching, no export statements, no activation scripts to remember.

Verify it worked:

mise current
# terraform  1.8.5  .mise.toml
# kubectl    1.30.2 .mise.toml
# node       20.14.0 .mise.toml
# python     3.12.3  .mise.toml

Deep Dive: How mise Actually Works

The config file cascade

mise reads .mise.toml files starting from the current directory and walking up toward $HOME. This means you can have global defaults at ~/.config/mise/config.toml and project-specific overrides at the repo level. Project config always wins.

It also understands legacy version files — if your project already has an .nvmrc, .node-version, .python-version, or .terraform-version, mise picks those up automatically without any migration.

Shims vs PATH injection

mise uses PATH injection (not shims like asdf). When you run eval "$(mise activate bash)", it prepends a managed directory to your PATH. This is faster and more transparent — which terraform gives you a real binary path, not a shim wrapper. No mysterious slowdowns on every command invocation.

Plugin ecosystem

mise uses the same plugin registry as asdf, which means it supports 400+ tools out of the box. Installing a tool not in the core list is as simple as:

# Install a specific tool version interactively
mise use [email protected]

# List all available versions for a tool
mise ls-remote terraform

# See what's installed globally
mise ls

Advanced Usage: CI/CD Integration

GitHub Actions

This is where mise really pays off. Instead of pinning tool versions in three different places (local, CI yaml, Dockerfile), everything comes from the single .mise.toml in the repo.

name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install mise
        uses: jdx/mise-action@v2
        # Automatically reads .mise.toml and installs all tools

      - name: Verify versions
        run: mise current

      - name: Terraform plan
        run: |
          terraform init
          terraform plan

The jdx/mise-action action handles caching too — it stores downloaded tool binaries between runs, so your pipeline doesn’t re-download Terraform from scratch every time.

GitLab CI

default:
  before_script:
    - curl https://mise.run | sh
    - echo 'export PATH="$HOME/.local/share/mise/shims:$PATH"' >> ~/.bashrc
    - source ~/.bashrc
    - mise install

deploy:
  script:
    - terraform init
    - terraform apply -auto-approve

For repeated use, build a base Docker image with mise pre-installed rather than running the install script on every job.

Dockerfile integration

FROM ubuntu:24.04

# Install mise
RUN curl https://mise.run | sh
ENV PATH="/root/.local/share/mise/shims:/root/.local/bin:$PATH"

WORKDIR /app

# Copy version config first for better layer caching
COPY .mise.toml .
RUN mise install

COPY . .
RUN terraform init

Environment variables for CI

mise respects environment-based configuration, which is handy for overriding versions in specific pipeline stages without editing the config file:

# Override a specific tool version via env var
MISE_TERRAFORM_VERSION=1.7.0 terraform plan

# Or set globally for the job
export MISE_TERRAFORM_VERSION=1.7.0

Practical Tips from Production Use

Commit .mise.toml, never .tool-versions

mise supports both .mise.toml and the asdf-compatible .tool-versions format. Always prefer .mise.toml for new projects — TOML gives you richer configuration options (environment variables, task definitions, hooks) that the flat .tool-versions format can’t express.

Use mise tasks for project scripts

One underused feature: mise can replace your Makefile or ad-hoc shell scripts with typed, documented tasks:

[tools]
terraform = "1.8.5"
kubectl = "1.30.2"

[tasks.plan]
description = "Run terraform plan for staging"
run = "terraform -chdir=infra/staging plan"

[tasks.deploy]
description = "Apply and update kubeconfig"
run = [
  "terraform -chdir=infra/staging apply -auto-approve",
  "aws eks update-kubeconfig --name my-cluster"
]
# List available tasks
mise tasks

# Run a task
mise run plan
mise run deploy

Now your entire team runs the same commands using the same tool versions. New team members clone the repo, run mise install, and they’re productive in minutes.

Set project-scoped environment variables

mise also manages environment variables per project — a clean replacement for direnv in many cases:

[tools]
terraform = "1.8.5"

[env]
TF_WORKSPACE = "staging"
AWS_PROFILE = "my-company-staging"
KUBECONFIG = "./kubeconfig"

These variables activate automatically when you enter the directory and deactivate when you leave. No more forgetting to export AWS_PROFILE and accidentally deploying to production from the wrong context.

Trust configuration for shared machines

On shared servers or CI agents, mise requires you to explicitly trust config files before it loads them (a security feature to prevent malicious .mise.toml files from running arbitrary scripts):

# Trust a specific config file
mise trust /path/to/project/.mise.toml

# Or trust all configs in CI (use only in controlled environments)
export MISE_TRUSTED_CONFIG_PATHS="/path/to/project/.mise.toml"

Check version drift across the team

Add a CI step that explicitly verifies versions match the declared config — useful as a gate before expensive plan/apply steps:

#!/bin/bash
# scripts/check-versions.sh
set -e

EXPECTED_TF=$(mise current terraform 2>/dev/null | awk '{print $2}')
ACTUAL_TF=$(terraform version -json | python3 -c "import sys,json; print(json.load(sys.stdin)['terraform_version'])")

if [ "$EXPECTED_TF" != "$ACTUAL_TF" ]; then
  echo "Version mismatch: expected $EXPECTED_TF, got $ACTUAL_TF"
  exit 1
fi

echo "All versions match mise config"

Migration from asdf, nvm, pyenv, tfenv

If you’re already using these tools, migration is low-risk:

  • asdf: mise reads .tool-versions automatically. Just install mise and it picks up your existing config. Remove asdf when you’re confident.
  • nvm: mise reads .nvmrc and .node-version. Run mise install and both tools coexist during transition.
  • pyenv: Reads .python-version. Same story — coexistence is fine during migration.
  • tfenv / tgenv: No automatic detection, but migration is just adding terraform = "x.y.z" to .mise.toml.

The migration strategy I’ve used in production: add .mise.toml to the repo alongside existing version files, then remove the old files in a follow-up PR once the team has confirmed everything works.

When mise Might Not Be the Right Fit

It’s honest to acknowledge the edges. mise manages tool versions, not runtime dependencies like npm packages or pip packages. You still need npm install and pip install -r requirements.txt for that. And if your team is fully standardized on a container-first workflow where every command runs inside Docker, the benefit is smaller (though mise still helps on the host for the tools that manage the containers).

For language runtimes with complex virtual environment semantics (especially Python), mise activates the Python version but you still manage virtualenvs separately — though mise use [email protected] combined with the [env] block for VIRTUAL_ENV can cover most cases cleanly.

Share: