Skip to main content

App Configuration

Miren uses a convention over configuration approach. Most apps need little configuration: Miren can detect your language and build an image, or run a configured image with its own startup defaults. When you need to customize, you add a .miren/app.toml file.

Minimum working example

The smallest useful .miren/app.toml names the app and its start command:

.miren/app.toml
name = "myapp"

[services.web]
command = "npm start"

Deploy with miren deploy and Miren builds the image and runs web with that command. Everything else on this page is additive: environment variables, more services, scaling, disks.

Have an agent inspect your app

Install the Miren agent skills and ask your AI coding agent to "set up this app on Miren." The app-setup skill can inspect the repository, identify the services and environment variables it needs, and prepare a working .miren/app.toml. Use the rest of this page when you want to review or customize the result.

When You Don't Need app.toml

If your app is a single web service with a standard language stack, Miren handles everything:

  • Language and build: Detected from your project files (package.json, go.mod, Gemfile, etc.) — see Language Guides
  • Start command: Detected from your framework or Procfile
  • Scaling: Web services autoscale based on traffic by default

You can deploy with just:

miren init
miren deploy

What miren init Does for You

miren init does more than scaffold a config file. It scans your project for the environment variables your app actually needs to boot, splits them into things it can handle for you and things it can't, and stages whatever it can find.

Detection

For each supported stack (Python, Node.js, Bun, Go, Ruby, Rust), miren init:

  • Reads your manifest (Gemfile, package.json, pyproject.toml, Cargo.toml, go.mod) to map known libraries to the env vars they typically expect — pgDATABASE_URL, @sentry/nodeSENTRY_DSN, and so on.
  • Greps your source code for direct env reads (ENV['X'], process.env.X, os.Getenv("X"), std::env::var("X"), Bun.env.X) and notes whether each one has a fallback.
  • Parses any .env.sample / .env.example files in the repo as a declaration of what's expected.
  • Recognizes framework-specific names like RAILS_ENV, NODE_ENV, RUST_LOG, and RAILS_MASTER_KEY.

Each detected variable gets a confidence: required, recommended, or optional. A direct source reference without a fallback is required. Library-based guesses are recommended unless the source confirms them, in which case they're elevated. Variables with a default-valued fallback in code (process.env.X ?? "...", cmp.Or(os.Getenv("X"), "...")) are optional.

Staging

For required variables, miren init tries to handle them automatically:

  • Has a sensible default (e.g. RAILS_ENV=production) → written to app.toml so it's visible.
  • Can be generated (e.g. Rails SECRET_KEY_BASE) → a cryptographically random value is generated and pre-set on the app, the same as if you'd run miren config set yourself.
  • Can be read from a local file (e.g. RAILS_MASTER_KEY from config/master.key or config/credentials/production.key) → read from disk and pre-set on the app, again the same as miren config set.
  • Anything else → listed as "must be configured manually" with miren config set.

Pre-set values are picked up by your first miren deploy automatically, so generated secrets and read-in keys are present from the very first build without an extra step.

Sensitive variables marked as such (whether by detection or because the key looks like a secret) are masked in CLI output and never written to app.toml in plaintext.

When You Need app.toml

Create .miren/app.toml when you need to:

  • Run multiple services — web server plus workers, databases, or caches
  • Set environment variables — configuration your app reads at runtime
  • Tune scaling — adjust concurrency thresholds or use fixed instance counts
  • Attach persistent disks — for databases or file storage
  • Run an existing image — deploy a prebuilt container without a Dockerfile or source build
  • Customize builds — specify a Dockerfile, language version, or extra build steps
  • Configure addons — managed databases and other backing services (see Addons)

Configuration Sections

Here's how the sections of app.toml map to your application's lifecycle:

Build

The [build] section controls how Miren builds your container image. Override the detected language version, point to a custom Dockerfile, or add post-build steps.

[build]
version = "3.12"
onbuild = ["npm run build"]

See Language Guides for build details per language.

Services

The [services.<name>] sections define the processes your app runs. Each service gets its own command, scaling configuration, and optionally its own container image.

[services.web]
command = "node server.js"

[services.worker]
command = "node worker.js"

To deploy an existing image as the app, set image on the web service. Miren uses it directly when no Dockerfile is configured:

[services.web]
image = "ghcr.io/example/myapp:latest"

The optional args array replaces the image's CMD while preserving its ENTRYPOINT. Miren passes the array directly, without shell expansion. Leave both args and command unset to use the image's defaults unchanged; use command for the existing full /bin/sh -c override. Miren also inherits a single TCP port from the image's EXPOSE metadata. Set port when the image exposes no ports or several.

See Services for patterns like running databases alongside your app.

Scaling

Each service has a [services.<name>.concurrency] section that controls how it scales. Web services default to autoscaling; everything else defaults to a single fixed instance.

[services.web.concurrency]
mode = "auto"
requests_per_instance = 20

[services.worker.concurrency]
mode = "fixed"
num_instances = 3

