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
makeornpm run) - Inject environment variables
- Decrypt secrets via SOPS
- Load
.envfiles 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:
mise.toml— the base config, always loadedmise.<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 value | Profile file loaded |
|---|---|
dev | mise.dev.toml |
production | mise.production.toml |
uat | mise.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.envHow mise decrypts at runtime
When using a task to manually load secrets (e.g., in mise.production.toml), the task typically includes:
- Calls
sops -d .secrets.envto decrypt the file in memory. - Uses
xargsandexportto inject the resulting key/value pairs as environment variables into the current shell session. - 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 devLocally (permanent default)
Add to ~/.zshrc or ~/.bashrc:
export MISE_ENV=devThis 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> myimageIn CI (GitHub Actions)
- name: Deploy
run: |
docker run -d
-e MISE_ENV=production
-e SOPS_AGE_KEY=${{ secrets.AGE_PRIVATE_KEY }}
myimageBecause MISE_ENV=production is already baked into the Dockerfile ENV, passing it at runtime is optional but explicit is always clearer.
Summary
| Environment | MISE_ENV | Profile file | Secrets source | Key required |
|---|---|---|---|---|
| Local dev | dev | mise.dev.toml | .env (plain) | No |
| UAT | uat | mise.uat.toml | secrets.uat.env (sops) | Yes |
| Production | production | mise.production.toml | secrets.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.