Managing Environments with mise and MISE_ENV

What is mise?

mise (formerly rtx) is a polyglot runtime and task manager. It replaces tools like nvm, pyenv, rbenv, and direnv with a single unified tool. Beyond managing language runtimes, mise can:

  • Run project tasks (like make or npm run)
  • Inject environment variables
  • Decrypt secrets via SOPS
  • Load .env files automatically

Configuration lives in mise.toml at the root of your project.


The MISE_ENV Profile System

mise supports environment profiles via the MISE_ENV variable. When set, mise loads two config files:

  1. mise.toml — the base config, always loaded
  2. mise.<MISE_ENV>.toml — the profile overlay, loaded on top

Settings in the profile file override or extend the base. This makes it straightforward to have environment-specific behaviour without duplicating your entire config.

Profile file naming

MISE_ENV valueProfile file loaded
devmise.dev.toml
productionmise.production.toml
uatmise.uat.toml
(unset)only mise.toml

Project Structure

A typical layout with three environments:

app/
├── mise.toml              # base: tools, shared tasks
├── mise.dev.toml          # dev: loads .env, dev-specific tasks
├── mise.uat.toml          # uat: loads secrets.uat.env via sops
├── mise.production.toml   # prod: loads secrets.env via sops
├── .env                   # local dev secrets (gitignored)
├── secrets.env            # prod secrets, sops-encrypted (committed)
└── secrets.uat.env        # uat secrets, sops-encrypted (committed)

Base Config (mise.toml)

The base config defines tools and tasks that are shared across all environments. It does not load any secrets itself.

[tools]
python = "3.11"
 
[tasks.install]
description = "Install Python dependencies"
run = "pip install -r requirements.txt"
 
[tasks.migrate]
description = "Run DB migrations"
run = "alembic upgrade head"

Dev Profile (mise.dev.toml)

The dev profile loads a plain .env file (gitignored, local only). No encryption needed. Tasks can be overridden or extended to add dev-specific behaviour like starting a local database container.

[env]
_.file = ".env"
 
[tasks.setup]
description = "Start DB, migrate, and launch dev server"
run = """
pip install -r requirements.txt
docker compose up -d
sleep 5
alembic upgrade head
reflex run
"""

.env contains plain key/value pairs pointing at local infrastructure:

DATABASE_URL=postgresql://user:pass@localhost:5432/myapp
OPENAI_API_KEY=sk-...
API_URL=http://localhost:8503

Production Profile (mise.production.toml)

The production profile uses the _.sops directive to decrypt an encrypted secrets file at task startup. mise calls sops internally — no manual decryption step needed.

[tasks.prod]
description = "Migrate and run production server"
run = """
export $(sops -d .secrets.env | xargs) # Manually load secrets
alembic upgrade head
python worker.py &
reflex run --env prod --backend-host 0.0.0.0
"""

secrets.env is a SOPS-encrypted dotenv file committed to the repository. The encryption key (SOPS_AGE_KEY) must be available in the environment at runtime — mise handles the rest.


UAT Profile (mise.uat.toml)

UAT follows the same pattern as production but points at a separate secrets file with UAT-specific values (different database, different API keys, etc.).

[env]
_.sops = "secrets.uat.env"
 
[tasks.prod]
description = "Migrate and run UAT server"
run = """
alembic upgrade head
reflex run --env prod --backend-host 0.0.0.0
"""

Secrets Management with SOPS

SOPS (Secrets OPerationS) encrypts files in place. When combined with Age keys, encrypted files can be safely committed to version control.

Encrypting a secrets file

# Encrypt in place (requires .sops.yaml config or --age flag)
sops -e -i secrets.env

How mise decrypts at runtime

When using a task to manually load secrets (e.g., in mise.production.toml), the task typically includes:

  1. Calls sops -d .secrets.env to decrypt the file in memory.
  2. Uses xargs and export to inject the resulting key/value pairs as environment variables into the current shell session.
  3. The task then runs with those variables set.

No plaintext file is ever written to disk.


Activating an Environment

Locally (per shell session)

export MISE_ENV=dev
mise run dev

Locally (permanent default)

Add to ~/.zshrc or ~/.bashrc:

export MISE_ENV=dev

This makes dev the default for all interactive shells. CI and containers always set their own MISE_ENV explicitly, so there is no conflict.

In a Dockerfile

ENV MISE_ENV=production
CMD ["mise", "run", "prod"]

The container also needs SOPS_AGE_KEY passed at runtime:

docker run -e SOPS_AGE_KEY=<your-age-key> myimage

In CI (GitHub Actions)

- name: Deploy
  run: |
    docker run -d 
      -e MISE_ENV=production 
      -e SOPS_AGE_KEY=${{ secrets.AGE_PRIVATE_KEY }} 
      myimage

Because MISE_ENV=production is already baked into the Dockerfile ENV, passing it at runtime is optional but explicit is always clearer.


Summary

EnvironmentMISE_ENVProfile fileSecrets sourceKey required
Local devdevmise.dev.toml.env (plain)No
UATuatmise.uat.tomlsecrets.uat.env (sops)Yes
Productionproductionmise.production.tomlsecrets.env (sops)Yes

The key principle: the base mise.toml owns tools and shared tasks; each profile owns its secrets source and any environment-specific task overrides. Switching environments is a single variable — nothing else changes.