See Application Scaling for tuning guidance.

Persistent Storage

Services can attach disks for data that needs to survive restarts. Disks use exclusive leasing and require fixed concurrency with a single instance.

[services.db.concurrency]
mode = "fixed"
num_instances = 1

[[services.db.disks]]
name = "postgres-data"
mount_path = "/var/lib/postgresql/data"
size_gb = 20

See Persistent Storage for local shared storage and Miren Disks.

Environment Variables

Environment variables are declared with [[env]] at the top level (available to all services) or [[services.<name>.env]] for a specific service. Service-level env vars are merged with global ones.

# Available to all services
[[env]]
key = "DATABASE_URL"
value = "postgres://db.app.miren:5432/myapp"

# Only for the worker service
[[services.worker.env]]
key = "WORKER_CONCURRENCY"
value = "5"

Env Var Metadata

Each env var supports optional metadata fields for documentation and validation:

[[env]]
key = "API_KEY"
value = ""
required = true
sensitive = true
description = "Third-party API key for payment processing"
FieldTypeDescription
keystringVariable name (required)
valuestringVariable value
backendstringSecret backend to source the value from, instead of value — see Secrets
refstringReference to the secret within that backend
requiredboolIf true, deploy will fail when this variable has no value
sensitiveboolIf true, the value is masked in CLI output and logs
descriptionstringHuman-readable explanation of what this variable is for

The required flag is useful for variables whose values differ per environment—declare them in app.toml with an empty value and required = true, then set the actual value with miren env set before deploying. The sensitive flag ensures secrets aren't accidentally exposed in terminal output.

Referencing a Secret

For a real credential, sensitive is not enough: it masks display but the value still sits in your config. Use backend and ref to point at a secret instead, so app.toml holds only a pointer and stays safe to commit:

[[env]]
key = "STRIPE_API_KEY"
backend = "cluster"
ref = "payments/stripe-key"

Set value or ref, never both. A referenced variable is sensitive automatically.

Variables Miren Injects

Miren injects these automatically. You don't declare them, and your app can read them to find out what it is:

VariableValueInjected
MIREN_RUNTIME_APPThe app nameEvery sandbox
MIREN_RUNTIME_VERSIONThe deployed version, e.g. v1Every sandbox
MIREN_RUNTIME_INSTANCE_NUMThis instance's number, starting at 0. See Scaling.Instance-backed sandboxes only

Sandboxes also get PORT (the port your web service should listen on) and the workload identity variables.

Injected vs. CLI variables

MIREN_RUNTIME_* is the namespace Miren injects into your app; the MIREN_IDENTITY_* workload identity variables are injected too. Most other MIREN_* variables — such as MIREN_CLUSTER and MIREN_CONFIG — are input to the miren CLI, used to pick what a command acts on (see CI/CD Deployment).

MIREN_APP is both: an input to the CLI, and — for a transition window — a deprecated injected alias of MIREN_RUNTIME_APP (along with MIREN_VERSION and MIREN_INSTANCE_NUM), so apps still reading the old names keep working. The two roles no longer collide: when the CLI runs inside a sandbox and the injected MIREN_APP matches the sandbox's own app, it ignores that value and resolves from your .miren/app.toml. Read the MIREN_RUNTIME_* names for injected runtime values; the aliases will be removed in a future release.

The MIREN_ prefix is reserved

You can't set your own variables under MIREN_miren env set MIREN_FOO=bar fails with cannot set MIREN_ environment variables. Miren owns the whole namespace so it can add injected variables without colliding with yours. Name your own APP_* or anything else.

Traffic Routing

For HTTP services, Miren handles routing automatically. For non-HTTP services (TCP/UDP), you can expose ports directly using the ports array:

[services.irc]
command = "./ircd"

[[services.irc.ports]]
port = 6667
name = "irc"
type = "tcp"
node_port = 6667

See Traffic Routing for the full picture — HTTP ingress, TCP/UDP routing, multi-port services, and the PORT environment variable.

Complete Example

name = "myapp"

[[env]]
key = "DATABASE_URL"
value = "postgres://user:pass@postgres.app.miren:5432/myapp"

[[env]]
key = "SECRET_KEY"
required = true
sensitive = true
description = "Application secret for session signing"

[build]
version = "3.12"

[services.web]
command = "gunicorn app:app --bind 0.0.0.0:8000"
port = 8000

[services.web.concurrency]
mode = "auto"
requests_per_instance = 20

[services.worker]
command = "celery -A app worker"

[services.worker.concurrency]
mode = "fixed"
num_instances = 2

[services.postgres]
image = "postgres:16"

[[services.postgres.env]]
key = "PGDATA"
value = "/var/lib/postgresql/data/pgdata"

[services.postgres.concurrency]
mode = "fixed"
num_instances = 1

[[services.postgres.disks]]
name = "myapp-pgdata"
mount_path = "/var/lib/postgresql/data"
size_gb = 20

Reference

For a complete field-by-field listing of every app.toml option, see the app.toml Reference.