# Miren Docs
> Enjoy the Deploy
This file contains all documentation content in a single document following the llmstxt.org standard.
## Addons
Addons are managed services that Miren provisions and operates for your app. Instead of running your own database as a service, you configure an addon, and Miren handles the infrastructure — creating the server, injecting connection credentials, and cleaning up when you're done.
## Addons vs. Services
| | Addons | Services |
|---|--------|----------|
| **Setup** | One line in app.toml | Configure image, env, disks, scaling |
| **Management** | Miren provisions and manages | You manage the process |
| **Credentials** | Automatically injected as env vars | You configure manually |
| **Best for** | Production databases, managed infrastructure | Custom software, full control |
If you just need a PostgreSQL database for your app, use an addon. If you need custom PostgreSQL extensions or full control over the configuration, run it as a [service](/services).
## Available Addons
| Addon | Description | Supported Versions | Default |
|-------|-------------|--------------------|---------|
| `miren-postgresql` | Managed PostgreSQL database | 14, 15, 16, 17, 18 | 18 |
| `miren-mysql` | Managed MySQL database | 8, 9 | 9 |
| `miren-valkey` | Managed Valkey key-value store (Redis-compatible) | 7, 8, 9 | 9 |
| `miren-rabbitmq` | Managed RabbitMQ message broker | 3, 4 | 4 |
| `miren-memcache` | Managed Memcached in-memory cache | 1.4, 1.5, 1.6 | 1.6 |
List available addons on your cluster:
```miren
miren addon list-available
```
## Installing an Addon
### Via app.toml (recommended)
Declare addons in your `.miren/app.toml`. They're provisioned automatically on deploy:
```toml
name = "myapp"
[services.web]
command = "npm start"
[addons.miren-postgresql]
variant = "small"
```
### Via CLI
Attach an addon to an existing app:
```miren
miren addon create miren-postgresql:small -a myapp
```
## Version Selection
Each addon uses a default software version (e.g., PostgreSQL 17). You can choose a different version when installing an addon.
### Via app.toml
```toml
[addons.miren-postgresql]
variant = "small"
version = "16"
```
### Via CLI
```miren
miren addon create miren-postgresql:small -a myapp --version 16
```
The `version` value is typically a tag from the [supported versions](#available-addons) listed above (e.g., `16` or `17`).
If no version is specified, the addon's default version is used.
Miren validates that the image is accessible in its registry before provisioning begins. If the image cannot be found, the `addon create` or deploy will fail immediately with a clear error.
### Custom images
If you need a custom build of the addon software (e.g., with additional extensions or patches), you can set `version` to a full OCI image reference:
```toml
[addons.miren-postgresql]
variant = "small"
version = "registry.example.com/custom-postgres:16-custom"
```
When the version value contains `:`, Miren uses it as the complete image reference instead of appending it as a tag to the addon's default base image. The image must be accessible from the cluster at provisioning time.
## Environment Variables
When an addon is provisioned, Miren injects connection credentials as environment variables into your app. These are available to all services in the app. Variables marked sensitive are redacted in `miren env list` output.
### PostgreSQL (`miren-postgresql`)
| Variable | Description | Example |
|----------|-------------|---------|
| `DATABASE_URL` | Full connection URL (sensitive) | `postgres://user:pass@host:5432/dbname` |
| `PGHOST` | Database hostname | `10.10.0.196` |
| `PGPORT` | Database port | `5432` |
| `PGUSER` | Database username | `myapp` |
| `PGPASSWORD` | Database password (sensitive) | — |
| `PGDATABASE` | Database name | `myapp` |
Most frameworks and ORMs connect automatically using `DATABASE_URL`.
### MySQL (`miren-mysql`)
| Variable | Description | Example |
|----------|-------------|---------|
| `DATABASE_URL` | Full connection URL (sensitive) | `mysql://user:pass@host:3306/dbname` |
| `MYSQL_HOST` | Database hostname | `10.10.0.196` |
| `MYSQL_PORT` | Database port | `3306` |
| `MYSQL_USER` | Database username | `myapp` |
| `MYSQL_PASSWORD` | Database password (sensitive) | — |
| `MYSQL_DATABASE` | Database name | `myapp` |
### Valkey (`miren-valkey`)
Valkey is wire-compatible with Redis, so Miren injects both `VALKEY_*` and `REDIS_*` variables pointing at the same server. Use whichever your client library expects.
| Variable | Description | Example |
|----------|-------------|---------|
| `VALKEY_URL` / `REDIS_URL` | Full connection URL (sensitive) | `redis://:pass@host:6379` |
| `VALKEY_HOST` / `REDIS_HOST` | Server hostname | `10.10.0.196` |
| `VALKEY_PORT` / `REDIS_PORT` | Server port | `6379` |
| `VALKEY_PASSWORD` / `REDIS_PASSWORD` | Password (sensitive) | — |
### RabbitMQ (`miren-rabbitmq`)
| Variable | Description | Example |
|----------|-------------|---------|
| `RABBITMQ_URL` | Full AMQP connection URL (sensitive) | `amqp://user:pass@host:5672/%2F` |
| `RABBITMQ_HOST` | Broker hostname | `10.10.0.196` |
| `RABBITMQ_PORT` | Broker port | `5672` |
| `RABBITMQ_USER` | Broker username | `myapp` |
| `RABBITMQ_PASSWORD` | Broker password (sensitive) | — |
| `RABBITMQ_VHOST` | Virtual host | `/` |
### Memcached (`miren-memcache`)
| Variable | Description | Example |
|----------|-------------|---------|
| `MEMCACHE_URL` | Connection URL | `memcache://host:11211` |
| `MEMCACHE_HOST` | Server hostname | `10.10.0.196` |
| `MEMCACHE_PORT` | Server port | `11211` |
### Inspecting injected variables
You can verify the variables are set on your app:
```miren
miren env list -a myapp
```
## Variants
Each addon offers variants that control the resource allocation and architecture:
```miren
miren addon variants miren-postgresql
```
### PostgreSQL & MySQL Variants
PostgreSQL and MySQL each offer two variants:
| Variant | Description | Use case |
|---------|-------------|----------|
| `small` | Dedicated server (1 GB storage) | Production apps needing isolation |
| `shared` | Multi-app shared server | Development, staging, or small apps |
**Dedicated** (`small`): Each app gets its own database instance with dedicated storage. Best for production workloads where you need isolation and predictable performance. Start here if your app might grow.
**Shared** (`shared`): Multiple apps share a single database server, each with their own logical database and credentials. The shared server does not isolate workloads — a heavy query in one app can affect others on the same server. This variant is designed for small internal tools, staging environments, and apps you know will stay lightweight.
### Valkey, RabbitMQ, and Memcached Variants
These addons currently offer only the dedicated `small` variant — each app gets its own server instance.
If no variant is specified, the default (`small`) is used.
:::note Changing variants
There is currently no way to migrate between variants (e.g. upgrading from `shared` to `small`). If you need to switch, you would need to back up your data, destroy the addon, recreate it with the new variant, and restore. We plan to add in-place variant upgrades in a future release.
:::
## Addon Lifecycle
### Provisioning
When you deploy an app with addons or run `addon create`, Miren:
1. Creates the addon association (status: **pending**)
2. Provisions the backing infrastructure (status: **provisioning**)
3. Injects environment variables into your app configuration
4. Marks the addon as ready (status: **active**)
5. Starts your app with the injected credentials
Provisioning typically takes 1–2 minutes for a new dedicated server (longer if the PostgreSQL image needs to be pulled for the first time).
Your app won't start until addon provisioning completes — Miren holds off launching your app's processes until all addons reach active status.
### Checking Status
List addons attached to your app:
```miren
miren addon list -a myapp
```
### Removing an Addon
Remove an addon and delete its data:
```miren
miren addon destroy miren-postgresql -a myapp
```
:::warning
Destroying an addon permanently deletes the database and all its data. This cannot be undone.
:::
To remove an addon via app.toml, delete the `[addons.miren-postgresql]` section and redeploy. Miren detects the removal and deprovisions the addon.
## Example: Bun + PostgreSQL
A simple web server that tracks page visits using PostgreSQL:
**`.miren/app.toml`**:
```toml
name = "my-bun-app"
[services.web]
command = "bun run index.ts"
[addons.miren-postgresql]
variant = "shared"
```
**`index.ts`**:
```typescript
const sql = new SQL({ url: process.env.DATABASE_URL });
await sql`
CREATE TABLE IF NOT EXISTS visits (
id SERIAL PRIMARY KEY,
visited_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
`;
const server = Bun.serve({
port: process.env.PORT || 3000,
async fetch(req) {
await sql`INSERT INTO visits DEFAULT VALUES`;
const [{ count }] = await sql`SELECT COUNT(*) as count FROM visits`;
return new Response(`Visits: ${count}\n`);
},
});
```
Deploy:
```miren
miren deploy
```
Miren provisions PostgreSQL, injects `DATABASE_URL`, and starts your app once the database is ready.
## Backing Up and Restoring
:::info Early Version
Addon backup and restore uses the general-purpose disk backup system. We plan to add addon-aware backup commands in a future release that will simplify this workflow. For now, the steps below work reliably for PostgreSQL addon data.
The `disk backup` and `disk restore` commands must be run directly on the server (via SSH or `miren ssh`), not from your local machine. Remote backup support is planned.
:::
Each PostgreSQL addon stores its data on a Miren disk. You can back up and restore this disk using the `miren disk backup` and `miren disk restore` commands.
### Finding the Disk Name
List disks to find the one belonging to your addon:
```miren
miren debug disk list
```
Addon disks are named with a `pg-` prefix. For dedicated (`small`) addons, the name includes your app name (e.g. `pg-pg-myapp-s...-data`). For shared addons, it starts with `pg-shared-data-`.
### Creating a Backup
Back up the disk to a compressed snapshot file. This must be run on the server:
```miren
miren disk backup -n
```
This creates a timestamped `.miren.zst` file in the current directory. If the disk is currently in use, the backup will be crash-consistent (safe for PostgreSQL, which uses write-ahead logging).
Example:
```miren
miren disk backup -n pg-pg-myapp-sCZDabc123-data
# Output: pg-pg-myapp-sCZDabc123-data-20260324-120000.miren.zst
```
You can specify a custom output path with `-o`:
```miren
miren disk backup -n pg-pg-myapp-sCZDabc123-data -o /backups/myapp-db.miren.zst
```
### Restoring from a Backup
To restore from a backup, provide the snapshot file. This must also be run on the server:
```miren
miren disk restore -s
```
The restore procedure recreates the disk with the original name. If the disk already exists, use `--force` to overwrite:
```miren
miren disk restore -s myapp-db.miren.zst --force
```
To restore to a different disk name:
```miren
miren disk restore -s myapp-db.miren.zst -n new-disk-name
```
After restoring, restart your app to pick up the restored data:
```miren
miren app restart myapp
```
### Backup Recommendations
- **Schedule regular backups** for production databases, especially before destructive operations like `addon destroy`
- **Store backups off-server** — copy snapshot files to external storage
- **Test restores periodically** to verify your backups are valid
- Backups are compressed with zstd and include checksum verification
---
## Admin Interface
:::info Labs Feature
The admin interface is a [labs feature](/labs) and is disabled by default. Enable it with `--labs adminapi` or `MIREN_LABS=adminapi` when starting the server.
:::
The admin interface allows you to expose custom administrative functions in your application that can be called from the CLI or other tooling. This is useful for user management, cache clearing, database operations, and other maintenance tasks.
## How It Works
Your application exposes admin methods via a JSON-RPC 2.0 endpoint at a well-known path. When you run `miren admin`, the CLI:
1. Looks up your app and retrieves the admin token
2. Sends a JSON-RPC request to your app's web service
3. Returns the response (or error) to you
```text
miren admin delete-user user_id=123 --app myapp
|
v
JSON-RPC POST to /.well-known/miren/admin
Authorization: Bearer
{"jsonrpc":"2.0","method":"delete-user","params":{"user_id":"123"},"id":1}
|
v
Your app processes the request and returns a response
```
## Implementing the Admin Endpoint
### Endpoint Requirements
Your web service must expose:
| Requirement | Value |
|-------------|-------|
| Path | `/.well-known/miren/admin` |
| Method | POST |
| Content-Type | `application/json` |
| Protocol | JSON-RPC 2.0 |
### Security
Admin calls are authenticated using a bearer token that Miren generates for your app. Your app receives this token via the `ADMIN_TOKEN` environment variable.
**You must validate this token on every request:**
```go
func authMiddleware(token string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if token != "" {
auth := r.Header.Get("Authorization")
if !strings.HasPrefix(auth, "Bearer ") {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
if strings.TrimPrefix(auth, "Bearer ") != token {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
}
next.ServeHTTP(w, r)
})
}
```
The admin endpoint also receives an `X-Miren-Access` header:
- `internal` — Request is from Miren's admin system (trusted)
- `public` — Request is from an external client (Miren strips any client-provided value)
You can use this header for additional access control if your endpoint is accidentally exposed to the internet.
### JSON-RPC 2.0 Format
Requests follow the standard JSON-RPC 2.0 format:
```json
{
"jsonrpc": "2.0",
"method": "method-name",
"params": {"key": "value"},
"id": 1
}
```
Successful responses:
```json
{
"jsonrpc": "2.0",
"result": {"any": "data"},
"id": 1
}
```
Error responses:
```json
{
"jsonrpc": "2.0",
"error": {
"code": -32001,
"message": "user not found",
"data": {"user_id": "123"}
},
"id": 1
}
```
### Standard Error Codes
| Code | Meaning |
|------|---------|
| -32700 | Parse error (invalid JSON) |
| -32600 | Invalid request |
| -32601 | Method not found |
| -32602 | Invalid params |
| -32603 | Internal error |
| < 0 | Application-specific errors |
## Method Introspection
When you run `miren admin --list`, Miren sends a JSON-RPC request with the reserved method name `$methods` to your admin endpoint. If your app handles this method, it should return an array of objects describing the available admin methods. This is optional — if your app doesn't handle `$methods`, the `--list` command will report an error, but regular method calls still work.
The `$methods` request has no params:
```json
{
"jsonrpc": "2.0",
"method": "$methods",
"id": 1
}
```
Response:
```json
{
"jsonrpc": "2.0",
"result": [
{
"name": "list-users",
"description": "List all users in the system",
"category": "users",
"params": {"limit": "number", "offset": "number"}
},
{
"name": "get-user",
"description": "Get a specific user by ID",
"category": "users",
"params": {"user_id": "string"}
},
{
"name": "clear-cache",
"description": "Clear the application cache",
"category": "maintenance"
}
],
"id": 1
}
```
Method metadata fields:
| Field | Required | Description |
|-------|----------|-------------|
| `name` | Yes | Method name |
| `description` | No | Human-readable description |
| `category` | No | Grouping for display (e.g., "users", "maintenance") |
| `params` | No | Parameter definitions as `{"name": "type"}` |
## Complete Example (Go)
Here's a complete example using the `jsonrpc3` library:
```go
package main
"context"
"log"
"net/http"
"os"
"strings"
"miren.dev/jsonrpc3/go/jsonrpc3"
)
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
adminToken := os.Getenv("ADMIN_TOKEN")
if adminToken == "" {
log.Println("WARNING: ADMIN_TOKEN not set")
}
// Create the admin handler with method definitions
adminMethods := jsonrpc3.NewMethodMap()
// Register admin methods with introspection metadata
adminMethods.Register("list-users", listUsers,
jsonrpc3.WithDescription("List all users in the system"),
jsonrpc3.WithParams(map[string]string{
"limit": "number",
"offset": "number",
}),
jsonrpc3.WithCategory("users"),
)
adminMethods.Register("get-user", getUser,
jsonrpc3.WithDescription("Get a specific user by ID"),
jsonrpc3.WithParams(map[string]string{
"user_id": "string",
}),
jsonrpc3.WithCategory("users"),
)
adminMethods.Register("clear-cache", clearCache,
jsonrpc3.WithDescription("Clear the application cache"),
jsonrpc3.WithCategory("maintenance"),
)
// Create the HTTP handler for JSON-RPC
rpcHandler := jsonrpc3.NewHTTPHandler(adminMethods)
// Wrap with auth middleware
authHandler := authMiddleware(adminToken, rpcHandler)
// Mount admin endpoint at well-known path
http.Handle("/.well-known/miren/admin", authHandler)
log.Printf("Starting server on port %s", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}
func authMiddleware(token string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if token != "" {
auth := r.Header.Get("Authorization")
if !strings.HasPrefix(auth, "Bearer ") {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
if strings.TrimPrefix(auth, "Bearer ") != token {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
}
next.ServeHTTP(w, r)
})
}
// Sample data
var users = map[string]map[string]any{
"user-1": {"id": "user-1", "name": "Alice", "email": "alice@example.com"},
"user-2": {"id": "user-2", "name": "Bob", "email": "bob@example.com"},
}
type listUsersParams struct {
Limit int `json:"limit"`
Offset int `json:"offset"`
}
func listUsers(ctx context.Context, params jsonrpc3.Params, caller jsonrpc3.Caller) (any, error) {
p := listUsersParams{Limit: 10, Offset: 0}
if params != nil {
_ = params.Decode(&p)
}
var result []map[string]any
for _, user := range users {
result = append(result, user)
}
return map[string]any{
"users": result,
"total": len(users),
}, nil
}
type userIDParams struct {
UserID string `json:"user_id"`
}
func getUser(ctx context.Context, params jsonrpc3.Params, caller jsonrpc3.Caller) (any, error) {
var p userIDParams
if params == nil {
return nil, jsonrpc3.NewInvalidParamsError("user_id is required")
}
if err := params.Decode(&p); err != nil {
return nil, jsonrpc3.NewInvalidParamsError("invalid params")
}
if p.UserID == "" {
return nil, jsonrpc3.NewInvalidParamsError("user_id is required")
}
user, ok := users[p.UserID]
if !ok {
return nil, jsonrpc3.NewError(-32001, "user not found", nil)
}
return user, nil
}
func clearCache(ctx context.Context, params jsonrpc3.Params, caller jsonrpc3.Caller) (any, error) {
// Your cache clearing logic here
return map[string]any{
"cleared": true,
}, nil
}
```
## Other Languages
The admin interface is language-agnostic. Any language that can:
1. Handle HTTP POST requests
2. Parse and generate JSON
3. Implement the JSON-RPC 2.0 protocol
can expose admin methods. The key requirements are:
- Listen on `/.well-known/miren/admin`
- Validate the `Authorization: Bearer ` header against `ADMIN_TOKEN`
- Handle JSON-RPC requests and return proper responses
- Optionally implement `$methods` for introspection
### Python Example
```python
from flask import Flask, request, jsonify
app = Flask(__name__)
ADMIN_TOKEN = os.environ.get('ADMIN_TOKEN', '')
@app.route('/.well-known/miren/admin', methods=['POST'])
def admin_endpoint():
# Validate token
auth = request.headers.get('Authorization', '')
if ADMIN_TOKEN and auth != f'Bearer {ADMIN_TOKEN}':
return 'Unauthorized', 401
data = request.json
method = data.get('method')
params = data.get('params', {})
req_id = data.get('id')
# Handle introspection
if method == '$methods':
return jsonify({
'jsonrpc': '2.0',
'result': [
{'name': 'get-stats', 'description': 'Get app statistics'},
],
'id': req_id
})
# Handle your methods
if method == 'get-stats':
return jsonify({
'jsonrpc': '2.0',
'result': {'users': 42, 'requests': 1000},
'id': req_id
})
return jsonify({
'jsonrpc': '2.0',
'error': {'code': -32601, 'message': 'Method not found'},
'id': req_id
})
```
## Calling Admin Methods
Once your app exposes the admin interface, use the CLI to call methods:
```miren
# List available methods
miren admin --list
# Call a method
miren admin get-user user_id=user-1
# Call with complex parameters
miren admin update-config settings='{"debug": true}'
# Output as JSON (for scripting)
miren admin get-stats --json | jq '.total'
```
See [Admin Commands](/command/admin) for full CLI documentation.
## Next Steps
- [Admin Commands](/command/admin) — CLI reference for `miren admin`
- [Services](/services) — Configure your app's web service
- [Getting Started](/getting-started) — Deploy your first app
---
## Agent Skills
You shouldn't have to context-switch out of your editor to deploy an app or check why something's unhealthy. Miren's agent skills let your AI coding agent operate your infrastructure directly — deploy, diagnose, and manage apps without leaving the conversation.
The skills work with [Claude Code](https://claude.ai/code), [Codex](https://github.com/openai/codex), [Amp](https://ampcode.com), [Pi](https://pi.dev), and [OpenCode](https://github.com/opencode-ai/opencode). Source and setup instructions are at [github.com/mirendev/miren-skills](https://github.com/mirendev/miren-skills).
:::note
Skills make these docs faster to act on — your agent can read a page about scaling and immediately run the commands — but the docs remain the authoritative reference. When in doubt, the docs are the source of truth.
:::
## Installation
### Claude Code
```bash
/plugin marketplace add mirendev/miren-skills
/plugin install miren@miren
```
### Codex CLI
```bash
git clone https://github.com/mirendev/miren-skills
cp -r miren-skills/.agents/skills/* ~/.agents/skills/
```
### Amp
From the command palette (`Ctrl+O` in CLI, `Cmd+Shift+P` in VS Code):
```text
skill: add https://github.com/mirendev/miren-skills
```
### Pi
```bash
pi install git:github.com/mirendev/miren-skills
```
### OpenCode
```bash
git clone https://github.com/mirendev/miren-skills
cp -r miren-skills/.agents/skills/* ~/.config/opencode/skills/
```
## What's included
### `use-miren`
The core skill. Once installed, your agent knows how to use the `miren` CLI — it discovers commands via `miren help`, targets clusters with `-C`, and uses `--json` output for reliable parsing. You don't need to teach it anything; just mention Miren and it kicks in.
### `app-setup`
Getting a new app onto Miren means figuring out what it needs — env vars, databases, services, build config — and wiring it all up. This agent does the detective work for you. Point it at your source code and it walks you through the whole setup, from stack detection to a working `.miren/app.toml`.
Try asking:
- "Help me set up this app on Miren"
- "What does this app need to run?"
### `app-health`
Instead of piecing together app status from multiple commands, ask your agent to check on an app. It pulls together service states, deployment history, logs, and diagnostics into a single report with actionable recommendations. Defaults to the app in your current directory.
Try asking:
- "How's this app doing?"
- "Check the health of myapp"
### `cluster-health`
Same idea, but across your whole cluster. Surveys every app and service, then gives you a prioritized breakdown — what's healthy, what needs attention, and what to do about it.
Try asking:
- "How's the cluster looking?"
- "Give me a health check on garden"
## Commands reference
The skills teach agents to discover commands on their own via `miren help`:
```bash
miren help --commands # list all commands
miren help app list # help for a specific command
```
Most commands accept `-C ` to target a specific cluster.
---
## App Configuration
Miren uses a **convention over configuration** approach. Most apps deploy with zero configuration—Miren detects your language, builds your image, and runs it with sensible defaults. When you need to customize, you add a `.miren/app.toml` file.
## 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 [Supported Languages](/languages)
- **Start command**: Detected from your framework or `Procfile`
- **Scaling**: Web services autoscale based on traffic by default
You can deploy with just:
```miren
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 — `pg` → `DATABASE_URL`, `@sentry/node` → `SENTRY_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
- **Customize builds** — specify a Dockerfile, language version, or extra build steps
- **Configure addons** — managed databases and other backing services (see [Addons](/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.
```toml
[build]
version = "3.12"
onbuild = ["npm run build"]
```
See [Supported Languages](/languages) for build details per language.
### Services
The `[services.]` sections define the processes your app runs. Each service gets its own command, scaling configuration, and optionally its own container image.
```toml
[services.web]
command = "node server.js"
[services.worker]
command = "node worker.js"
```
See [Services](/services) for patterns like running databases alongside your app.
### Scaling
Each service has a `[services..concurrency]` section that controls how it scales. Web services default to autoscaling; everything else defaults to a single fixed instance.
```toml
[services.web.concurrency]
mode = "auto"
requests_per_instance = 20
[services.worker.concurrency]
mode = "fixed"
num_instances = 3
```
See [Application Scaling](/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.
```toml
[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](/disks) 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..env]]` for a specific service. Service-level env vars are merged with global ones.
```toml
# 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:
```toml
[[env]]
key = "API_KEY"
value = ""
required = true
sensitive = true
description = "Third-party API key for payment processing"
```
| Field | Type | Description |
|-------|------|-------------|
| `key` | string | Variable name (required) |
| `value` | string | Variable value |
| `required` | bool | If `true`, deploy will fail when this variable has no value |
| `sensitive` | bool | If `true`, the value is masked in CLI output and logs |
| `description` | string | Human-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.
### Traffic Routing
For HTTP services, Miren handles routing automatically. For non-HTTP services (TCP/UDP), you can expose ports directly using the `ports` array:
```toml
[services.irc]
command = "./ircd"
[[services.irc.ports]]
port = 6667
name = "irc"
type = "tcp"
node_port = 6667
```
See [Traffic Routing](/traffic-routing) for the full picture — HTTP ingress, TCP/UDP routing, multi-port services, and the `PORT` environment variable.
## Complete Example
```toml
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](/app-toml).
---
## app.toml Reference
Complete reference for `.miren/app.toml` — the configuration file for Miren applications.
For a guide-style introduction, see [App Configuration](/app-configuration).
## File Structure
```toml
name = "myapp"
post_import = "make db-migrate"
include = ["configs/"]
# Global environment variables
[[env]]
key = "DATABASE_URL"
value = "postgres://db.app.miren:5432/myapp"
# Build configuration
[build]
version = "3.12"
dockerfile = "Dockerfile.miren"
onbuild = ["npm run build"]
# Service definitions
[services.web]
command = "node server.js"
port = 3000
[services.web.concurrency]
mode = "auto"
requests_per_instance = 10
scale_down_delay = "15m"
shutdown_timeout = "10s"
[services.worker]
command = "node worker.js"
[services.worker.concurrency]
mode = "fixed"
num_instances = 2
shutdown_timeout = "10s"
[services.db]
image = "postgres:16"
[[services.db.disks]]
name = "pgdata"
mount_path = "/var/lib/postgresql/data"
size_gb = 20
# Addons
[addons.storage]
variant = "minio"
# CLI Aliases
[aliases]
console = "app run bin/rails console"
tail = "logs app -f"
```
## Top-Level Fields
| Field | Type | Description | Default |
|-------|------|-------------|---------|
| `name` | string | Application name | Inferred from directory name |
| `post_import` | string | Command to run after importing a new version (e.g. database migrations) | — |
| `include` | string[] | Extra files or directories to include in the build context | — |
| `concurrency` | int | **Legacy.** Global concurrency target. Use `[services..concurrency]` instead. | — |
## `[[env]]` — Environment Variables {#env}
Declares environment variables available to all services. Service-level `[[services..env]]` entries are merged with these.
```toml
[[env]]
key = "DATABASE_URL"
value = "postgres://db.app.miren:5432/myapp"
[[env]]
key = "SECRET_KEY"
required = true
sensitive = true
description = "Used for session signing"
```
| Field | Type | Description | Default |
|-------|------|-------------|---------|
| `key` | string | Variable name. **Required.** | — |
| `value` | string | Variable value | `""` |
| `required` | bool | Fail deploy if value is empty | `false` |
| `sensitive` | bool | Mask value in CLI output and logs | `false` |
| `description` | string | Human-readable explanation of this variable | — |
:::note Validation
Every env entry must have a non-empty `key`. If `required` is `true` and `value` is empty at deploy time, the deploy fails.
:::
## `[build]` — Build Configuration {#build}
Controls how Miren builds your container image.
```toml
[build]
version = "3.12"
dockerfile = "Dockerfile.custom"
onbuild = ["npm run build", "npm prune --production"]
alpine_image = "alpine:3.19"
```
| Field | Type | Description | Default |
|-------|------|-------------|---------|
| `version` | string | Language/runtime version (e.g. `"20"` for Node, `"3.12"` for Python) | Detected from project files |
| `dockerfile` | string | Path to a custom Dockerfile | Auto-detected (`Dockerfile.miren` or built-in) |
| `onbuild` | string[] | Commands to run in `/app` after the main build steps | — |
| `alpine_image` | string | Custom Alpine base image for the runtime stage | Built-in default |
## `[services.]` — Service Configuration {#services}
Each named section under `services` defines a process in your app. See [Services](/services) for usage patterns.
```toml
[services.web]
command = "node server.js"
port = 3000
port_name = "http"
port_type = "http"
[services.postgres]
image = "postgres:16"
```
| Field | Type | Description | Default |
|-------|------|-------------|---------|
| `command` | string | Command to run | Image's default entrypoint |
| `port` | int | Port the service listens on (single-port shorthand) | `3000` (web only) |
| `port_name` | string | Named port identifier (single-port shorthand) | Service name |
| `port_type` | string | `"http"` or `"tcp"` (single-port shorthand) | `"http"` |
| `ports` | [[port]](#ports) | Multi-port configuration array | — |
| `port_timeout` | duration | Time to wait for the service to bind its port at startup (e.g. `"60s"`, `"2m"`) | `"15s"` |
| `image` | string | Container image to use instead of the app's built image | App's built image |
| `env` | [[env]](#env) | Service-specific environment variables (same schema as global `[[env]]`) | — |
| `concurrency` | [concurrency](#concurrency) | Scaling configuration | See defaults below |
| `disks` | [[disk]](#disks) | Persistent disk attachments | — |
:::note
You cannot mix the single-port fields (`port`, `port_name`, `port_type`) with the `ports` array on the same service.
:::
### `[services..concurrency]` — Scaling {#concurrency}
Controls how many instances of a service run. See [Application Scaling](/scaling) for tuning guidance.
**Default for `web`:** auto mode, 10 requests per instance, 15m scale-down delay, 10s shutdown timeout.
**Default for all other services:** fixed mode, 1 instance, 10s shutdown timeout.
```toml
# Autoscaling
[services.web.concurrency]
mode = "auto"
requests_per_instance = 10
scale_down_delay = "15m"
shutdown_timeout = "10s"
# Fixed instances
[services.worker.concurrency]
mode = "fixed"
num_instances = 2
shutdown_timeout = "10s"
```
| Field | Type | Description | Default |
|-------|------|-------------|---------|
| `mode` | string | `"auto"` or `"fixed"` | `"auto"` for web, `"fixed"` for others |
| `requests_per_instance` | int | Target concurrent requests per instance (auto mode only) | `10` |
| `scale_down_delay` | duration | Time to wait before removing idle instances (auto mode only) | `"15m"` |
| `num_instances` | int | Exact number of instances to run (fixed mode only) | `1` |
| `shutdown_timeout` | duration | Time to wait for graceful shutdown during redeploy | `"10s"` |
:::note Validation
- `mode` must be `"auto"` or `"fixed"`.
- In **auto** mode: `requests_per_instance` must be non-negative, `scale_down_delay` must be a valid Go duration, and `num_instances` must not be set.
- In **fixed** mode: `num_instances` must be at least 1, and `requests_per_instance` / `scale_down_delay` must not be set.
- `shutdown_timeout` must be a valid Go duration (e.g. `"10s"`, `"30s"`).
:::
### `[[services..ports]]` — Ports {#ports}
Configures network ports for a service. Use this when a service needs multiple ports or non-HTTP protocols. See [Traffic Routing](/traffic-routing) for usage patterns and examples.
```toml
[[services.app.ports]]
port = 3000
name = "http"
type = "http"
[[services.app.ports]]
port = 7000
name = "data"
type = "tcp"
node_port = 7000
```
| Field | Type | Description | Default |
|-------|------|-------------|---------|
| `port` | int | Port your process listens on (1–65535). **Required.** | — |
| `name` | string | Unique name for this port. **Required.** | — |
| `type` | string | `"http"` for web traffic, `"tcp"` for raw TCP, `"udp"` for UDP | `"http"` |
| `node_port` | int | Port to expose on the host machine (1–65535) | (none) |
:::note Validation
- `port` must be between 1 and 65535.
- `name` is required and must be unique within the service.
- `type` must be `"http"`, `"tcp"`, or `"udp"`.
- Each `(port, type)` pair must be unique within the service (`"tcp"` and `"http"` share the TCP transport, so port 8080 with type `"http"` and port 8080 with type `"tcp"` conflict, but port 53 with `"tcp"` and port 53 with `"udp"` are allowed).
- `node_port` must be between 1 and 65535 and unique across the cluster.
:::
### `[[services..disks]]` — Persistent Disks {#disks}
Attaches persistent storage to a service. See [Persistent Storage](/disks) for details on local storage vs. Miren Disks.
```toml
# Local storage (simple, node-local)
[[services.web.disks]]
name = "data"
provider = "local"
mount_path = "/miren/data/local"
# Miren Disk (cloud-synced, experimental)
[[services.db.disks]]
name = "pgdata"
mount_path = "/var/lib/postgresql/data"
size_gb = 20
filesystem = "ext4"
```
| Field | Type | Description | Default |
|-------|------|-------------|---------|
| `name` | string | Unique disk name. **Required.** | — |
| `provider` | string | `"miren"` for cloud-synced disks, `"local"` for node-local storage | `"miren"` |
| `mount_path` | string | Mount point inside the container. **Required.** | — |
| `size_gb` | int | Disk size in gigabytes (required for new miren disks, ignored for local) | — |
| `filesystem` | string | `"ext4"`, `"xfs"`, or `"btrfs"` (miren disks only) | `"ext4"` |
| `read_only` | bool | Mount as read-only | `false` |
| `lease_timeout` | duration | How long to wait when acquiring the exclusive disk lease (miren disks only) | — |
:::note Validation
- `name` and `mount_path` are required.
- `provider` must be `"miren"` (default) or `"local"`.
- For miren disks: `filesystem` must be `ext4`, `xfs`, or `btrfs`; `size_gb` must be non-negative; services **must** use `mode = "fixed"` and `num_instances = 1`.
- `lease_timeout` must be a valid Go duration (e.g. `"30s"`, `"2m"`).
:::
## `[addons.]` — Addons {#addons}
Configures managed backing services. The `` is the addon identifier (e.g. `miren-postgresql`). See [Addons](/addons) for a full guide.
When you deploy, Miren provisions declared addons and injects connection credentials as environment variables before starting your app.
```toml
[addons.miren-postgresql]
variant = "small"
version = "16"
```
| Field | Type | Description | Default |
|-------|------|-------------|---------|
| `variant` | string | Addon variant (e.g. `small`, `shared`) | Addon's default variant |
| `version` | string | Software version tag, or a full image reference if it contains `:` | Addon's default version |
Run `miren addon variants ` to see available variants and `miren addon list-available` to see default versions.
Addons removed from app.toml are automatically deprovisioned on the next deploy.
## `[aliases]` — CLI Aliases {#aliases}
Defines custom shortcuts for frequently-used CLI commands. When you run `miren `, it expands to the full command before execution.
```toml
[aliases]
console = "app run bin/rails console"
tail = "logs app -f"
```
With the above configuration:
- `miren console` expands to `miren app run bin/rails console`
- `miren tail` expands to `miren logs app -f`
Alias names can contain multiple words, which lets you create command namespaces:
```toml
[aliases]
"x tail" = "logs app -f"
"x console" = "app run bin/rails console"
```
Then `miren x tail` and `miren x console` work as shortcuts.
Any extra arguments you pass after the alias name are appended to the expanded command.
:::note Validation
- Each word in the alias name must start with a lowercase letter and contain only lowercase letters, numbers, dashes, and underscores.
- The command string must not be empty.
- Alias names must not shadow built-in commands (e.g. you cannot define an alias named `version` or `app list`).
- Aliases are expanded only once — an alias cannot reference another alias.
:::
## Duration Format
Fields marked as `duration` accept Go duration strings: a sequence of decimal numbers with unit suffixes. Valid units are `s` (seconds), `m` (minutes), `h` (hours).
Examples: `"10s"`, `"2m"`, `"1h30m"`, `"15m"`.
---
## Changelog
All notable changes to Miren Runtime will be documented in this file.
## Unreleased
*main*
---
## v0.9.0
*2026-05-28*
**Breaking Changes**
- **`auth provider add` reshaped into per-type subcommands** - The asymmetric pair of `auth provider add NAME --provider-url ...` (OIDC) and the separate `auth provider add-password NAME ...` is gone, replaced by a single shape: `auth provider add oidc|github|password NAME [flags]`. Migration: prepend `oidc` to existing OIDC commands, and replace `add-password` with `add password` (with a space). The CLI now exposes three types directly instead of an "oidc with optional connector" indirection. ([#817](https://github.com/mirendev/runtime/pull/817))
**Features**
- **Route protection: native GitHub identity provider** - GitHub was the awkward gap in the v0.8.0 route protection story. It has no OIDC endpoint, so the answer was "stand up Dex yourself." Miren now talks to GitHub directly via an embedded Dex connector library, and adding a provider is just `miren auth provider add github my-gh --client-id $ID --client-secret $SECRET --org mirendev:platform,eng`. Org and team membership land in your app as `X-User-Login` and `X-User-Groups` headers. ([#817](https://github.com/mirendev/runtime/pull/817))
**Improvements**
- **`miren doctor server` checks QUIC reachability** - The endpoint probe now exercises QUIC alongside HTTPS/HTTP. A host firewall that allows TCP 8443 but blocks UDP 8443 (a common UFW misconfiguration) used to silently break every external `miren deploy` with all-green doctor output; the new probe surfaces it with a pointed "host firewall may be blocking inbound UDP" message. ([#819](https://github.com/mirendev/runtime/pull/819))
- **`miren server register` restarts the systemd unit for you** - Re-registering against a live cluster used to print a "you must now restart miren server" warning that was easy to miss. The CLI now detects an active `miren.service` and restarts it itself, while fresh `miren install` flows stay quiet because install owns the lifecycle. ([#824](https://github.com/mirendev/runtime/pull/824))
- **Better admin CLI per-method help** - `miren admin --help` (or `-h`) now shows method help instead of erroring with `unknown parameter(s): help`. Calling a method that declares params with no args renders the help block instead of a raw JSON-RPC error. Missing-required and unknown-param errors embed the full method definition so you can see what was expected at the point of failure. ([#823](https://github.com/mirendev/runtime/pull/823))
**Bug Fixes**
- **Fixed `sandbox exec` stdout redirection and short-ID lookup** - `miren sandbox exec -i app cat /data/db > backup.db` used to print the database contents to the terminal because the TTY check only looked at stdin; redirected stdout now stays binary-clean and skips PTY allocation. The same command also now resolves short IDs like `7g7` (as displayed by `sandbox list`) instead of erroring with "no container found". ([#828](https://github.com/mirendev/runtime/pull/828))
- **Fixed deploy panic leaving the server lock stuck for 30 minutes** - An explain-mode deploy could race RPC stream-handler goroutines against the main goroutine closing the status channel, panicking the deploy and leaving the server-side deploy lock held until its TTL. The status channel is now serialized behind a mutex, and a panic-recovery guard releases the lock immediately on crash. ([#822](https://github.com/mirendev/runtime/pull/822))
- **Fixed ephemeral preview deploys scaling under load** - Ephemeral pools followed normal auto-mode scaling, so traffic bursts ratcheted the pool up instead of queuing on the single preview sandbox. Ephemeral pools are now pinned at `desired_instances = 1` and refuse to scale. The same PR fixes an activator race that returned leases with empty URLs (causing httpingress to fail with `unsupported protocol scheme ""`) and bumps the per-request wait cap from 50s to 120s to cover cold image pulls. ([#821](https://github.com/mirendev/runtime/pull/821))
- **Fixed `tls.additional_names` / `tls.additional_ips` rejected in `behind-proxy-http` mode** - The validator refused these fields under `behind-proxy-http`, but they also feed the API server and etcd certs, which exist regardless of ingress mode. Operators had no way to set just the API cert SANs, which broke external `miren deploy` with `leaf cert SAN doesn't match`. ([#820](https://github.com/mirendev/runtime/pull/820))
---
## v0.8.0
*2026-05-20*
**Breaking Changes**
- **Ingress configuration reshaped around named modes** - The ingress and TLS configuration is now organized as three explicit modes — `tls-autoprovision` (default, behavior unchanged), `behind-proxy-http`, and `behind-proxy-https` — with an optional `ingress.address` for custom bind addresses. The legacy `tls.standard_tls` knob is retired (dev environments already used `--self-signed-tls`). One small behavior change in autoprovision mode: requests to raw-IP or localhost Hosts on `:80` now follow the HTTPS redirect like any other request instead of being shortcut to the default route over plain HTTP. Pick `behind-proxy-http` if you want explicit plain-HTTP for a dev workflow. ([#799](https://github.com/mirendev/runtime/pull/799))
**Features**
- **Route protection: shared-password auth** - Protect any route behind a shared password. Run `miren auth provider add-password my-gate`, then `miren route protect blog.example.com --provider my-gate`, and your route gets a login form with a 24h encrypted session cookie on success. Good for staging gates, internal dashboards, and anywhere "the half-dozen people who know the password" is enough. ([#787](https://github.com/mirendev/runtime/pull/787))
- **Route protection: OIDC single sign-on** - Plug your identity provider into a Miren route and the authenticated user's identity arrives at your app as plain HTTP headers like `X-User-Email`. No OAuth library, JWT validation, or callback handler needed in your code. Google, GitLab, and self-hosted Keycloak work directly; GitHub via a Dex federation layer. ([#764](https://github.com/mirendev/runtime/pull/764), [#788](https://github.com/mirendev/runtime/pull/788))
- **Route protection: Web Application Firewall** - Inspect requests for attack payloads before they reach your app. `miren route waf --level N` runs Coraza with the full OWASP Core Rule Set in front of any route, blocking SQL injection, XSS, path traversal, command injection, and friends. Levels 1-4 map to OWASP paranoia levels; level 1 is the right starting point for most apps. When both auth and WAF are on a route, WAF runs first. See the [route protection docs](https://miren.md/route-protect) and the [announcement post](https://miren.dev/blog/route-protection). ([#786](https://github.com/mirendev/runtime/pull/786))
- **Ephemeral deployments** - Deploy a labeled, time-boxed version of your app that lives alongside the active version and is reachable at `.` without touching production traffic. `miren deploy --ephemeral pr-33 --ttl 24h` creates one; `miren app versions` shows what's live, what's ephemeral, and when each expires. Expired versions are cleaned up automatically, and TLS certs provision on first hit so ephemeral subdomains get real certificates instead of the cluster's self-signed fallback. ([#745](https://github.com/mirendev/runtime/pull/745), [#807](https://github.com/mirendev/runtime/pull/807))
- **Smarter `miren init`** - `miren init` now scans your project for the environment variables your app actually needs and pre-sets them on the app before the first deploy, the same as if you'd run `miren config set` yourself. Generated secrets (Rails `SECRET_KEY_BASE`), file-backed keys (`RAILS_MASTER_KEY`), framework defaults, and source-detected reads across Python/Node/Bun/Go/Ruby/Rust are recognized; the ones we can resolve are picked up automatically by the first build. See [What `miren init` Does for You](/app-configuration#what-miren-init-does-for-you). ([#567](https://github.com/mirendev/runtime/pull/567))
**Improvements**
- **Automatic npm/bun installation for mixed-stack apps** - Rails, Django, Go, or Rust apps that ship a `package.json` (or `bun.lock`/`bun.lockb`) now get npm or bun installed onto the base image automatically and `npm install`/`bun install` run as the unprivileged `app` user. Apps that vendor their own `node_modules` still skip the install step. ([#810](https://github.com/mirendev/runtime/pull/810))
- **Per-service `port_timeout` in app.toml** - Services that need longer than the default 15s to bind their port (Prisma migrations on first boot are a classic culprit) can now declare `port_timeout = "120s"` in their service block. Web and worker services in the same app can hold different timeouts so a slow-booting worker doesn't make web fail-fast change too. ([#792](https://github.com/mirendev/runtime/pull/792))
- **Sandbox hostname in `/etc/hosts`** - Each sandbox now gets a real IP→hostname entry in `/etc/hosts` so processes that resolve their own hostname (e.g. Erlang's EPMD) see their sandbox IP instead of loopback. ([#784](https://github.com/mirendev/runtime/pull/784))
- **CLI top-level help grouped by user intent** - `miren --help` now organizes its 30+ top-level commands into five named buckets (Getting started, Monitoring your app, Configuring your app, Client operations, Server operations) so deployers don't have to scan past server-administration commands to find `deploy` and `rollback`. ([#812](https://github.com/mirendev/runtime/pull/812))
- **Honest upload progress and ETA in `miren deploy`** - The "total" in the upload progress line is no longer fictional and the line now shows a time-domain ETA (`eta ~8h 15m`) instead of a fluctuating projected total. ([#804](https://github.com/mirendev/runtime/pull/804))
- **`sandbox exec` accepts a positional sandbox ID** - `miren sandbox exec yKm -- hostname` now works the way you'd expect from copying the ID out of `sandbox list`. ([#800](https://github.com/mirendev/runtime/pull/800))
- **Trustworthy public-address advertising** - The coordinator now trusts per-family netcheck results so an IPv4 success doesn't drop IPv6 reachability, and CGNAT/Tailscale addresses in `100.64.0.0/10` are filtered out of discovered IPs by default. Operators who actually want a CGNAT address advertised can set it explicitly via `AdditionalIPs`. ([#808](https://github.com/mirendev/runtime/pull/808), [#809](https://github.com/mirendev/runtime/pull/809))
- **Better `.gitignore` handling in source uploads** - Switched the source-bundler to go-git's gitignore matcher, which correctly handles negations like Bridgetown's default `!/tmp/pids/` pattern. Vanilla `bridgetown new` apps now deploy without a missing-pidfile crash. ([#777](https://github.com/mirendev/runtime/pull/777))
**Bug Fixes**
- **Fixed containerd FIFO directory leak on sandbox teardown** - Sandbox cleanup paths now attach a non-nil `cio.NewAttach()` when fetching tasks for deletion so containerd actually removes its FIFO directories. The leak was exhausting `/run`'s inode budget on long-lived hosts. ([#797](https://github.com/mirendev/runtime/pull/797))
- **Fixed `miren upgrade --check --channel` ignoring `--channel`** - The version/channel resolution happened after the `--check` early return, so `miren upgrade --check --channel main` was silently reporting against `latest`. Resolution is now shared across `miren upgrade`, `server upgrade`, and `runner upgrade` so they can't drift apart again. ([#798](https://github.com/mirendev/runtime/pull/798))
- **Fixed `miren login` cancel being reported as a timeout** - Hitting Ctrl-C during login now distinguishes cancellation from `context.DeadlineExceeded` and reports it accordingly. ([#773](https://github.com/mirendev/runtime/pull/773))
---
## v0.7.1
*2026-04-22*
**Improvements**
- **Cut steady-state log noise** - Roughly 76% reduction in server log volume during normal operation. Removed per-request auth success logs, redundant controller reconcile lines, heartbeat spam, and other content-free chatter that drowned out useful signal. ([#767](https://github.com/mirendev/runtime/pull/767))
**Bug Fixes**
- **Fixed Valkey addon causing app connection errors** - The valkey sandbox was starting without `--requirepass` while apps received a URL containing the password, so every client got `ERR AUTH called without any password configured`. Password is now baked directly into the server command at provision time. ([#771](https://github.com/mirendev/runtime/pull/771))
- **Fixed short IDs not resolving for several commands** - `sandbox exec`, `logs sandbox`, and `deploy cancel` weren't resolving short IDs like you'd expect from seeing them in other output. Worst case was `deploy cancel` silently no-op'ing while reporting success, leaving the deploy lock stuck until its 30-minute timeout. ([#769](https://github.com/mirendev/runtime/pull/769))
- **Fixed Rust source-only deploys shipping stale binaries** - Changing only source files in a Rust app could silently ship the previously built binary. The workspace crate now force-rebuilds on every deploy while the dependency cache stays intact. ([#768](https://github.com/mirendev/runtime/pull/768))
- **Fixed activator fail-fast killing healthy replacement deploys** - When a crash-looping version was replaced with a fix, the activator counted dead sandboxes from the broken version and fail-fasted the fix before it got to boot a sandbox of its own. Fail-fast accounting is now scoped to the requesting version. ([#766](https://github.com/mirendev/runtime/pull/766))
- **Fixed silent app.toml parse errors during build** - A parse error in `app.toml` used to be logged and discarded, and the build continued as if no config existed — services, env vars, and addons silently gone. Parse errors now fail the build and surface the full diagnostic (file, line, suggestions) to the client. ([#765](https://github.com/mirendev/runtime/pull/765))
---
## v0.7.0
*2026-04-14*
**Breaking Changes**
- **Local shared storage is now opt-in** - Apps that need persistent local storage now declare it explicitly as a disk with `provider = "local"` in app.toml instead of getting an automatic bind mount at `/miren/data/local`. This makes storage dependencies visible to the scheduler, which needs to know about them as we build out multi-node cluster support. If your app has existing data in local storage, Miren detects it automatically, keeps mounting it, and shows a warning during deploy with a link to add the config. No data loss, just a nudge to make the dependency explicit at your own pace. See [Migrating from automatic local storage](https://miren.md/disks#migrating-from-automatic-local-storage) for details. ([#700](https://github.com/mirendev/runtime/pull/700), [#719](https://github.com/mirendev/runtime/pull/719))
**Features**
- **Managed addons** - Miren now provisions and manages backing services alongside your apps. Add a database or cache with `miren addon create`, and Miren handles the container lifecycle, networking, and credential injection. Launch includes PostgreSQL, MySQL, Valkey, Memcache, and RabbitMQ, with version selection and custom OCI image support. ([#688](https://github.com/mirendev/runtime/pull/688), [#706](https://github.com/mirendev/runtime/pull/706), [#720](https://github.com/mirendev/runtime/pull/720), [#726](https://github.com/mirendev/runtime/pull/726), [#727](https://github.com/mirendev/runtime/pull/727), [#743](https://github.com/mirendev/runtime/pull/743), [#755](https://github.com/mirendev/runtime/pull/755), [#758](https://github.com/mirendev/runtime/pull/758), [#760](https://github.com/mirendev/runtime/pull/760))
- **Short entity IDs** - Entities now get short 3-8 character identifiers that work anywhere a full ID does. CLI tables are easier to read and IDs are easy to type from memory. ([#696](https://github.com/mirendev/runtime/pull/696), [#721](https://github.com/mirendev/runtime/pull/721), [#741](https://github.com/mirendev/runtime/pull/741))
- **CLI aliases** - Define custom command shortcuts in `.miren/app.toml` under `[aliases]`. Supports multi-word names and appends extra arguments. ([#693](https://github.com/mirendev/runtime/pull/693))
- **Disk undelete** - Deleted disks are now soft-deleted with a 7-day retention window. `disk undelete` restores them, and `disk list-deleted` shows what's recoverable. ([#694](https://github.com/mirendev/runtime/pull/694))
- **`miren app restart`** - New command to restart an app immediately. Deploys also now reset crash cooldown, so a new deploy isn't blocked by stale backoff from a previous crash. ([#702](https://github.com/mirendev/runtime/pull/702))
**Improvements**
- **Better config error messages** - Typos and validation errors in `.miren/app.toml` now show file location, underline the problem, and suggest corrections with color output. ([#709](https://github.com/mirendev/runtime/pull/709))
- **Faster `app list`** - App list aggregation moved server-side, significantly reducing round trips for clusters with many apps. ([#701](https://github.com/mirendev/runtime/pull/701))
- **Disk-aware pool draining** - Services with disks now drain old pools before starting new ones, preventing data conflicts during deploys. ([#725](https://github.com/mirendev/runtime/pull/725))
- **Hardened embedded etcd** - Defenses against bbolt freelist bloat that could cause etcd to slow down over time. ([#733](https://github.com/mirendev/runtime/pull/733))
- **App name in sandbox list** - `sandbox list` now shows which app each sandbox belongs to. ([#757](https://github.com/mirendev/runtime/pull/757))
- **Dual-stack IP discovery** - Cluster address reporting now discovers both IPv4 and IPv6 public addresses, and TLS certificates include public IP SANs automatically. ([#708](https://github.com/mirendev/runtime/pull/708), [#712](https://github.com/mirendev/runtime/pull/712))
**Bug Fixes**
- **Fixed deploy from subdirectory using wrong source directory** ([#716](https://github.com/mirendev/runtime/pull/716))
- **Friendlier TLS certificate behavior when DNS isn't ready yet** - ACME challenges now back off gracefully instead of burning through rate limits when a domain's DNS isn't fully propagated. ([#710](https://github.com/mirendev/runtime/pull/710), [#730](https://github.com/mirendev/runtime/pull/730))
- **Fixed overlay IP allocator assigning duplicate IPs after restart** ([#707](https://github.com/mirendev/runtime/pull/707))
- **Fixed sandbox hostname resolution for addon sub-containers** ([#728](https://github.com/mirendev/runtime/pull/728))
- **Fixed stale pool reuse when volume/mount config changes** ([#718](https://github.com/mirendev/runtime/pull/718))
- **Self-healing loop-backed volumes after unclean shutdown** - After a SIGKILL, miren now detects and cleans up stale loop devices instead of mounting a second one and corrupting the filesystem. ([#756](https://github.com/mirendev/runtime/pull/756))
- **Fixed release bundle detection for CLI-only installs** ([#759](https://github.com/mirendev/runtime/pull/759))
---
## v0.6.1
*2026-03-24*
**Improvements**
- **Faster system log queries** - `miren logs system` now returns results in under a second instead of ~10 seconds by using VictoriaLogs' native `limit` parameter instead of server-side sorting. ([#681](https://github.com/mirendev/runtime/pull/681))
- **JSON output for more CLI commands** - `debug netdb list`, `debug netdb status`, and `doctor config` now support `--format json`. Added `--json` as a shorthand for `--format json` on all commands that support it. ([#687](https://github.com/mirendev/runtime/pull/687))
**Bug Fixes**
- **Fixed delta deploys failing when cached and changed files share directories** - Deploying after a cached delta could fail with `mkdir: file exists` when both the cached and changed file sets contained the same directory. ([#689](https://github.com/mirendev/runtime/pull/689))
- **Fixed CLI commands ignoring per-app cluster selection** - Commands like `route list`, `sandbox list`, and `app list` now respect the cluster chosen via `cluster switch` in an app directory, instead of silently falling back to the global active cluster. ([#683](https://github.com/mirendev/runtime/pull/683))
**Documentation**
- Added a top-level Deployment docs page covering the full deploy lifecycle. ([#684](https://github.com/mirendev/runtime/pull/684))
---
## v0.6.0
*2026-03-17*
**Features**
- **Disk storage system overhaul** - The disk subsystem has been rewritten with a new Universal mode backed by loop devices, replacing the previous LSVD/NBD architecture. Existing LSVD volumes are migrated automatically on first boot. Includes disk backup/restore support and a new cloud accelerator mode for segment upload/replay. ([#639](https://github.com/mirendev/runtime/pull/639))
- **System logs accessible from the CLI** - `miren logs system` queries server logs directly from the CLI — same interface as app logs, with `--follow`, `--last`, and `--format json` all working. Under the hood, server logs are ingested into VictoriaLogs through a structured handler, so all attributes (module, level, error fields) are preserved and queryable. `miren logs` is also restructured into proper subcommands (`app`, `sandbox`, `build`, `system`) — bare `miren logs` still works as shorthand for `miren logs app`. ([#645](https://github.com/mirendev/runtime/pull/645), [#662](https://github.com/mirendev/runtime/pull/662), [#679](https://github.com/mirendev/runtime/pull/679))
- **Delta file transfer for deploys** - The deploy client now sends a file manifest first; the server compares it against the cached source from the previous deploy and the client only uploads what changed. Subsequent deploys of unchanged code upload nothing. ([#670](https://github.com/mirendev/runtime/pull/670))
- **Wildcard routes** - Route all subdomains of a domain to a single app with `miren route set *.example.com myapp`. Wildcard routes match any subdomain and the bare domain, with exact routes taking priority. TLS certificates are provisioned automatically for each matching subdomain. ([#659](https://github.com/mirendev/runtime/pull/659))
**Improvements**
- **Upload progress bar and caching summary** - Deploy uploads show a real progress bar with percentage and speed. After upload, a summary shows how many files were cached from the previous deploy and estimated time saved. ([#680](https://github.com/mirendev/runtime/pull/680))
- **Eager TLS certificate provisioning** - Adding a route now triggers certificate provisioning immediately in the background. HTTPS connections no longer stall 30–90 seconds on first access. ([#661](https://github.com/mirendev/runtime/pull/661), [#664](https://github.com/mirendev/runtime/pull/664))
- **Deploy progress tracks build steps** - The progress bar now tracks build steps instead of layer downloads, so it stays active on cached-image deploys. ([#665](https://github.com/mirendev/runtime/pull/665))
- **JSON output for log and app commands** - `miren logs` (all subcommands), `miren app`, `miren app status`, and `miren app history` all support `--format json` for scripting and automation. ([#675](https://github.com/mirendev/runtime/pull/675), [#673](https://github.com/mirendev/runtime/pull/673))
- **Group help for CLI discovery** - `miren help --commands` lists all commands with synopses (supports `--format json`). `miren help app.list version sandbox.stop` shows help for multiple commands at once using dot notation. ([#676](https://github.com/mirendev/runtime/pull/676))
- **System requirements check at install** - The installer now verifies at least 4 GB memory and 50 GB storage before proceeding. ([#663](https://github.com/mirendev/runtime/pull/663))
**Bug Fixes**
- **Fixed rapid deploys causing orphaned sandboxes** - Deploying the same app multiple times in quick succession no longer leaves orphaned sandboxes running or stale routing entries. ([#672](https://github.com/mirendev/runtime/pull/672))
- **Fixed disk config silent failures** - Using `size` instead of `size_gb` in disk config no longer leaves sandboxes stuck in PENDING forever; unknown fields now surface as errors. ([#666](https://github.com/mirendev/runtime/pull/666))
- **Fixed app TUI showing wrong concurrency** - The `miren app` details view now correctly shows fixed instance counts instead of "auto" for services with explicit concurrency settings. ([#667](https://github.com/mirendev/runtime/pull/667))
- **Fixed env var file values including trailing newlines** - `KEY=@filepath` now trims trailing line endings from file contents. ([#671](https://github.com/mirendev/runtime/pull/671))
---
## v0.5.0
*2026-03-10*
**Features**
- **Multi-port and non-HTTP services** - Apps can now expose multiple ports with different protocols (TCP, UDP, HTTP) using `[[services..ports]]` in app.toml. Node ports are validated at deploy time to prevent conflicts. Enables use cases like IRC servers alongside HTTP endpoints. ([#641](https://github.com/mirendev/runtime/pull/641))
- **CI deployments with GitHub Actions** - Deploy from GitHub Actions and other CI systems using short-lived identity tokens instead of long-lived secrets. Bind repos to apps with `miren auth ci add --github OWNER/REPO -a APP`, and GitHub Actions will authenticate automatically. Target clusters from CI with the `MIREN_CLUSTER` env var. ([#631](https://github.com/mirendev/runtime/pull/631), [#633](https://github.com/mirendev/runtime/pull/633), [#635](https://github.com/mirendev/runtime/pull/635))
- **Required environment variables** - Declare env vars as `required = true` in app.toml and deploys will fail early with a clear message listing what's missing. Vars can also be marked `sensitive` and given a `description` that appears in `miren env list`. ([#638](https://github.com/mirendev/runtime/pull/638))
**Improvements**
- **Zero-config app initialization** - Running any app command without an `app.toml` now offers to run `miren init` for you interactively, inferring the app name from your directory. In CI, the error message tells you what to do. ([#654](https://github.com/mirendev/runtime/pull/654))
- **Bun runtime detection without lockfile** - Bun apps without dependencies are now detected via `bunfig.toml`, the `packageManager` field in package.json, or `bun`/`bunx` commands in package.json scripts. ([#656](https://github.com/mirendev/runtime/pull/656))
- **`env set`/`env delete` show deployment progress** - Environment variable changes now go through the deployment service and show activation progress and routes, matching the UX of `deploy` and `rollback`. ([#630](https://github.com/mirendev/runtime/pull/630))
**Bug Fixes**
- **Fixed 502 errors during deployment rollovers** - The HTTP ingress now retries on a stale connection instead of immediately returning 502, and the launcher waits for new sandboxes to be ready before scaling down old ones. ([#637](https://github.com/mirendev/runtime/pull/637))
- **Fixed streaming responses being buffered** - SSE, chunked downloads, and long-polling now work correctly. The previous timeout implementation buffered entire responses in memory before sending; timeouts are now handled at the transport level so data streams incrementally. ([#634](https://github.com/mirendev/runtime/pull/634))
- **Fixed `miren exec` panic after command finishes** - `miren exec` and `miren app run` no longer panic when the remote command completes. ([#655](https://github.com/mirendev/runtime/pull/655))
- **Fixed `app run` panic when stdin isn't a terminal** - `miren app run` no longer panics in non-interactive contexts like piped input or CI. ([#632](https://github.com/mirendev/runtime/pull/632))
- **Fixed firewall rules leaking on sandbox teardown** - Port forwarding rules are now cleaned up when sandboxes stop, preventing stale rules from making node ports unreachable after redeployment. ([#644](https://github.com/mirendev/runtime/pull/644))
---
## v0.4.1
*2026-02-18*
**Bug Fixes**
- **Fixed app startup directory regression** - Apps deployed before the WORKDIR fix (v0.4.0) would boot with CWD `/` instead of `/app`, causing crashes. The `/app` default is now restored as a fallback for existing app versions without a stored `start_directory`. ([#621](https://github.com/mirendev/runtime/pull/621), [#623](https://github.com/mirendev/runtime/pull/623))
- **Fixed noisy RPC error logs** - Deploying to a server without telemetry capabilities no longer produces a spurious `[ERROR] error resolving capability` message. Optional capability lookups now degrade quietly at Debug level. ([#622](https://github.com/mirendev/runtime/pull/622))
---
## v0.4.0
*2026-02-17*
**Features**
- **First-class rollbacks** - Redeploy a previous app version without rebuilding. Use `miren rollback` for an interactive picker that shows your recent versions, or `miren deploy --version ` to deploy a specific version directly. Each rollback creates a full deployment record with provenance tracking. ([#590](https://github.com/mirendev/runtime/pull/590))
- **OpenTelemetry tracing** - Comprehensive distributed tracing across the Miren runtime. Traces cover HTTP ingress, deploy/build pipelines, containerd operations, and CLI commands. CLI spans are shipped through the server via a proxy exporter. Cluster identity is included in trace resource attributes. See [Observability](/observability) for configuration details. ([#595](https://github.com/mirendev/runtime/pull/595), [#601](https://github.com/mirendev/runtime/pull/601), [#602](https://github.com/mirendev/runtime/pull/602), [#609](https://github.com/mirendev/runtime/pull/609))
**Improvements**
- **Per-app cluster pinning** - The CLI now remembers which cluster to use for each app. The first time you use `-C ` with a command, that cluster is saved locally for the app. Resolution order: explicit `-C` flag > per-app pin > global active cluster. Use `miren cluster current` to see which cluster the CLI will target. ([#596](https://github.com/mirendev/runtime/pull/596))
- **Better app.toml error messages** - Invalid `app.toml` files now surface the actual parse error instead of the confusing "app is required" message. ([#614](https://github.com/mirendev/runtime/pull/614))
- **Exclude dead sandboxes from listing** - `miren sandbox list` now hides dead sandboxes by default, showing only active ones. Use `--all` to see everything. ([#585](https://github.com/mirendev/runtime/pull/585))
- **Redact secrets from debug bundles** - `miren debug bundle` now redacts sensitive information and includes guidance on sharing bundles safely. ([#612](https://github.com/mirendev/runtime/pull/612))
- **`-v` flag moved to `-V`** - The version flag is now `-V`/`--version`, freeing `-v` for `--verbose` across all commands. ([#593](https://github.com/mirendev/runtime/pull/593), [#594](https://github.com/mirendev/runtime/pull/594))
- **Disk mount robustness** - Improved disk mount lifecycle handling with better error recovery and integration test coverage. ([#597](https://github.com/mirendev/runtime/pull/597))
**Bug Fixes**
- **Fixed Dockerfile WORKDIR ignored** - App containers now honor the `WORKDIR` directive from your Dockerfile instead of always defaulting to `/`. ([#617](https://github.com/mirendev/runtime/pull/617))
- **Fixed nested .gitignore files ignored in deploy** - Deploy tarballs now respect `.gitignore` files in subdirectories, not just the root. This prevents things like `web/node_modules/` from inflating your deploy upload. ([#608](https://github.com/mirendev/runtime/pull/608))
- **Fixed disk space GC** - Garbage collection for BuildKit cache and registry blobs now works correctly, preventing disk space from being consumed by stale build artifacts. ([#588](https://github.com/mirendev/runtime/pull/588))
- **Fixed activator pool cache lockout** - The activator pool cache no longer permanently locks out at MaxPoolSize, which was preventing scale-to-zero apps from recovering after hitting the pool size limit. ([#616](https://github.com/mirendev/runtime/pull/616))
- **Fixed disk lease leak in sandbox cleanup** - Periodic sandbox cleanup now properly releases disk leases before deleting sandboxes. ([#613](https://github.com/mirendev/runtime/pull/613))
- **Fixed empty host in route commands** - `miren route set` and `miren route show` now validate that a host is provided instead of accepting empty strings. ([#591](https://github.com/mirendev/runtime/pull/591))
- **Fixed deployment cluster filter** - Deployments are now correctly filtered by cluster, and `miren app history` defaults are improved. ([#589](https://github.com/mirendev/runtime/pull/589))
---
## v0.3.1
*2026-02-06*
**Features**
- **Build-time environment variables** - Environment variables are now available during the build process, so build commands like `npm run build` can access API keys, database URLs, and other configuration. Variables from `app.toml`, existing config, and `--env`/`--secret` CLI flags are all injected at build time. ([#581](https://github.com/mirendev/runtime/pull/581))
**Improvements**
- **Preserve disk mounts during server restart** - The LSVD disk manager now survives server restarts, keeping disk mounts active. Use `systemctl reload miren` for a soft restart that preserves mounts, or `systemctl restart miren` for a full restart. This significantly reduces disruption when updating the miren server. ([#554](https://github.com/mirendev/runtime/pull/554))
**For existing installations:** To enable this feature, either re-run `sudo miren server install --force` to regenerate the systemd unit file, or manually add the following line to `/etc/systemd/system/miren.service` under the `[Service]` section and run `systemctl daemon-reload`:
```
ExecReload=/bin/kill -USR1 $MAINPID
```
- **Batch env var setting** - Setting multiple environment variables now creates a single app version instead of N intermediate versions. ([#578](https://github.com/mirendev/runtime/pull/578))
- **Smarter deploy coalescing** - Rapid successive deploys are now coalesced so only the latest version is launched, avoiding unnecessary churn from intermediate sandbox pools. ([#579](https://github.com/mirendev/runtime/pull/579))
- **Default app name from app.toml** - CLI commands like `app delete`, `route set`, and `route set-default` now infer the app name from `.miren/app.toml` when not specified. ([#562](https://github.com/mirendev/runtime/pull/562))
- **Clearer scaling display** - Instance counts now show `(auto)` or `(fixed)` suffix, and sandbox pool listings include a MODE column. ([#566](https://github.com/mirendev/runtime/pull/566))
- **Deploy validation for services** - Deploys now fail early with a clear error when no services are defined, instead of silently deploying an unservable app. ([#563](https://github.com/mirendev/runtime/pull/563))
- **Smarter `miren upgrade`** - Upgrade now checks permissions upfront, offers interactive sudo vs user-directory install picker, and supports a `--user` flag for non-root installation. ([#564](https://github.com/mirendev/runtime/pull/564))
**Bug Fixes**
- **Fixed build log retention** - Build logs are now properly persisted when using the central BuildKit daemon, restoring the ability to retrieve build output after a deploy with `miren logs --build`. ([#561](https://github.com/mirendev/runtime/pull/561), [#570](https://github.com/mirendev/runtime/pull/570))
- **Fixed orphaned container shims** - Containerd shims are no longer left behind when app containers crash. ([#577](https://github.com/mirendev/runtime/pull/577))
- **Fixed env vars wiped by app.toml** - Adding the first `[[env]]` entry to `app.toml` no longer silently drops existing environment variables. ([#580](https://github.com/mirendev/runtime/pull/580))
- **Fixed DNS during sandbox startup** - DNS no longer returns empty responses during sandbox startup; unknown source IPs are now lazily resolved via entity store lookup. ([#576](https://github.com/mirendev/runtime/pull/576))
- **Fixed nil panic in `miren env list`** - Running `miren env list` when an app isn't deployed to the current cluster no longer crashes. ([#572](https://github.com/mirendev/runtime/pull/572))
---
## v0.3.0
*2026-01-27*
**Features**
- **Automatic image garbage collection** - Container images are now automatically garbage collected to prevent disk exhaustion. Images are kept if less than 30 days old or within the last 20 releases per app. Garbage collection runs weekly or immediately when disk usage exceeds 80%. ([#544](https://github.com/mirendev/runtime/pull/544))
- **Deploy-time environment variables** - Set environment variables atomically with your deployment using `miren deploy -e KEY=VALUE` or `-s SECRET=value` for sensitive values. Supports reading from files with `@file` syntax and interactive prompts. ([#521](https://github.com/mirendev/runtime/pull/521))
- **Graceful shutdown during redeploy** - Apps now receive proper graceful shutdown time during redeploy instead of being force-killed. Configure per-service with `shutdown_timeout` in app.toml (default 10 seconds). ([#520](https://github.com/mirendev/runtime/pull/520))
- **`miren deploy cancel`** - Cancel stuck in-progress deployments without waiting for the 30-minute lock expiry. ([#517](https://github.com/mirendev/runtime/pull/517))
- **`miren debug bundle`** - New diagnostic command that collects system info, logs, container state, and process info into a tar.gz archive for troubleshooting. ([#531](https://github.com/mirendev/runtime/pull/531))
- **Domain allow list for TLS** - Automatic TLS certificate provisioning is now restricted to domains with explicitly configured routes, preventing certificate issuance for arbitrary domains pointed at the server. ([#542](https://github.com/mirendev/runtime/pull/542))
**Improvements**
- **Better app history display** - The `miren app history` command now shows deployment IDs (useful for `deploy cancel`) and improved status formatting. ([#529](https://github.com/mirendev/runtime/pull/529), [#532](https://github.com/mirendev/runtime/pull/532))
**Bug Fixes**
- **Fixed server restart killing all sandboxes** - Sandbox recovery no longer incorrectly kills all sandboxes when the server restarts. ([#546](https://github.com/mirendev/runtime/pull/546))
- **Fixed disk lease transfers** - Disk leases are now properly released when sandboxes stop, allowing new sandboxes to acquire them. ([#516](https://github.com/mirendev/runtime/pull/516))
- **Fixed sandbox exec issues** - Fixed panic when running `sandbox exec` without a TTY and fixed wrong entrypoint being applied to service containers. ([#515](https://github.com/mirendev/runtime/pull/515), [#518](https://github.com/mirendev/runtime/pull/518))
- **Fixed deployment panic handling** - Panics during deployment are now properly marked as failed instead of leaving deployments stuck. ([#513](https://github.com/mirendev/runtime/pull/513))
- **Fixed WebSocket 502 errors** - WebSocket connections no longer fail with 502 Bad Gateway. ([#507](https://github.com/mirendev/runtime/pull/507))
- **Fixed cluster switch for multi-identity setups** - Improved identity handling and UX when switching between clusters. ([#535](https://github.com/mirendev/runtime/pull/535))
---
## v0.2.1
*2025-12-19*
**Improvements**
- **Improved `miren doctor`** - The diagnostic command now suggests commands for you to run instead of running them automatically. Includes better guidance for config, auth, and server issues, plus clickable routes in app listings. ([#491](https://github.com/mirendev/runtime/pull/491))
- **Smarter install defaults** - `miren server install` and `miren download` now default to the version matching your binary instead of always using `main`. ([#500](https://github.com/mirendev/runtime/pull/500), [#502](https://github.com/mirendev/runtime/pull/502))
**Bug Fixes**
- **Fixed scale-to-zero pool deletion** - Pools for apps at scale-to-zero are no longer prematurely deleted after being idle for an hour, which was causing "pool has reached maximum size" errors when traffic resumed. ([#497](https://github.com/mirendev/runtime/pull/497))
- **Fixed activator cache cleanup** - The activator now proactively cleans up cached pool references when pools are deleted, preventing stale cache errors. ([#498](https://github.com/mirendev/runtime/pull/498))
- **Fixed disk debug commands** - `miren debug disk status` and related commands now correctly parse disk IDs. ([#499](https://github.com/mirendev/runtime/pull/499))
---
## v0.2.0
*2025-12-17*
**Features**
- **`miren app run`** - Run commands in a one-off sandbox with your app's configuration. Great for debugging, migrations, or one-off tasks. ([#489](https://github.com/mirendev/runtime/pull/489))
- **Persistent BuildKit daemon** - Builds are now significantly faster thanks to a persistent BuildKit daemon that maintains layer caching across builds. No more cold starts! ([#490](https://github.com/mirendev/runtime/pull/490))
- **`miren doctor` command** - New diagnostic command to help troubleshoot your Miren setup. Includes `miren doctor apps` to check app status and `miren doctor auth` to verify authentication. ([#484](https://github.com/mirendev/runtime/pull/484))
- **`miren deploy --analyze`** - Preview what Miren will detect about your app before actually building it. Great for understanding how your project will be configured. ([#485](https://github.com/mirendev/runtime/pull/485))
- **Rust and uv support** - Miren now auto-detects Rust projects and Python projects using uv, and builds them appropriately. ([#485](https://github.com/mirendev/runtime/pull/485))
- **Log filtering** - Filter logs by service name with `miren logs --service ` and by content with `miren logs -g `. Also includes a faster chunked log streaming API under the hood. ([#487](https://github.com/mirendev/runtime/pull/487), [#466](https://github.com/mirendev/runtime/pull/466))
- **Debug networking commands** - New `miren debug netdb` commands for inspecting IP allocations and cleaning up orphaned leases. Helpful for advanced troubleshooting. ([#478](https://github.com/mirendev/runtime/pull/478))
**Bug Fixes**
- **Fixed IP address leaks** - Resolved several issues where IP addresses could leak during sandbox lifecycle events, container cleanup, and entity patch failures. ([#479](https://github.com/mirendev/runtime/pull/479))
- **Fixed stale pool reference** - Deleting and recreating an IP pool no longer causes "error acquiring lease" failures. ([#483](https://github.com/mirendev/runtime/pull/483))
- **Fixed LSVD write handling** - LSVD now uses proper Go file writes instead of raw unix calls, improving reliability. ([#477](https://github.com/mirendev/runtime/pull/477))
- **Fixed deployment cancellation race** - Cancelling a deploy with Ctrl-C no longer causes a race condition between the main and UI goroutines. ([#482](https://github.com/mirendev/runtime/pull/482))
- **Fixed authentication bypass** - Local/non-cloud mode now properly requires client certificates. ([#469](https://github.com/mirendev/runtime/pull/469))
- **Fixed entity revision check** - Entity patches no longer incorrectly enforce revision checks when `fromRevision` is 0. ([#470](https://github.com/mirendev/runtime/pull/470))
- **Fixed IPv6 environments** - VictoriaMetrics and VictoriaLogs now listen on IPv6, fixing issues in environments with IPv6 enabled. ([#481](https://github.com/mirendev/runtime/pull/481))
**Documentation**
- Updated system requirements to 4GB RAM and 20GB disk ([#480](https://github.com/mirendev/runtime/pull/480))
- Improved getting started documentation ([#471](https://github.com/mirendev/runtime/pull/471))
- Fixed missing pages in docs sidebar navigation ([#467](https://github.com/mirendev/runtime/pull/467))
---
## v0.1.0
*2025-12-09*
Initial preview release.
---
## CI/CD Deployment
Deploy to Miren from CI/CD pipelines without storing long-lived secrets. Instead of managing API keys or certificates, your CI system presents a short-lived OIDC identity token that Miren validates directly with the identity provider.
GitHub Actions works out of the box. Other OIDC-capable CI systems (GitLab CI, CircleCI, etc.) are supported with manual configuration.
:::info Looking for Route Protection?
This page covers OIDC for **CI/CD deployment authentication** — letting pipelines deploy without stored secrets. For putting single sign-on in front of your **application's HTTP routes**, see [Protecting Routes](/route-protect).
:::
## How It Works
1. **Create an OIDC binding** on your Miren cluster that links an app to an identity provider and subject pattern.
2. **Your CI job runs** and the Miren CLI auto-detects the CI environment (e.g., GitHub Actions sets `ACTIONS_ID_TOKEN_REQUEST_URL`).
3. **The CLI requests a short-lived token** from the CI provider's OIDC endpoint, with the cluster hostname as the audience.
4. **Miren validates the token** by fetching the provider's OIDC discovery document and JWKS keys, then checks that the token's subject and claims match a configured binding.
No secrets are stored in your CI system. The OIDC token is issued fresh for each job and expires in minutes.
:::tip Per-PR Preview Deploys
Pairing OIDC with [Pull Request Environments](/pr-environments) lets each PR get its own short-lived preview URL — see that page for the full workflow.
:::
## Quick Start with GitHub Actions
### Step 1: Get Your Cluster Address
On a machine where you're already authenticated with the cluster, export the cluster address for use in CI:
```miren
miren cluster export-address
```
This outputs a string like:
```
miren.example.com:8443;sha1:a1b2c3d4e5f6...
```
The value includes the cluster address and a TLS certificate fingerprint for verification. Store this as a GitHub Actions secret named `MIREN_CLUSTER`.
### Step 2: Add an OIDC Binding
Create a binding that allows your GitHub repository to deploy:
```miren
miren auth ci myapp --github acme/web-app
```
This creates a binding with:
- **Issuer:** `https://token.actions.githubusercontent.com`
- **Subject pattern:** `repo:acme/web-app:*` (matches all branches and events)
- **Allowed events:** `push,workflow_dispatch` (by default)
You can restrict further — see [Restricting Access](#restricting-access) below.
### Step 3: Add the GitHub Actions Workflow
```yaml
name: Deploy
on:
push:
branches: [main]
permissions:
id-token: write # Required — allows the job to request an OIDC token
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy
uses: mirendev/actions/deploy@main
with:
cluster: ${{ secrets.MIREN_CLUSTER }}
app: myapp
```
The key pieces:
- **`permissions.id-token: write`** — tells GitHub to make an OIDC token available to the job.
- **`mirendev/actions/deploy`** — installs the Miren CLI, connects to the cluster (verifying the TLS certificate fingerprint), and runs `miren deploy`. The `cluster` input takes the value from `miren cluster export-address`.
- The CLI detects `ACTIONS_ID_TOKEN_REQUEST_URL` and `ACTIONS_ID_TOKEN_REQUEST_TOKEN` in the environment, requests an OIDC token with the cluster hostname as the audience, and sends it as a bearer token.
### Alternative: Using a Config File
Instead of `MIREN_CLUSTER`, you can provide a minimal `clientconfig.yaml` with no credentials:
```yaml
active_cluster: production
clusters:
production:
hostname: miren.example.com:8443
```
Commit this to your repository (e.g., as `.miren/clientconfig.yaml`) and set `MIREN_CONFIG: .miren/clientconfig.yaml` in your workflow. The CLI will auto-detect OIDC the same way. `MIREN_CLUSTER` is preferred because it also verifies the TLS certificate fingerprint and requires no file in your repository.
## Restricting Access
The `--github` shorthand provides sensible defaults, but you can tighten access further.
### Subject Patterns
The subject pattern controls which GitHub Actions runs can authenticate. GitHub sets the token subject to a string like `repo:acme/web-app:ref:refs/heads/main`.
The default pattern `repo:acme/web-app:*` matches all refs and event types. To restrict to a specific branch:
```miren
miren auth ci myapp --github acme/web-app \
--subject "repo:acme/web-app:ref:refs/heads/main"
```
Glob patterns are supported. `*` matches any characters **including** `/`, unlike standard path matching. `?` matches a single character.
### Allowed Events
By default, only `push` and `workflow_dispatch` events are permitted. To also allow `pull_request` events:
```miren
miren auth ci myapp --github acme/web-app \
--allowed-events push,workflow_dispatch,pull_request
```
### Allowed Refs
Restrict deployments to specific git refs:
```miren
miren auth ci myapp --github acme/web-app \
--allowed-refs "refs/heads/main,refs/tags/v*"
```
### Claim Conditions
Claim conditions (events and refs) use the same glob pattern syntax as subject patterns. Comma-separated values mean "match any one of these alternatives."
## What OIDC Callers Can Do
OIDC-authenticated callers have a scoped set of permissions. They can:
- **Deploy** — create and manage deployments, deploy new versions
- **Build** — build from source, analyze apps
- **Read logs** — stream and view application logs
- **View app status** — check app info and configuration
- **Report telemetry** — send trace spans
OIDC callers **cannot** perform administrative operations like deleting apps, managing clusters, modifying routes, or accessing other apps.
## Other CI Platforms
For CI systems other than GitHub Actions, use the `--issuer` and `--subject` flags to create a binding:
```miren
miren auth ci myapp \
--issuer https://gitlab.com \
--subject "project_path:acme/web-app:ref_type:branch:ref:main"
```
The CLI auto-detects OIDC tokens in GitHub Actions. For other platforms, you can adapt the following script which replicates what the GitHub Actions do — downloading the CLI, connecting to the cluster, and deploying:
```bash
#!/usr/bin/env bash
set -euo pipefail
# --- Configuration (set these in your CI environment) ---
# MIREN_CLUSTER: cluster address with TLS fingerprint (from `miren cluster export-address`)
# MIREN_APP: application name to deploy
: "${MIREN_CLUSTER:?MIREN_CLUSTER must be set}"
: "${MIREN_APP:?MIREN_APP must be set}"
# 1. Install the Miren CLI
MIREN_VERSION="${MIREN_VERSION:-latest}"
ARCH="$(uname -m)"
case "$ARCH" in
x86_64) ARCH=amd64 ;;
aarch64) ARCH=arm64 ;;
esac
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
if [ "$MIREN_VERSION" = "latest" ]; then
DOWNLOAD_URL="https://api.miren.cloud/assets/release/miren/latest/miren-${OS}-${ARCH}.zip"
else
DOWNLOAD_URL="https://api.miren.cloud/assets/release/miren/${MIREN_VERSION}/miren-${OS}-${ARCH}.zip"
fi
curl -fLO "$DOWNLOAD_URL"
unzip -q "miren-${OS}-${ARCH}.zip"
install -m 755 miren /usr/local/bin/
miren version
# 2. Deploy
# MIREN_CLUSTER and MIREN_APP are read from the environment automatically.
# In GitHub Actions, the CLI auto-detects the OIDC token. For other CI systems,
# you'll need to acquire an OIDC token from your provider and set
# ACTIONS_ID_TOKEN_REQUEST_URL and ACTIONS_ID_TOKEN_REQUEST_TOKEN, or
# pass the token through your platform's native mechanism.
miren deploy
```
Each CI platform has its own way of issuing OIDC tokens — check your provider's documentation for details on obtaining one.
## Managing Bindings
### List Bindings
```miren
miren auth ci list -a myapp
```
Example output:
```
ID PROVIDER ISSUER SUBJECT CONDITIONS
abc123 github https://token.actions.githubusercontent.com repo:acme/web-app:* event_name=push,workflow_dispatch
def456 generic https://gitlab.com project_path:acme/web-*
```
### Remove a Binding
```miren
miren auth ci remove abc123
```
## CLI Reference
### `miren auth ci`
Create an OIDC binding for an application.
| Flag | Description |
|------|-------------|
| `-a, --app` | Application name (required) |
| `--github OWNER/REPO` | GitHub shorthand — sets issuer, subject pattern, and provider automatically |
| `--issuer URL` | OIDC issuer URL (required if `--github` is not used) |
| `--subject PATTERN` | Glob pattern for the token subject claim |
| `--allowed-events EVENTS` | Comma-separated event names to allow (default with `--github`: `push,workflow_dispatch`) |
| `--allowed-refs PATTERN` | Glob pattern for allowed git refs |
| `--description TEXT` | Human-readable description of this binding |
Either `--github` or `--issuer` is required.
### `miren auth ci list`
List OIDC bindings for an application.
| Flag | Description |
|------|-------------|
| `-a, --app` | Application name (required) |
### `miren auth ci remove`
Remove an OIDC binding by ID.
```miren
miren auth ci remove
```
### `miren cluster export-address`
Export a cluster address with its TLS certificate fingerprint, for use with the `MIREN_CLUSTER` environment variable.
```miren
# Export the active cluster
miren cluster export-address
# Export a specific cluster
miren cluster export-address -C my-cluster
```
Output format: `address:port;sha1:fingerprint`
## Environment Variables
| Variable | Description |
|----------|-------------|
| `MIREN_CLUSTER` | Cluster address with optional TLS fingerprint (`address:port;sha1:fingerprint`). The CLI connects directly — no config file needed. Can also be a cluster name from an existing config. |
| `MIREN_APP` | Target application name. Equivalent to `-a myapp` on commands. |
| `MIREN_CONFIG` | Path to a `clientconfig.yaml` file. Alternative to `MIREN_CLUSTER` when you need a config file. |
## Troubleshooting
### "OIDC token request failed" in GitHub Actions
Ensure your workflow has `permissions.id-token: write`. Without this, GitHub does not set the `ACTIONS_ID_TOKEN_REQUEST_URL` environment variable and the CLI cannot request a token.
```yaml
permissions:
id-token: write
contents: read
```
### "OIDC access denied" or token not matching any binding
Check that:
- The subject pattern on the binding matches the token's subject. Use `miren auth ci list` to see the configured pattern.
- GitHub's token subject format is `repo:OWNER/REPO:ref:refs/heads/BRANCH` for push events and `repo:OWNER/REPO:environment:ENVIRONMENT` for environment-triggered runs. Make sure your pattern accounts for this.
- The allowed events include the event type that triggered the workflow (e.g., `push`, `pull_request`).
### CLI not using OIDC (falling back to other auth)
OIDC auto-detection only activates when **all** of these are true:
1. The cluster config has no `identity` field.
2. The cluster config has `cloud_auth: false` (or the field is absent).
3. The environment variables `ACTIONS_ID_TOKEN_REQUEST_URL` and `ACTIONS_ID_TOKEN_REQUEST_TOKEN` are both set.
Using `MIREN_CLUSTER` satisfies conditions 1 and 2 automatically since the auto-created cluster config has no identity or cloud_auth. If you use a `clientconfig.yaml` instead, make sure it doesn't have those fields set.
### Token audience mismatch
The CLI automatically sets the token audience to the cluster hostname. Ensure the address in `MIREN_CLUSTER` (or `hostname` in your config file) exactly matches the server's hostname, including port if non-standard.
### TLS certificate fingerprint mismatch
If you see `TLS certificate fingerprint mismatch`, the server's certificate has changed since you ran `miren cluster export-address`. Re-run the export command and update your `MIREN_CLUSTER` secret with the new value.
---
## miren addon create
Attach an addon to an application
:::note
This command requires the `addons` [labs feature](/labs) to be enabled.
:::
## Usage
```bash
miren addon create [flags]
```
## Arguments
- `spec` — Addon spec (e.g., miren-postgresql:small)
## Flags
- `--version, -V` — Software version or full image reference (e.g., 16, or registry.example.com/postgres:16-custom)
## Config Options
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
## App Options
- `--app, -a` — Application name
- `--dir, -d` — Directory to run from (default: `.`)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Attach a PostgreSQL addon:**
```bash
miren addon create miren-postgresql:small
```
**Attach a PostgreSQL addon with a specific version:**
```bash
miren addon create miren-postgresql:small --version 16
```
## See also
- [`miren addon`](/command/addon)
---
## miren addon destroy
Remove an addon from an application
:::note
This command requires the `addons` [labs feature](/labs) to be enabled.
:::
## Usage
```bash
miren addon destroy [flags]
```
## Arguments
- `name` — Addon name (e.g., miren-postgresql)
## Flags
- `--force, -f` — Skip confirmation prompt
## Config Options
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
## App Options
- `--app, -a` — Application name
- `--dir, -d` — Directory to run from (default: `.`)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Remove an addon:**
```bash
miren addon destroy miren-postgresql
```
**Remove without confirmation:**
```bash
miren addon destroy miren-postgresql --force
```
## See also
- [`miren addon`](/command/addon)
---
## miren addon list-available
List available addons
:::note
This command requires the `addons` [labs feature](/labs) to be enabled.
:::
## Usage
```bash
miren addon list-available [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--format` — Output format (text, json) (default: `text`)
- `--json` — Shorthand for --format json
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**List available addons:**
```bash
miren addon list-available
```
## See also
- [`miren addon`](/command/addon)
---
## miren addon list
List addons attached to an application
:::note
This command requires the `addons` [labs feature](/labs) to be enabled.
:::
## Usage
```bash
miren addon list [flags]
```
## Flags
- `--format` — Output format (text, json) (default: `text`)
- `--json` — Shorthand for --format json
## Config Options
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
## App Options
- `--app, -a` — Application name
- `--dir, -d` — Directory to run from (default: `.`)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**List addons for the current app:**
```bash
miren addon list
```
## See also
- [`miren addon`](/command/addon)
---
## miren addon variants
Show variants for an addon
:::note
This command requires the `addons` [labs feature](/labs) to be enabled.
:::
## Usage
```bash
miren addon variants [flags]
```
## Arguments
- `addon` — Addon name (e.g., miren-postgresql)
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--format` — Output format (text, json) (default: `text`)
- `--json` — Shorthand for --format json
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Show variants for PostgreSQL:**
```bash
miren addon variants miren-postgresql
```
## See also
- [`miren addon`](/command/addon)
---
## miren addon
Addon management commands
## Usage
```bash
miren addon [flags]
```
## Subcommands
- [`miren addon create`](/command/addon-create) — Attach an addon to an application
- [`miren addon destroy`](/command/addon-destroy) — Remove an addon from an application
- [`miren addon list`](/command/addon-list) — List addons attached to an application
- [`miren addon list-available`](/command/addon-list-available) — List available addons
- [`miren addon variants`](/command/addon-variants) — Show variants for an addon
---
## miren admin
Call an admin method on an application
The admin interface allows you to execute custom administrative functions in your running application—useful for user management, cache clearing, database operations, and other maintenance tasks.
## Listing Methods
Use the `--list` flag to discover what admin methods your application exposes:
```bash
$ miren admin --list
Admin methods for go-admin
clear-cache
│ Clear the application cache
delete-user
│ Delete a user by ID
└ user_id string
get-stats
│ Get application statistics
get-user
│ Get a specific user by ID
└ user_id string
list-users
│ List all users in the system
├ limit number
└ offset number
Usage: miren admin -a go-admin [key=value ...]
```
## Parameter Validation
By default, `miren admin` validates method names and parameters against the application's introspection data before making the call. This catches typos and missing required parameters early.
To skip validation (e.g., if your app doesn't support introspection), use `--no-validate`:
```bash
miren admin --no-validate some-method
```
## Output Formats
The admin command automatically chooses the output format based on context:
- **TTY (terminal)**: Uses a human-friendly pretty format by default
- **Non-TTY (pipes, scripts)**: Uses highlighted JSON by default
You can override this behavior:
```bash
# Force JSON output for scripting
miren admin --json get-stats | jq '.total_users'
# Force pretty output even when piping
miren admin --pretty get-user user_id=123
```
## Error Handling
If the admin call fails, the command exits with a non-zero status and displays the error:
```bash
$ miren admin get-user user_id=nonexistent
admin call failed (code -32001): user not found
```
Error codes follow JSON-RPC conventions:
- `-32700`: Parse error
- `-32600`: Invalid request
- `-32601`: Method not found
- `-32602`: Invalid params
- `-32603`: Internal error
- Custom codes (negative numbers): Application-specific errors
## Usage
```bash
miren admin [args...] [flags]
```
## Arguments
- `method` — Admin method to call
## Flags
- `--func-help` — Show help for a specific admin method
- `--json, -j` — Output as highlighted JSON (default for non-TTY)
- `--list, -l` — List available admin methods
- `--no-validate` — Skip method/parameter validation
- `--params-file, -f` — Read params as JSON from file (use - for stdin)
- `--pretty, -p` — Render output in a human-friendly format (default for TTY)
## Config Options
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
## App Options
- `--app, -a` — Application name
- `--dir, -d` — Directory to run from (default: `.`)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**List available admin methods:**
```bash
miren admin --list -a myapp
```
**Call an admin method:**
```bash
miren admin health -a myapp
```
**Call a method with JSON output:**
```bash
miren admin stats -a myapp --json
```
**Call a method with params from a file:**
```bash
miren admin migrate -a myapp -f params.json
```
---
## miren alias list
List configured CLI aliases
## Usage
```bash
miren alias list [flags]
```
## Flags
- `--format` — Output format (text, json) (default: `text`)
- `--json` — Shorthand for --format json
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren alias`](/command/alias)
---
## miren alias
CLI alias management
## Usage
```bash
miren alias [flags]
```
## Subcommands
- [`miren alias list`](/command/alias-list) — List configured CLI aliases
---
## miren app delete
Delete an application and all its resources
## Usage
```bash
miren app delete [flags]
```
## Arguments
- `appname` — Name of the app to delete
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--force, -f` — Force delete without confirmation
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Delete an app (with confirmation prompt):**
```bash
miren app delete myapp
```
**Delete without confirmation:**
```bash
miren app delete myapp --force
```
## See also
- [`miren app`](/command/app)
---
## miren app history
Show deployment history for an application
## Usage
```bash
miren app history [flags]
```
## Flags
- `--all` — Show all deployments (ignore limit)
- `--detailed` — Show all columns including git information
- `--format` — Output format (text, json) (default: `text`)
- `--hide-failed` — Hide failed deployments
- `--json` — Shorthand for --format json
- `--limit, -n` — Maximum number of deployments to show (default: `10`)
- `--status, -s` — Filter by status (active, failed, rolled_back)
## Config Options
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
## App Options
- `--app, -a` — Application name
- `--dir, -d` — Directory to run from (default: `.`)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Show deployment history:**
```bash
miren app history
```
**Show detailed history with git info:**
```bash
miren app history --detailed
```
**Show only active deployments, limited to 5:**
```bash
miren app history --status active --limit 5
```
## See also
- [`miren app`](/command/app)
---
## miren app list
List all applications
## Usage
```bash
miren app list [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--format` — Output format (text, json) (default: `text`)
- `--json` — Shorthand for --format json
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**List all apps:**
```bash
miren app list
```
**List apps as JSON:**
```bash
miren app list --format json
```
## See also
- [`miren app`](/command/app)
---
## miren app restart
Restart an application
## Usage
```bash
miren app restart [flags]
```
## Flags
- `--service, -s` — Restart only a specific service
## Config Options
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
## App Options
- `--app, -a` — Application name
- `--dir, -d` — Directory to run from (default: `.`)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Restart the current app:**
```bash
miren app restart
```
**Restart a specific service:**
```bash
miren app restart -s web
```
## See also
- [`miren app`](/command/app)
---
## miren app run
Open interactive shell in a new sandbox
This command creates a temporary sandbox using your app's configuration (image, environment variables, working directory) and connects you to an interactive shell. The sandbox is automatically cleaned up when you exit.
This is useful for:
- Debugging application issues in an isolated environment
- Running one-off commands with your app's configuration
- Exploring the container filesystem
- Testing changes before deploying
### How It Works
1. Miren fetches your app's active version configuration
2. Creates an ephemeral sandbox with the same image, environment variables, and working directory as your deployed app
3. Waits for the sandbox to become ready
4. Connects your terminal to an interactive shell inside the sandbox
5. Cleans up the sandbox automatically when you disconnect
:::tip
The ephemeral sandbox runs independently from your production sandboxes. Any changes you make (files created, packages installed) are discarded when you exit.
:::
:::note
If you need to run commands in an existing production sandbox, use `miren sandbox exec` instead.
:::
## Usage
```bash
miren app run [args...] [flags]
```
## Config Options
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
## App Options
- `--app, -a` — Application name
- `--dir, -d` — Directory to run from (default: `.`)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Open a shell in your app's environment:**
```bash
miren app run
```
**Run a specific command:**
```bash
miren app run -- bin/rails console
```
**Run database migrations:**
```bash
miren app run -- bin/rails db:migrate
```
## See also
- [`miren app`](/command/app)
---
## miren app status
Show current status of an application
## Usage
```bash
miren app status [flags]
```
## Flags
- `--format` — Output format (text, json) (default: `text`)
- `--json` — Shorthand for --format json
## Config Options
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
## App Options
- `--app, -a` — Application name
- `--dir, -d` — Directory to run from (default: `.`)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Show status for the current app:**
```bash
miren app status
```
**Show status for a specific app:**
```bash
miren app status -a myapp
```
## See also
- [`miren app`](/command/app)
---
## miren app versions
List app versions with status
## Usage
```bash
miren app versions [flags]
```
## Flags
- `--ephemeral` — Show only ephemeral versions
- `--format` — Output format (text, json) (default: `text`)
- `--json` — Shorthand for --format json
- `--limit, -n` — Max versions to show (default: `20`)
## Config Options
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
## App Options
- `--app, -a` — Application name
- `--dir, -d` — Directory to run from (default: `.`)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**List all versions:**
```bash
miren app versions
```
**List only ephemeral versions:**
```bash
miren app versions --ephemeral
```
## See also
- [`miren app`](/command/app)
---
## miren app
Get information about an application
## Usage
```bash
miren app [flags]
```
## Flags
- `--config-only` — Only show the configuration
- `--format` — Output format (text, json) (default: `text`)
- `--graph, -g` — Graph the app stats
- `--json` — Shorthand for --format json
- `--watch, -w` — Watch the app stats
## Config Options
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
## App Options
- `--app, -a` — Application name
- `--dir, -d` — Directory to run from (default: `.`)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Show app info for the current directory:**
```bash
miren app
```
**Show info for a specific app:**
```bash
miren app -a myapp
```
**Watch app stats in real time:**
```bash
miren app --watch
```
**Show only the app configuration:**
```bash
miren app --config-only
```
## Subcommands
- [`miren app delete`](/command/app-delete) — Delete an application and all its resources
- [`miren app history`](/command/app-history) — Show deployment history for an application
- [`miren app list`](/command/app-list) — List all applications
- [`miren app restart`](/command/app-restart) — Restart an application
- [`miren app run`](/command/app-run) — Open interactive shell in a new sandbox
- [`miren app status`](/command/app-status) — Show current status of an application
- [`miren app versions`](/command/app-versions) — List app versions with status
---
## miren apps
List all applications (alias for 'app list')
## Usage
```bash
miren apps [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--format` — Output format (text, json) (default: `text`)
- `--json` — Shorthand for --format json
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**List all apps:**
```bash
miren apps
```
---
## miren auth ci add
Add a CI authentication binding to an application
## Usage
```bash
miren auth ci add [flags]
```
## Flags
- `--allowed-events` — Comma-separated event names to allow (default: push,workflow_dispatch)
- `--allowed-refs` — Glob pattern for allowed git refs
- `--description` — Human-readable description of this binding
- `--github` — GitHub owner/repo shorthand (sets issuer, subject, provider)
- `--issuer` — OIDC issuer URL
- `--subject` — Glob pattern for the token subject
## Config Options
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
## App Options
- `--app, -a` — Application name
- `--dir, -d` — Directory to run from (default: `.`)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren auth ci`](/command/auth-ci)
---
## miren auth ci list
List CI authentication bindings for an application
## Usage
```bash
miren auth ci list [flags]
```
## Config Options
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
## App Options
- `--app, -a` — Application name
- `--dir, -d` — Directory to run from (default: `.`)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren auth ci`](/command/auth-ci)
---
## miren auth ci remove
Remove a CI authentication binding
## Usage
```bash
miren auth ci remove [flags]
```
## Arguments
- `id` — ID of the CI authentication binding to remove
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren auth ci`](/command/auth-ci)
---
## miren auth ci
CI authentication binding management
## Usage
```bash
miren auth ci [flags]
```
## Subcommands
- [`miren auth ci add`](/command/auth-ci-add) — Add a CI authentication binding to an application
- [`miren auth ci list`](/command/auth-ci-list) — List CI authentication bindings for an application
- [`miren auth ci remove`](/command/auth-ci-remove) — Remove a CI authentication binding
## See also
- [`miren auth`](/command/auth)
---
## miren auth generate
Generate authentication config file
## Usage
```bash
miren auth generate [flags]
```
## Flags
- `--cluster-name, -C` — Name of the cluster (default: `local`)
- `--config-path, -c` — Path to the config file, - for stdout (default: `clientconfig.yaml`)
- `--data-path, -d` — Data path (default: `/var/lib/miren`)
- `--name, -n` — Name of the client certificate (default: `miren-user`)
- `--public-ip, -p` — Use public IP for the target, if available
- `--target, -t` — Hostname to embed in the config (default: `localhost`)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Generate auth config:**
```bash
miren auth generate
```
## See also
- [`miren auth`](/command/auth)
---
## miren auth provider add github
Add a GitHub identity provider
## Usage
```bash
miren auth provider add github [flags]
```
## Arguments
- `name` — Name for this identity provider
## Flags
- `--client-id` — GitHub OAuth app client ID
- `--client-secret` — GitHub OAuth app client secret
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--org` — GitHub org restriction (repeatable). Use "name" for any-member, or "name:team1,team2" to require team membership and populate X-User-Groups.
- `--update` — Overwrite an existing provider with the same name (rotates client secret)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Add a GitHub provider scoped to a team:**
```bash
miren auth provider add github my-gh \
--client-id $CLIENT_ID \
--client-secret $CLIENT_SECRET \
--org mirendev:platform,eng
```
## See also
- [`miren auth provider add`](/command/auth-provider-add)
---
## miren auth provider add oidc
Add an OIDC identity provider
## Usage
```bash
miren auth provider add oidc [flags]
```
## Arguments
- `name` — Name for this identity provider
## Flags
- `--client-id` — OAuth2 client ID
- `--client-secret` — OAuth2 client secret
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--provider-url` — OIDC provider URL (e.g., https://accounts.google.com)
- `--scope` — OAuth2 scopes (can be specified multiple times)
- `--update` — Overwrite an existing provider with the same name (rotates client secret)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Add a Google OIDC provider:**
```bash
miren auth provider add oidc my-google \
--provider-url https://accounts.google.com \
--client-id $CLIENT_ID \
--client-secret $CLIENT_SECRET \
--scope email --scope profile
```
## See also
- [`miren auth provider add`](/command/auth-provider-add)
---
## miren auth provider add password
Add a shared-password identity provider
## Usage
```bash
miren auth provider add password [flags]
```
## Arguments
- `name` — Name for this password provider
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--password` — Password (omit to prompt interactively, use @file to read from file)
- `--update` — Overwrite an existing provider with the same name (rotates password)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Add a password provider:**
```bash
miren auth provider add password my-pw --password hunter2
```
## See also
- [`miren auth provider add`](/command/auth-provider-add)
---
## miren auth provider add
Add an identity provider for route protection
## Usage
```bash
miren auth provider add [flags]
```
## Subcommands
- [`miren auth provider add github`](/command/auth-provider-add-github) — Add a GitHub identity provider
- [`miren auth provider add oidc`](/command/auth-provider-add-oidc) — Add an OIDC identity provider
- [`miren auth provider add password`](/command/auth-provider-add-password) — Add a shared-password identity provider
## See also
- [`miren auth provider`](/command/auth-provider)
---
## miren auth provider list
List identity providers
## Usage
```bash
miren auth provider list [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--format` — Output format (text, json) (default: `text`)
- `--json` — Shorthand for --format json
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren auth provider`](/command/auth-provider)
---
## miren auth provider remove
Remove an identity provider
## Usage
```bash
miren auth provider remove [flags]
```
## Arguments
- `name` — Name of the identity provider to remove
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--force` — Remove the provider even if it is attached to routes
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren auth provider`](/command/auth-provider)
---
## miren auth provider show
Show an identity provider
## Usage
```bash
miren auth provider show [flags]
```
## Arguments
- `name` — Name of the identity provider
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--format` — Output format (text, json) (default: `text`)
- `--json` — Shorthand for --format json
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren auth provider`](/command/auth-provider)
---
## miren auth provider
Identity provider management
## Usage
```bash
miren auth provider [flags]
```
## Subcommands
- [`miren auth provider add`](/command/auth-provider-add) — Add an identity provider for route protection
- [`miren auth provider list`](/command/auth-provider-list) — List identity providers
- [`miren auth provider remove`](/command/auth-provider-remove) — Remove an identity provider
- [`miren auth provider show`](/command/auth-provider-show) — Show an identity provider
## See also
- [`miren auth`](/command/auth)
---
## miren auth
Authentication commands
## Usage
```bash
miren auth [flags]
```
## Subcommands
- [`miren auth ci`](/command/auth-ci) — CI authentication binding management
- [`miren auth generate`](/command/auth-generate) — Generate authentication config file
- [`miren auth provider`](/command/auth-provider) — Identity provider management
---
## miren cluster add
Add a new cluster configuration
## Usage
```bash
miren cluster add [flags]
```
## Flags
- `--address, -a` — Address/hostname of the cluster (optional - will use from selected cluster)
- `--cluster, -c` — Name of the cluster to create (optional - will list available)
- `--force, -f` — Overwrite existing cluster configuration
- `--identity, -i` — Name of the identity to use (optional - will use the only one if single)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Add a cluster interactively:**
```bash
miren cluster add
```
**Add a cluster with a specific address:**
```bash
miren cluster add --cluster my-cluster --address 10.0.0.1:8443
```
## See also
- [`miren cluster`](/command/cluster)
---
## miren cluster current
Show the pinned cluster for this app
## Usage
```bash
miren cluster current [flags]
```
## Config Options
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
## App Options
- `--app, -a` — Application name
- `--dir, -d` — Directory to run from (default: `.`)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Show current cluster:**
```bash
miren cluster current
```
## See also
- [`miren cluster`](/command/cluster)
---
## miren cluster export-address
Export cluster address with TLS fingerprint for MIREN_CLUSTER
## Usage
```bash
miren cluster export-address [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Export active cluster:**
```bash
miren cluster export-address
```
**Export specific cluster:**
```bash
miren cluster export-address -C my-cluster
```
## See also
- [`miren cluster`](/command/cluster)
---
## miren cluster list
List all configured clusters
## Usage
```bash
miren cluster list [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--format` — Output format (text, json) (default: `text`)
- `--json` — Shorthand for --format json
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**List all clusters:**
```bash
miren cluster list
```
**List as JSON:**
```bash
miren cluster list --format json
```
## See also
- [`miren cluster`](/command/cluster)
---
## miren cluster remove
Remove a cluster from the configuration
## Usage
```bash
miren cluster remove [flags]
```
## Arguments
- `cluster` — Name of the cluster to remove
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Remove a cluster:**
```bash
miren cluster remove my-cluster
```
## See also
- [`miren cluster`](/command/cluster)
---
## miren cluster switch
Switch to a different cluster
## Usage
```bash
miren cluster switch [flags]
```
## Arguments
- `cluster` — Name of the cluster to switch to
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Switch to a cluster:**
```bash
miren cluster switch production
```
## See also
- [`miren cluster`](/command/cluster)
---
## miren cluster
List configured clusters
## Usage
```bash
miren cluster [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--format` — Output format (text, json) (default: `text`)
- `--json` — Shorthand for --format json
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**List clusters:**
```bash
miren cluster
```
## Subcommands
- [`miren cluster add`](/command/cluster-add) — Add a new cluster configuration
- [`miren cluster current`](/command/cluster-current) — Show the pinned cluster for this app
- [`miren cluster export-address`](/command/cluster-export-address) — Export cluster address with TLS fingerprint for MIREN_CLUSTER
- [`miren cluster list`](/command/cluster-list) — List all configured clusters
- [`miren cluster remove`](/command/cluster-remove) — Remove a cluster from the configuration
- [`miren cluster switch`](/command/cluster-switch) — Switch to a different cluster
---
## miren config info
Show configuration file locations and format
## Usage
```bash
miren config info [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--format` — Output format (text, json) (default: `text`)
- `--json` — Shorthand for --format json
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Show config info:**
```bash
miren config info
```
## See also
- [`miren config`](/command/config)
---
## miren config load
Load config and merge it with your current config
## Usage
```bash
miren config load [flags]
```
## Flags
- `--config` — Path to the config file to update
- `--force, -f` — Force the update
- `--input, -i` — Path to the input config file to add
- `--set-active, -a` — Set the active cluster
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Load a config file:**
```bash
miren config load --input cluster-config.yaml
```
**Load and set as active cluster:**
```bash
miren config load --input cluster-config.yaml --set-active
```
## See also
- [`miren config`](/command/config)
---
## miren config
Configuration file management
## Usage
```bash
miren config [flags]
```
## Subcommands
- [`miren config info`](/command/config-info) — Show configuration file locations and format
- [`miren config load`](/command/config-load) — Load config and merge it with your current config
---
## miren debug advertise
Show which addresses the server would advertise and why
## Usage
```bash
miren debug advertise [flags]
```
## Flags
- `--additional-ip` — Simulate a server-configured AdditionalIP (repeatable)
- `--cloud-url` — Cloud URL to use for netcheck (default: https://api.miren.cloud)
- `--format` — Output format (text, json) (default: `text`)
- `--json` — Shorthand for --format json
- `--listen` — Simulate the server's listen address (default: 0.0.0.0:8443)
- `--skip-netcheck` — Skip the netcheck call and only report interface scan
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren debug`](/command/debug)
---
## miren debug bundle
Create a support bundle with system debug information
## Usage
```bash
miren debug bundle [flags]
```
## Flags
- `--docker-container, -d` — Docker container name to get logs from (default: `miren`)
- `--namespace` — containerd namespace (default: `miren`)
- `--output, -o` — Output file path (default: `miren-debug.tar.gz`)
- `--since, -s` — Include logs since this time (default: `1 day ago`)
- `--socket` — path to containerd socket
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren debug`](/command/debug)
---
## miren debug colors
Print some colors
## Usage
```bash
miren debug colors [flags]
```
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren debug`](/command/debug)
---
## miren debug connection
Test connectivity and authentication with a server
## Usage
```bash
miren debug connection [flags]
```
## Flags
- `--cluster, -c` — Cluster name from config to test
- `--identity, -i` — Identity name to use for authentication
- `--insecure` — Skip TLS certificate verification
- `--server, -s` — Server hostname or IP address to test directly
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren debug`](/command/debug)
---
## miren debug ctr nuke
Nuke a containerd namespace
## Usage
```bash
miren debug ctr nuke [flags]
```
## Flags
- `--containerd-socket` — path to containerd socket
- `--containers, -c` — nuke containers only
- `--namespace, -n` — namespace to nuke
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren debug ctr`](/command/debug-ctr)
---
## miren debug ctr
Run ctr with miren defaults
## Usage
```bash
miren debug ctr [args...] [flags]
```
## Flags
- `--namespace, -n` — containerd namespace (default: `miren`)
- `--socket` — path to containerd socket
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Subcommands
- [`miren debug ctr nuke`](/command/debug-ctr-nuke) — Nuke a containerd namespace
## See also
- [`miren debug`](/command/debug)
---
## miren debug disk create
Create a disk entity for testing
Disks are normally created automatically when referenced from an app.toml. This command exists to test manual disk creation only.
## Usage
```bash
miren debug disk create [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--created-by, -c` — Creator ID for the disk
- `--filesystem, -f` — Filesystem type (ext4, xfs, btrfs) (default: `ext4`)
- `--name, -n` — Name for the disk
- `--remote-only, -r` — Store disk only in remote storage (no local replica)
- `--size, -s` — Size of disk in GB (default: `10`)
- `--volume-id, -V` — Attach to existing volume instead of creating new one
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren debug disk`](/command/debug-disk)
---
## miren debug disk delete
Delete a disk entity
:::warning
This is a dangerous command. Only disks without bound leases should be deleted. This marks the disk for deletion. The disk controller will clean up the underlying storage. Ensure no apps are using the disk before deletion.
:::
## Usage
```bash
miren debug disk delete [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--id, -i` — Disk ID to delete
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren debug disk`](/command/debug-disk)
---
## miren debug disk lease-delete
Delete a disk lease entity
## Usage
```bash
miren debug disk lease-delete [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--force, -f` — Force deletion without releasing
- `--id, -i` — Lease ID to delete
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren debug disk`](/command/debug-disk)
---
## miren debug disk lease-list
List all disk lease entities
## Usage
```bash
miren debug disk lease-list [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--disk, -d` — Filter by disk ID
- `--sandbox, -s` — Filter by sandbox ID
- `--status` — Filter by status (pending, bound, released, failed)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren debug disk`](/command/debug-disk)
---
## miren debug disk lease-release
Release a disk lease
## Usage
```bash
miren debug disk lease-release [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--id, -i` — Lease ID to release
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren debug disk`](/command/debug-disk)
---
## miren debug disk lease-status
Show detailed status of a disk lease
## Usage
```bash
miren debug disk lease-status [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--id, -i` — Lease ID to check
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren debug disk`](/command/debug-disk)
---
## miren debug disk lease
Create a disk lease for testing
## Usage
```bash
miren debug disk lease [flags]
```
## Flags
- `--app, -a` — App ID for the lease
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--disk, -d` — Disk ID to lease
- `--hours, -H` — Lease duration in hours (default: `2`)
- `--path, -p` — Mount path in sandbox (default: `/data`)
- `--readonly, -r` — Mount as read-only
- `--sandbox, -s` — Sandbox ID for the lease
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren debug disk`](/command/debug-disk)
---
## miren debug disk list
List all disk entities
## Usage
```bash
miren debug disk list [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren debug disk`](/command/debug-disk)
---
## miren debug disk migrate
Migrate LSVD volume to raw disk image
## Usage
```bash
miren debug disk migrate [flags]
```
## Flags
- `--data-path` — Path to LSVD data directory
- `--output, -o` — Output raw disk image path
- `--volume-name` — LSVD volume name
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren debug disk`](/command/debug-disk)
---
## miren debug disk mounts
List all mounted disks from /proc/mounts
## Usage
```bash
miren debug disk mounts [flags]
```
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren debug disk`](/command/debug-disk)
---
## miren debug disk status
Show status of a disk entity
## Usage
```bash
miren debug disk status [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--id, -i` — Disk ID to check
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren debug disk`](/command/debug-disk)
---
## miren debug disk
Disk entity debug commands
Commands for managing Miren disks. These commands are primarily used for troubleshooting and advanced operations.
## Disk Status Values
| Status | Description |
|--------|-------------|
| `provisioning` | Disk is being created and storage is being allocated |
| `provisioned` | Disk is ready and available for lease |
| `attached` | Disk has an active lease and is mounted |
| `detached` | Disk was previously attached but lease was released |
| `deleting` | Disk is marked for deletion |
| `error` | Disk encountered an error during provisioning |
## Lease Status Values
| Status | Description |
|--------|-------------|
| `pending` | Lease is waiting to acquire the disk |
| `bound` | Lease is active and disk is mounted |
| `released` | Lease has been released, cleanup pending |
| `failed` | Lease failed to acquire or mount the disk |
## Troubleshooting
**Disk stuck in "provisioning":**
Check server logs for storage backend errors:
```bash
miren debug disk status -i
```
**Lease stuck in "pending":**
The disk may not be provisioned yet, or another lease may have the disk:
```bash
miren debug disk lease-list -d
```
**App won't start due to disk timeout:**
Increase the `lease_timeout` in your app configuration, or check if another app has an active lease on the disk.
## Usage
```bash
miren debug disk [flags]
```
## Subcommands
- [`miren debug disk create`](/command/debug-disk-create) — Create a disk entity for testing
- [`miren debug disk delete`](/command/debug-disk-delete) — Delete a disk entity
- [`miren debug disk lease`](/command/debug-disk-lease) — Create a disk lease for testing
- [`miren debug disk lease-delete`](/command/debug-disk-lease-delete) — Delete a disk lease entity
- [`miren debug disk lease-list`](/command/debug-disk-lease-list) — List all disk lease entities
- [`miren debug disk lease-release`](/command/debug-disk-lease-release) — Release a disk lease
- [`miren debug disk lease-status`](/command/debug-disk-lease-status) — Show detailed status of a disk lease
- [`miren debug disk list`](/command/debug-disk-list) — List all disk entities
- [`miren debug disk migrate`](/command/debug-disk-migrate) — Migrate LSVD volume to raw disk image
- [`miren debug disk mounts`](/command/debug-disk-mounts) — List all mounted disks from /proc/mounts
- [`miren debug disk status`](/command/debug-disk-status) — Show status of a disk entity
## See also
- [`miren debug`](/command/debug)
---
## miren debug entity create
Create a new entity
## Usage
```bash
miren debug entity create [flags]
```
## Flags
- `--address, -a` — Address to listen on (default: `localhost:8443`)
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--dry-run, -d` — Dry run, do not actually create the entity
- `--id, -i` — ID of the entity (optional, auto-generated if not provided)
- `--path, -p` — Path to the entity file
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren debug entity`](/command/debug-entity)
---
## miren debug entity delete
Delete an entity
## Usage
```bash
miren debug entity delete [flags]
```
## Flags
- `--address, -a` — Address to listen on (default: `localhost:8443`)
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--id, -i` — Entity ID
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren debug entity`](/command/debug-entity)
---
## miren debug entity ensure
Ensure an entity exists
## Usage
```bash
miren debug entity ensure [flags]
```
## Flags
- `--address, -a` — Address to listen on (default: `localhost:8443`)
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--dry-run, -d` — Dry run, do not actually ensure the entity
- `--id, -i` — ID of the entity (required)
- `--path, -p` — Path to the entity file
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren debug entity`](/command/debug-entity)
---
## miren debug entity get
Get an entity
## Usage
```bash
miren debug entity get [flags]
```
## Flags
- `--address, -a` — Address to listen on (default: `localhost:8443`)
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--id, -i` — Entity ID
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren debug entity`](/command/debug-entity)
---
## miren debug entity list
List entities
## Usage
```bash
miren debug entity list [flags]
```
## Flags
- `--address` — Address to listen on (default: `localhost:8443`)
- `--attribute, -a` — Attribute to filter by
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--kind, -k` — Kind of entity to filter by
- `--value, -V` — Value to filter by
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren debug entity`](/command/debug-entity)
---
## miren debug entity patch
Patch an existing entity
## Usage
```bash
miren debug entity patch [flags]
```
## Flags
- `--address, -a` — Address to listen on (default: `localhost:8443`)
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--dry-run, -d` — Dry run, do not actually patch the entity
- `--id, -i` — ID of the entity (required)
- `--path, -p` — Path to the entity file with updates
- `--revision, -r` — Expected revision for optimistic concurrency (default: `0`)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren debug entity`](/command/debug-entity)
---
## miren debug entity put
Put an entity
:::warning
This is an advanced command. Use the higher-level commands like `miren deploy` instead when possible.
:::
## Usage
```bash
miren debug entity put [flags]
```
## Flags
- `--address, -a` — Address to listen on (default: `localhost:8443`)
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--dry-run, -d` — Dry run, do not actually put the entity
- `--id, -i` — ID of the entity
- `--path, -p` — Path to the entity
- `--update, -u` — Update the entity if it exists
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren debug entity`](/command/debug-entity)
---
## miren debug entity replace
Replace an existing entity
## Usage
```bash
miren debug entity replace [flags]
```
## Flags
- `--address, -a` — Address to listen on (default: `localhost:8443`)
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--dry-run, -d` — Dry run, do not actually replace the entity
- `--id, -i` — ID of the entity (required)
- `--path, -p` — Path to the entity file
- `--revision, -r` — Expected revision for optimistic concurrency (default: `0`)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren debug entity`](/command/debug-entity)
---
## miren debug entity
Entity store debug commands
Entities are the low-level objects stored in Miren's entity system. Most users won't need to use these commands directly. They're primarily useful for debugging and advanced use cases.
## What are Entities?
Entities are flexible metadata objects stored in Miren's etcd-backed entity store. Everything in Miren is an entity:
- **Apps** - Application definitions
- **Sandboxes** - Running containers
- **Versions** - Immutable app configurations
- **Clusters** - Cluster registrations
- **Users** - User accounts
## Usage
```bash
miren debug entity [flags]
```
## Subcommands
- [`miren debug entity create`](/command/debug-entity-create) — Create a new entity
- [`miren debug entity delete`](/command/debug-entity-delete) — Delete an entity
- [`miren debug entity ensure`](/command/debug-entity-ensure) — Ensure an entity exists
- [`miren debug entity get`](/command/debug-entity-get) — Get an entity
- [`miren debug entity list`](/command/debug-entity-list) — List entities
- [`miren debug entity patch`](/command/debug-entity-patch) — Patch an existing entity
- [`miren debug entity put`](/command/debug-entity-put) — Put an entity
- [`miren debug entity replace`](/command/debug-entity-replace) — Replace an existing entity
## See also
- [`miren debug`](/command/debug)
---
## miren debug netdb gc
Find and release orphaned IP leases
## Usage
```bash
miren debug netdb gc [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--dry-run, -n` — Show what would be released without making changes
- `--force, -f` — Skip confirmation prompt
- `--subnet, -s` — Only GC IPs in this subnet
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren debug netdb`](/command/debug-netdb)
---
## miren debug netdb list
List all IP leases from netdb
## Usage
```bash
miren debug netdb list [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--format` — Output format (text, json) (default: `text`)
- `--json` — Shorthand for --format json
- `--released, -R` — Show only released IPs
- `--reserved, -r` — Show only reserved (in-use) IPs
- `--subnet, -s` — Filter by subnet CIDR
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren debug netdb`](/command/debug-netdb)
---
## miren debug netdb release
Manually release IP leases
## Usage
```bash
miren debug netdb release [flags]
```
## Flags
- `--all, -a` — Release all reserved IPs (use with caution)
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--force, -f` — Skip confirmation prompt
- `--ip, -i` — Specific IP to release
- `--subnet, -s` — Release all reserved IPs in subnet
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren debug netdb`](/command/debug-netdb)
---
## miren debug netdb status
Show IP allocation status by subnet
## Usage
```bash
miren debug netdb status [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--format` — Output format (text, json) (default: `text`)
- `--json` — Shorthand for --format json
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren debug netdb`](/command/debug-netdb)
---
## miren debug netdb
Network database debug commands
## Usage
```bash
miren debug netdb [flags]
```
## Subcommands
- [`miren debug netdb gc`](/command/debug-netdb-gc) — Find and release orphaned IP leases
- [`miren debug netdb list`](/command/debug-netdb-list) — List all IP leases from netdb
- [`miren debug netdb release`](/command/debug-netdb-release) — Manually release IP leases
- [`miren debug netdb status`](/command/debug-netdb-status) — Show IP allocation status by subnet
## See also
- [`miren debug`](/command/debug)
---
## miren debug rbac test
Test RBAC evaluation with fetched rules
## Usage
```bash
miren debug rbac test [flags]
```
## Flags
- `--action, -a` — Action to test
- `--dir, -d` — Registration directory (default: `/var/lib/miren/server`)
- `--group, -g` — Groups to test with
- `--resource, -r` — Resource to test
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren debug rbac`](/command/debug-rbac)
---
## miren debug rbac
Fetch and display RBAC rules from miren.cloud
## Usage
```bash
miren debug rbac [flags]
```
## Flags
- `--dir, -d` — Registration directory (default: `/var/lib/miren/server`)
- `--raw, -r` — Show raw JSON response
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Subcommands
- [`miren debug rbac test`](/command/debug-rbac-test) — Test RBAC evaluation with fetched rules
## See also
- [`miren debug`](/command/debug)
---
## miren debug reindex
Rebuild all entity indexes from scratch
## Usage
```bash
miren debug reindex [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--dry-run, -d` — Show what would be done without making changes
- `--yes, -y` — Skip confirmation prompt
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren debug`](/command/debug)
---
## miren debug test load
Loadtest a URL
## Usage
```bash
miren debug test load [flags]
```
## Arguments
- `url` — URL to load test
## Flags
- `--accept, -A` — Accept header to use
- `--auth, -a` — Basic auth header to use
- `--concurrency, -c` — Number of concurrent requests to make (default: `50`)
- `--content-type, -T` — Content-Type header to use (default: `text/html`)
- `--cpus` — Number of CPUs to use
- `--data, -d` — HTTP request body
- `--data-file, -D` — File to use as request body
- `--disable-compression` — Disable compression
- `--disable-keepalives` — Disable keep-alives
- `--disable-redirects` — Disable redirects
- `--duration, -z` — Duration of the test (default: `0s`)
- `--h2` — Use HTTP/2
- `--header, -H` — HTTP header to use
- `--host` — Host header to use
- `--method, -m` — HTTP method to use (default: `GET`)
- `--output, -o` — Output type, the only supported value is 'csv'
- `--proxy, -x` — Proxy URL to use
- `--requests, -n` — Number of requests to make (default: `200`)
- `--timeout, -t` — Timeout for each request in seconds (default: `20`)
- `--user-agent, -U` — User-Agent header to use
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren debug test`](/command/debug-test)
---
## miren debug test
Debug test commands
## Usage
```bash
miren debug test [flags]
```
## Subcommands
- [`miren debug test load`](/command/debug-test-load) — Loadtest a URL
## See also
- [`miren debug`](/command/debug)
---
## miren debug
Debug and troubleshooting commands
## Usage
```bash
miren debug [flags]
```
## Subcommands
- [`miren debug advertise`](/command/debug-advertise) — Show which addresses the server would advertise and why
- [`miren debug bundle`](/command/debug-bundle) — Create a support bundle with system debug information
- [`miren debug colors`](/command/debug-colors) — Print some colors
- [`miren debug connection`](/command/debug-connection) — Test connectivity and authentication with a server
- [`miren debug ctr`](/command/debug-ctr) — Run ctr with miren defaults
- [`miren debug disk`](/command/debug-disk) — Disk entity debug commands
- [`miren debug entity`](/command/debug-entity) — Entity store debug commands
- [`miren debug netdb`](/command/debug-netdb) — Network database debug commands
- [`miren debug rbac`](/command/debug-rbac) — Fetch and display RBAC rules from miren.cloud
- [`miren debug reindex`](/command/debug-reindex) — Rebuild all entity indexes from scratch
- [`miren debug test`](/command/debug-test) — Debug test commands
---
## miren deploy cancel
Cancel an in-progress deployment
## Usage
```bash
miren deploy cancel [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--deployment, -d` — ID of the deployment to cancel
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Cancel the current deployment:**
```bash
miren deploy cancel
```
**Cancel a specific deployment:**
```bash
miren deploy cancel -d dep_abc123
```
## See also
- [`miren deploy`](/command/deploy)
---
## miren deploy
Deploy an application
## Usage
```bash
miren deploy [flags]
```
## Flags
- `--analyze` — Analyze the app without building (show detected stack, services, etc.)
- `--env, -e` — Set environment variable (KEY=VALUE, KEY=@file, or KEY to prompt)
- `--ephemeral` — Deploy as ephemeral preview with this label (e.g. feat-login)
- `--explain, -x` — Explain the build process
- `--explain-format` — Explain format (default: `auto`) (choices: `auto`, `plain`, `tty`, `rawjson`)
- `--force, -f` — Skip confirmation prompt
- `--sensitive, -s` — Set sensitive environment variable (masked in output)
- `--ttl` — TTL for ephemeral version (e.g. 48h) (default: `24h`)
- `--version, -V` — Deploy an existing version (skip build)
## Config Options
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
## App Options
- `--app, -a` — Application name
- `--dir, -d` — Directory to run from (default: `.`)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Basic:**
```bash
miren deploy
```
**Analyze:**
```bash
Before deploying, the system can tell you how it's going
to treat your application by running:
miren deploy --analyze
```
**Set environment variables during deploy:**
```bash
miren deploy -e DATABASE_URL=postgres://localhost/mydb
```
**Deploy a previously built version:**
```bash
miren deploy --version v3
```
## Subcommands
- [`miren deploy cancel`](/command/deploy-cancel) — Cancel an in-progress deployment
---
## miren disk backup
Backup a disk to a snapshot file
## Usage
```bash
miren disk backup [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--data-path` — Path to miren data directory (default: `/var/lib/miren`)
- `--name, -n` — Disk name to backup
- `--output, -o` — Output snapshot path (default: DISK-YYYYMMDD-HHMMSS.miren.zst)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren disk`](/command/disk)
---
## miren disk list-deleted
List deleted disks available for recovery
## Usage
```bash
miren disk list-deleted [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--data-path` — Path to miren data directory (default: `/var/lib/miren`)
- `--format` — Output format (text, json) (default: `text`)
- `--json` — Shorthand for --format json
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren disk`](/command/disk)
---
## miren disk restore
Restore a disk from a snapshot file
## Usage
```bash
miren disk restore [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--data-path` — Path to miren data directory (default: `/var/lib/miren`)
- `--force, -f` — Overwrite existing disk image without confirmation
- `--name, -n` — Disk name to restore to (default: original name from snapshot)
- `--snapshot, -s` — Path to snapshot file
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren disk`](/command/disk)
---
## miren disk undelete
Restore a recently deleted disk
## Usage
```bash
miren disk undelete [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--data-path` — Path to miren data directory (default: `/var/lib/miren`)
- `--name, -n` — Disk name to undelete
- `--volume-id, -V` — Volume ID to restore (when multiple deleted disks share a name)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## See also
- [`miren disk`](/command/disk)
---
## miren disk
Disk backup and recovery
## Usage
```bash
miren disk [flags]
```
## Subcommands
- [`miren disk backup`](/command/disk-backup) — Backup a disk to a snapshot file
- [`miren disk list-deleted`](/command/disk-list-deleted) — List deleted disks available for recovery
- [`miren disk restore`](/command/disk-restore) — Restore a disk from a snapshot file
- [`miren disk undelete`](/command/disk-undelete) — Restore a recently deleted disk
---
## miren doctor auth
Check authentication and user information
## Usage
```bash
miren doctor auth [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Check authentication:**
```bash
miren doctor auth
```
## See also
- [`miren doctor`](/command/doctor)
---
## miren doctor config
Check configuration files
## Usage
```bash
miren doctor config [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--format` — Output format (text, json) (default: `text`)
- `--json` — Shorthand for --format json
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Check config files:**
```bash
miren doctor config
```
## See also
- [`miren doctor`](/command/doctor)
---
## miren doctor server
Check server health and connectivity
## Usage
```bash
miren doctor server [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Check server connectivity:**
```bash
miren doctor server
```
## See also
- [`miren doctor`](/command/doctor)
---
## miren doctor
Diagnose miren environment and connectivity
## Usage
```bash
miren doctor [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Run all diagnostics:**
```bash
miren doctor
```
## Subcommands
- [`miren doctor auth`](/command/doctor-auth) — Check authentication and user information
- [`miren doctor config`](/command/doctor-config) — Check configuration files
- [`miren doctor server`](/command/doctor-server) — Check server health and connectivity
---
## miren download release
Download and extract miren release
## Usage
```bash
miren download release [flags]
```
## Flags
- `--branch, -b` — Branch name to download
- `--force, -f` — Force download even if release directory exists
- `--global, -g` — Install globally to /var/lib/miren/release
- `--output, -o` — Custom output directory
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Download the latest release:**
```bash
miren download release
```
## See also
- [`miren download`](/command/download)
---
## miren download
Download management commands
## Usage
```bash
miren download [flags]
```
## Subcommands
- [`miren download release`](/command/download-release) — Download and extract miren release
---
## miren env delete
Delete environment variables
## Usage
```bash
miren env delete [args...] [flags]
```
## Flags
- `--force, -f` — Skip confirmation prompt
- `--service, -S` — Delete env var from specific service only (if not specified, deletes global env var)
## Config Options
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
## App Options
- `--app, -a` — Application name
- `--dir, -d` — Directory to run from (default: `.`)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Delete a variable:**
```bash
miren env delete DATABASE_URL
```
**Delete without confirmation:**
```bash
miren env delete DATABASE_URL --force
```
**Delete a service-specific variable:**
```bash
miren env delete WORKERS --service worker
```
## See also
- [`miren env`](/command/env)
---
## miren env get
Get an environment variable value
## Usage
```bash
miren env get [flags]
```
## Arguments
- `key` — Environment variable key to get
## Flags
- `--service, -S` — Get env var for specific service (if not specified, gets global env var)
- `--unmask, -u` — Show actual value of sensitive variables instead of masking them
## Config Options
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
## App Options
- `--app, -a` — Application name
- `--dir, -d` — Directory to run from (default: `.`)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Get a variable value:**
```bash
miren env get DATABASE_URL
```
**Reveal a sensitive variable:**
```bash
miren env get SECRET_KEY --unmask
```
## See also
- [`miren env`](/command/env)
---
## miren env list
List all environment variables
## Usage
```bash
miren env list [flags]
```
## Flags
- `--format` — Output format (text, json) (default: `text`)
- `--json` — Shorthand for --format json
## Config Options
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
## App Options
- `--app, -a` — Application name
- `--dir, -d` — Directory to run from (default: `.`)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**List all variables:**
```bash
miren env list
```
**List as JSON:**
```bash
miren env list --format json
```
## See also
- [`miren env`](/command/env)
---
## miren env set
Set environment variables for an application
## Usage
```bash
miren env set [flags]
```
## Flags
- `--env, -e` — Set environment variables (use KEY to prompt, KEY=VALUE to set directly, KEY=@file to read from file)
- `--sensitive, -s` — Set sensitive environment variables (use KEY to prompt with masking, KEY=VALUE to set directly, KEY=@file to read from file)
- `--service, -S` — Set env var for specific service only (if not specified, sets for all services)
## Config Options
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
## App Options
- `--app, -a` — Application name
- `--dir, -d` — Directory to run from (default: `.`)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Set an environment variable:**
```bash
miren env set -e DATABASE_URL=postgres://localhost/mydb
```
**Set a sensitive variable (prompted with masking):**
```bash
miren env set -s SECRET_KEY
```
**Set a variable from a file:**
```bash
miren env set -e CONFIG=@config.json
```
**Set a variable for a specific service:**
```bash
miren env set -e WORKERS=4 --service worker
```
## See also
- [`miren env`](/command/env)
---
## miren env
Environment variable management commands
## Usage
```bash
miren env [flags]
```
## Subcommands
- [`miren env delete`](/command/env-delete) — Delete environment variables
- [`miren env get`](/command/env-get) — Get an environment variable value
- [`miren env list`](/command/env-list) — List all environment variables
- [`miren env set`](/command/env-set) — Set environment variables for an application
---
## miren help
Show help for one or more commands
## Usage
```bash
miren help [args...] [flags]
```
## Flags
- `--commands` — List all commands with their synopsis
- `--format` — Output format (text, json) (default: `text`)
- `--json` — Shorthand for --format json
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**List all commands:**
```bash
miren help --commands
```
**List all commands as JSON:**
```bash
miren help --commands --format json
```
**Show help for multiple commands:**
```bash
miren help app.list version sandbox.stop
```
---
## miren init
Initialize a new application
## Usage
```bash
miren init [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--dir, -d` — Application directory (defaults to current directory)
- `--name, -n` — Application name (defaults to directory name)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Initialize in current directory:**
```bash
miren init
```
**Initialize with a specific name:**
```bash
miren init --name myapp
```
---
## miren login
Authenticate with miren.cloud
## Usage
```bash
miren login [flags]
```
## Flags
- `--force, -f` — Overwrite existing identity without prompting
- `--identity, -i` — Name for this identity in config (default: `cloud`)
- `--key-name, -k` — Name for the authentication key (default: `miren-cli`)
- `--no-save` — Don't save credentials to config file
- `--url, -u` — Cloud URL (default: `https://miren.cloud`)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Login:**
```bash
miren login
```
**Login to a specific cloud instance:**
```bash
miren login --url https://cloud.example.com
```
---
## miren logout
Remove local authentication credentials
## Usage
```bash
miren logout [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--identity, -i` — Name of the identity to remove
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Logout:**
```bash
miren logout
```
---
## miren logs app
View application logs
## Subcommands
`miren logs` has subcommands for different log sources:
```bash
miren logs app # Application logs (default)
miren logs sandbox # Sandbox logs
miren logs build # Build logs
miren logs system # System/server logs
```
Running `miren logs` without a subcommand shows app logs (backward compatible).
## Time Range
By default, logs show the last 100 lines. Use `--last` to specify a time range:
```bash
# Show logs from the last 5 minutes
miren logs --last 5m
# Show logs from the last hour
miren logs --last 1h
# Show logs from the last 24 hours
miren logs --last 24h
```
## Following Logs
Use `--follow` (or `-f`) to stream logs in real-time:
```bash
# Follow logs as they arrive
miren logs -f
# Follow logs for a specific app
miren logs app -a myapp -f
```
## Filtering by Service
Use `--service` to filter logs by service name (app logs only):
```bash
# Show only logs from the web service
miren logs app --service web
# Show worker service logs containing "error"
miren logs app --service worker -g error
```
## Filtering Logs
Use the `--grep` (or `-g`) flag to filter log output. The filter supports multiple syntax options for flexible searching.
### Filter Syntax
| Syntax | Description | Example |
|--------|-------------|---------|
| `word` | Match logs containing "word" (case-insensitive) | `error` |
| `"phrase"` | Match logs containing exact phrase | `"connection failed"` |
| `'phrase'` | Match logs containing exact phrase (alternate) | `'connection failed'` |
| `/regex/` | Match logs matching regex pattern | `/err(or)?/` |
| `-term` | Exclude logs matching term | `-debug` |
| `term1 term2` | Match logs containing ALL terms (AND) | `error timeout` |
### Filter Details
- **Case-insensitive**: All word and phrase matches are case-insensitive
- **AND logic**: Multiple terms must all match for a log line to be included
- **Negation**: Prefix any term with `-` to exclude matching lines
- **Quotes**: Use double (`"`) or single (`'`) quotes for phrases with spaces
- **Regex**: Enclose patterns in forward slashes (`/pattern/`) for regex matching
## Log Output Format
Log entries are displayed with the following format:
```
S 2024-01-15 10:30:45: [source] Log message here
```
- **Stream prefix**: `S` (stdout), `E` (stderr), `ERR` (error), `U` (user-oob)
- **Timestamp**: Date and time when the log was generated
- **Source**: Optional source identifier (sandbox ID, truncated if long)
- **Message**: The actual log content
## Usage
```bash
miren logs app [flags]
```
## Flags
- `--follow, -f` — Follow log output (live tail)
- `--format` — Output format (text, json) (default: `text`)
- `--grep, -g` — Filter logs (e.g., 'error', '"exact phrase"', 'error -debug', '/regex/')
- `--json` — Shorthand for --format json
- `--last, -l` — Show logs from the last duration
- `--service` — Filter logs by service name (e.g., 'web', 'worker')
## Config Options
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
## App Options
- `--app, -a` — Application name
- `--dir, -d` — Directory to run from (default: `.`)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**View logs for the current app:**
```bash
miren logs app
```
**Follow logs for a specific app:**
```bash
miren logs app -a myapp -f
```
**Filter logs by service:**
```bash
miren logs app --service web -f
```
## See also
- [`miren logs`](/command/logs)
---
## miren logs build
View build logs
## Usage
```bash
miren logs build [flags]
```
## Arguments
- `version` — Build version (e.g., v3)
## Flags
- `--follow, -f` — Follow log output (live tail)
- `--format` — Output format (text, json) (default: `text`)
- `--grep, -g` — Filter logs (e.g., 'error', '"exact phrase"', 'error -debug', '/regex/')
- `--json` — Shorthand for --format json
- `--last, -l` — Show logs from the last duration
## Config Options
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
## App Options
- `--app, -a` — Application name
- `--dir, -d` — Directory to run from (default: `.`)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**View build logs for a version:**
```bash
miren logs build v3
```
**View build logs for a specific app:**
```bash
miren logs build v3 -a myapp
```
## See also
- [`miren logs`](/command/logs)
---
## miren logs sandbox
View sandbox logs
## Usage
```bash
miren logs sandbox [flags]
```
## Arguments
- `sandboxid` — Sandbox ID
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--follow, -f` — Follow log output (live tail)
- `--format` — Output format (text, json) (default: `text`)
- `--grep, -g` — Filter logs (e.g., 'error', '"exact phrase"', 'error -debug', '/regex/')
- `--json` — Shorthand for --format json
- `--last, -l` — Show logs from the last duration
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**View logs for a sandbox:**
```bash
miren logs sandbox sb_abc123
```
**Follow sandbox logs:**
```bash
miren logs sandbox sb_abc123 -f
```
## See also
- [`miren logs`](/command/logs)
---
## miren logs system
View system logs
## Usage
```bash
miren logs system [flags]
```
## Arguments
- `component` — System component to filter by (e.g., 'etcd', 'scheduler')
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--follow, -f` — Follow log output (live tail)
- `--format` — Output format (text, json) (default: `text`)
- `--grep, -g` — Filter logs (e.g., 'error', '"exact phrase"', 'error -debug', '/regex/')
- `--json` — Shorthand for --format json
- `--last, -l` — Show logs from the last duration
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**View all system logs:**
```bash
miren logs system
```
**View logs for a specific component:**
```bash
miren logs system etcd
```
**Follow system logs:**
```bash
miren logs system -f
```
## See also
- [`miren logs`](/command/logs)
---
## miren logs
View logs (defaults to app logs)
## Subcommands
`miren logs` has subcommands for different log sources:
```bash
miren logs app # Application logs (default)
miren logs sandbox # Sandbox logs
miren logs build # Build logs
miren logs system # System/server logs
```
Running `miren logs` without a subcommand shows app logs (backward compatible).
## Time Range
By default, logs show the last 100 lines. Use `--last` to specify a time range:
```bash
# Show logs from the last 5 minutes
miren logs --last 5m
# Show logs from the last hour
miren logs --last 1h
# Show logs from the last 24 hours
miren logs --last 24h
```
## Following Logs
Use `--follow` (or `-f`) to stream logs in real-time:
```bash
# Follow logs as they arrive
miren logs -f
# Follow logs for a specific app
miren logs app -a myapp -f
```
## Filtering by Service
Use `--service` to filter logs by service name (app logs only):
```bash
# Show only logs from the web service
miren logs app --service web
# Show worker service logs containing "error"
miren logs app --service worker -g error
```
## Filtering Logs
Use the `--grep` (or `-g`) flag to filter log output. The filter supports multiple syntax options for flexible searching.
### Filter Syntax
| Syntax | Description | Example |
|--------|-------------|---------|
| `word` | Match logs containing "word" (case-insensitive) | `error` |
| `"phrase"` | Match logs containing exact phrase | `"connection failed"` |
| `'phrase'` | Match logs containing exact phrase (alternate) | `'connection failed'` |
| `/regex/` | Match logs matching regex pattern | `/err(or)?/` |
| `-term` | Exclude logs matching term | `-debug` |
| `term1 term2` | Match logs containing ALL terms (AND) | `error timeout` |
### Filter Details
- **Case-insensitive**: All word and phrase matches are case-insensitive
- **AND logic**: Multiple terms must all match for a log line to be included
- **Negation**: Prefix any term with `-` to exclude matching lines
- **Quotes**: Use double (`"`) or single (`'`) quotes for phrases with spaces
- **Regex**: Enclose patterns in forward slashes (`/pattern/`) for regex matching
## Log Output Format
Log entries are displayed with the following format:
```
S 2024-01-15 10:30:45: [source] Log message here
```
- **Stream prefix**: `S` (stdout), `E` (stderr), `ERR` (error), `U` (user-oob)
- **Timestamp**: Date and time when the log was generated
- **Source**: Optional source identifier (sandbox ID, truncated if long)
- **Message**: The actual log content
## Usage
```bash
miren logs [flags]
```
## Flags
- `--follow, -f` — Follow log output (live tail)
- `--format` — Output format (text, json) (default: `text`)
- `--grep, -g` — Filter logs (e.g., 'error', '"exact phrase"', 'error -debug', '/regex/')
- `--json` — Shorthand for --format json
- `--last, -l` — Show logs from the last duration
- `--service` — Filter logs by service name (e.g., 'web', 'worker')
## Config Options
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
## App Options
- `--app, -a` — Application name
- `--dir, -d` — Directory to run from (default: `.`)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**View logs for the current app:**
```bash
miren logs
```
**Follow logs in real time:**
```bash
miren logs -f
```
**Show logs from the last 5 minutes, filtered for errors:**
```bash
miren logs --last 5m -g error
```
## Subcommands
- [`miren logs app`](/command/logs-app) — View application logs
- [`miren logs build`](/command/logs-build) — View build logs
- [`miren logs sandbox`](/command/logs-sandbox) — View sandbox logs
- [`miren logs system`](/command/logs-system) — View system logs
---
## miren rollback
Roll back to a previous version
## Usage
```bash
miren rollback [flags]
```
## Config Options
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
## App Options
- `--app, -a` — Application name
- `--dir, -d` — Directory to run from (default: `.`)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Rollback the app in the current directory:**
```bash
miren rollback
```
**Rollback a specific app:**
```bash
miren rollback -a myapp
```
---
## miren route list
List all HTTP routes
## Usage
```bash
miren route list [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--format` — Output format (text, json) (default: `text`)
- `--json` — Shorthand for --format json
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**List all routes:**
```bash
miren route list
```
**List as JSON:**
```bash
miren route list --format json
```
## See also
- [`miren route`](/command/route)
---
## miren route protect
Protect an HTTP route with an identity provider
## Usage
```bash
miren route protect [flags]
```
## Arguments
- `host` — Hostname for the route (e.g., example.com); omit and pass --default for the default route
## Flags
- `--claim-header` — Claim to header mapping in format 'claim:header' (e.g., 'email:X-User-Email')
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--default` — Protect the default route (instead of a hostname)
- `--format` — Output format (text, json) (default: `text`)
- `--json` — Shorthand for --format json
- `--provider` — Name of the identity provider
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Protect a route with an identity provider:**
```bash
miren route protect example.com --provider my-google-oidc --claim-header email:X-User-Email
```
**Protect the default route:**
```bash
miren route protect --default --provider my-google-oidc
```
## See also
- [`miren route`](/command/route)
---
## miren route remove
Remove an HTTP route
## Usage
```bash
miren route remove [flags]
```
## Arguments
- `host` — Hostname of the route to remove
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Remove a route:**
```bash
miren route remove example.com
```
## See also
- [`miren route`](/command/route)
---
## miren route set-default
Set an app as the default route
## Usage
```bash
miren route set-default [flags]
```
## Arguments
- `appname` — Application name to set as default route
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Set the default route:**
```bash
miren route set-default myapp
```
## See also
- [`miren route`](/command/route)
---
## miren route set
Create or update an HTTP route
## Usage
```bash
miren route set [flags]
```
## Arguments
- `host` — Hostname for the route (e.g., example.com or *.example.com)
- `appname` — Application name to route to
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Route a domain to an app:**
```bash
miren route set example.com myapp
```
## See also
- [`miren route`](/command/route)
---
## miren route show
Show details of an HTTP route
## Usage
```bash
miren route show [flags]
```
## Arguments
- `host` — Hostname of the route to show; omit and pass --default for the default route
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--default` — Show the default route (instead of a hostname)
- `--format` — Output format (text, json) (default: `text`)
- `--json` — Shorthand for --format json
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Show route details:**
```bash
miren route show example.com
```
## See also
- [`miren route`](/command/route)
---
## miren route unprotect
Remove identity-provider protection from an HTTP route
## Usage
```bash
miren route unprotect [flags]
```
## Arguments
- `host` — Hostname for the route (e.g., example.com); omit and pass --default for the default route
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--default` — Remove protection from the default route (instead of a hostname)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Remove protection from a route:**
```bash
miren route unprotect example.com
```
## See also
- [`miren route`](/command/route)
---
## miren route unset-default
Remove the default route
## Usage
```bash
miren route unset-default [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Remove the default route:**
```bash
miren route unset-default
```
## See also
- [`miren route`](/command/route)
---
## miren route waf
Manage WAF protection on an HTTP route
## Usage
```bash
miren route waf [flags]
```
## Arguments
- `host` — Hostname for the route (e.g., example.com); omit and pass --default for the default route
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--default` — Apply to the default route (instead of a hostname)
- `--disable` — Disable WAF on the route
- `--format` — Output format (text, json) (default: `text`)
- `--json` — Shorthand for --format json
- `--level` — OWASP CRS paranoia level (1-4) (default: `1`)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Enable WAF on a route with default paranoia level:**
```bash
miren route waf example.com
```
**Enable WAF with a specific paranoia level:**
```bash
miren route waf example.com --level 2
```
**Enable WAF on the default route:**
```bash
miren route waf --default
```
**Disable WAF on a route:**
```bash
miren route waf example.com --disable
```
## See also
- [`miren route`](/command/route)
---
## miren route
List all HTTP routes
## Usage
```bash
miren route [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--format` — Output format (text, json) (default: `text`)
- `--json` — Shorthand for --format json
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**List all routes:**
```bash
miren route
```
## Subcommands
- [`miren route list`](/command/route-list) — List all HTTP routes
- [`miren route protect`](/command/route-protect) — Protect an HTTP route with an identity provider
- [`miren route remove`](/command/route-remove) — Remove an HTTP route
- [`miren route set`](/command/route-set) — Create or update an HTTP route
- [`miren route set-default`](/command/route-set-default) — Set an app as the default route
- [`miren route show`](/command/route-show) — Show details of an HTTP route
- [`miren route unprotect`](/command/route-unprotect) — Remove identity-provider protection from an HTTP route
- [`miren route unset-default`](/command/route-unset-default) — Remove the default route
- [`miren route waf`](/command/route-waf) — Manage WAF protection on an HTTP route
---
## miren runner install
Install systemd service for miren runner
:::note
This command requires the `distributedrunners` [labs feature](/labs) to be enabled.
:::
## Usage
```bash
miren runner install [flags]
```
## Flags
- `--branch, -b` — Branch to download
- `--config` — Path to runner config (default: `/var/lib/miren/runner/config.yaml`)
- `--coordinator, -c` — Override coordinator address from the token
- `--data-path` — Path to store runner data (default: `/var/lib/miren/runner`)
- `--force, -f` — Overwrite existing service file
- `--labels` — Runner labels (key=value)
- `--listen, -l` — Address this runner will listen on
- `--name` — Human-readable name for this runner (defaults to hostname)
- `--no-start` — Do not start the service after installation
- `--skip-system-check` — Skip minimum system requirements check
- `--token, -t` — Enrollment token from 'miren runner token create'
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Install interactively:**
```bash
miren runner install
```
**Install with token (for automation):**
```bash
miren runner install --token mren_...
```
## See also
- [`miren runner`](/command/runner)
---
## miren runner join
Join this machine to a coordinator as a runner
:::note
This command requires the `distributedrunners` [labs feature](/labs) to be enabled.
:::
## Usage
```bash
miren runner join [flags]
```
## Arguments
- `tokenarg` — Join token from 'miren runner token create'
## Flags
- `--config` — Path to save runner config (default: `/var/lib/miren/runner/config.yaml`)
- `--coordinator, -c` — Override coordinator address from the token
- `--labels` — Additional labels for the runner (key=value)
- `--listen, -l` — Address this runner will listen on
- `--name` — Human-readable name for this runner (defaults to hostname)
- `--runner-id` — Specific runner ID to use (for reconnecting)
- `--token` — Enrollment token (or pass as positional arg / via stdin)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Join using a token:**
```bash
miren runner join mren_...
```
**Join with coordinator address override:**
```bash
miren runner join mren_... --coordinator 10.0.0.5:8443
```
## See also
- [`miren runner`](/command/runner)
---
## miren runner list
List all registered runners
:::note
This command requires the `distributedrunners` [labs feature](/labs) to be enabled.
:::
## Usage
```bash
miren runner list [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--format` — Output format (text, json) (default: `text`)
- `--json` — Shorthand for --format json
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**List runners:**
```bash
miren runner list
```
## See also
- [`miren runner`](/command/runner)
---
## miren runner remove
Remove a registered runner and clean up resources
:::note
This command requires the `distributedrunners` [labs feature](/labs) to be enabled.
:::
## Usage
```bash
miren runner remove [flags]
```
## Arguments
- `node` — Runner to remove (name, ID, or short ID)
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--force, -f` — Force removal even if the runner has active sandboxes
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Remove a runner by name:**
```bash
miren runner remove my-runner
```
**Force remove a runner with active sandboxes:**
```bash
miren runner remove my-runner --force
```
## See also
- [`miren runner`](/command/runner)
---
## miren runner service-status
Show miren-runner systemd service status
:::note
This command requires the `distributedrunners` [labs feature](/labs) to be enabled.
:::
## Usage
```bash
miren runner service-status [flags]
```
## Flags
- `--follow, -f` — Follow logs in real-time
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Show service status:**
```bash
miren runner service-status
```
**Follow service logs:**
```bash
miren runner service-status --follow
```
## See also
- [`miren runner`](/command/runner)
---
## miren runner start
Start this machine as a distributed runner
:::note
This command requires the `distributedrunners` [labs feature](/labs) to be enabled.
:::
## Usage
```bash
miren runner start [flags]
```
## Flags
- `--config` — Path to runner config (default: `/var/lib/miren/runner/config.yaml`)
- `--containerd-socket` — Path to containerd socket
- `--data-path` — Path to store runner data (default: `/var/lib/miren/runner`)
- `--listen, -l` — Address this runner will listen on (overrides config)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Start the runner:**
```bash
miren runner start
```
## See also
- [`miren runner`](/command/runner)
---
## miren runner status
Show runner health and configuration
:::note
This command requires the `distributedrunners` [labs feature](/labs) to be enabled.
:::
## Usage
```bash
miren runner status [flags]
```
## Flags
- `--config` — Path to runner config (default: `/var/lib/miren/runner/config.yaml`)
- `--data-path` — Path to runner data (default: `/var/lib/miren/runner`)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Check runner status:**
```bash
miren runner status
```
## See also
- [`miren runner`](/command/runner)
---
## miren runner token create
Create a join token for a runner
:::note
This command requires the `distributedrunners` [labs feature](/labs) to be enabled.
:::
## Usage
```bash
miren runner token create [flags]
```
## Flags
- `--addr, -a` — Override coordinator address baked into the token
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--expires, -e` — Hours until the invite expires (default: `1`)
- `--labels, -l` — Labels to apply to the runner (key=value format)
- `--name, -n` — Human-readable name for this invite
- `--reusable, -r` — Create a reusable invite (not consumed on use)
- `--ttl` — Time-to-live (e.g. 24h, 7d, 2w). Overrides --expires
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Create a one-time join token:**
```bash
miren runner token create
```
**Create a reusable token for automation:**
```bash
miren runner token create --reusable --name infra-terraform --ttl 0
```
**Create a token with a specific coordinator address:**
```bash
miren runner token create --addr 10.0.0.5:8443
```
## See also
- [`miren runner token`](/command/runner-token)
---
## miren runner token list
List all join tokens
:::note
This command requires the `distributedrunners` [labs feature](/labs) to be enabled.
:::
## Usage
```bash
miren runner token list [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--format` — Output format (text, json) (default: `text`)
- `--json` — Shorthand for --format json
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**List tokens:**
```bash
miren runner token list
```
## See also
- [`miren runner token`](/command/runner-token)
---
## miren runner token revoke
Revoke a join token
:::note
This command requires the `distributedrunners` [labs feature](/labs) to be enabled.
:::
## Usage
```bash
miren runner token revoke [flags]
```
## Arguments
- `tokenid` — ID of the token to revoke
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Revoke a token:**
```bash
miren runner token revoke inv_abc123
```
## See also
- [`miren runner token`](/command/runner-token)
---
## miren runner token
Manage join tokens
## Usage
```bash
miren runner token [flags]
```
## Subcommands
- [`miren runner token create`](/command/runner-token-create) — Create a join token for a runner
- [`miren runner token list`](/command/runner-token-list) — List all join tokens
- [`miren runner token revoke`](/command/runner-token-revoke) — Revoke a join token
## See also
- [`miren runner`](/command/runner)
---
## miren runner uninstall
Remove systemd service for miren runner
:::note
This command requires the `distributedrunners` [labs feature](/labs) to be enabled.
:::
## Usage
```bash
miren runner uninstall [flags]
```
## Flags
- `--data-path` — Path to runner data (default: `/var/lib/miren/runner`)
- `--remove-data` — Remove runner data directory
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Uninstall the runner service:**
```bash
miren runner uninstall
```
**Uninstall and remove all runner data:**
```bash
miren runner uninstall --remove-data
```
## See also
- [`miren runner`](/command/runner)
---
## miren runner upgrade rollback
Rollback runner to previous version
:::note
This command requires the `distributedrunners` [labs feature](/labs) to be enabled.
:::
## Usage
```bash
miren runner upgrade rollback [flags]
```
## Flags
- `--skip-health` — Skip health check after rollback
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Rollback to the previous version:**
```bash
miren runner upgrade rollback
```
## See also
- [`miren runner upgrade`](/command/runner-upgrade)
---
## miren runner upgrade
Upgrade miren runner to the latest or specified version
:::note
This command requires the `distributedrunners` [labs feature](/labs) to be enabled.
:::
## Usage
```bash
miren runner upgrade [flags]
```
## Flags
- `--channel` — Channel to use: 'latest' (stable releases, default) or 'main' (bleeding edge)
- `--check, -c` — Check for available updates only
- `--force, -f` — Force upgrade even if already up to date
- `--health-timeout` — Health check timeout in seconds (default: `60`)
- `--no-auto-rollback` — Disable automatic rollback on failure
- `--skip-health` — Skip health check after upgrade
- `--version, -V` — Specific version to upgrade to (e.g., v0.2.0)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Upgrade to the latest version:**
```bash
miren runner upgrade
```
**Check for available updates:**
```bash
miren runner upgrade --check
```
**Upgrade to a specific version:**
```bash
miren runner upgrade --version v0.2.0
```
## Subcommands
- [`miren runner upgrade rollback`](/command/runner-upgrade-rollback) — Rollback runner to previous version
## See also
- [`miren runner`](/command/runner)
---
## miren runner
Runner management commands
## Usage
```bash
miren runner [flags]
```
## Subcommands
- [`miren runner install`](/command/runner-install) — Install systemd service for miren runner
- [`miren runner join`](/command/runner-join) — Join this machine to a coordinator as a runner
- [`miren runner list`](/command/runner-list) — List all registered runners
- [`miren runner remove`](/command/runner-remove) — Remove a registered runner and clean up resources
- [`miren runner service-status`](/command/runner-service-status) — Show miren-runner systemd service status
- [`miren runner start`](/command/runner-start) — Start this machine as a distributed runner
- [`miren runner status`](/command/runner-status) — Show runner health and configuration
- [`miren runner token`](/command/runner-token) — Manage join tokens
- [`miren runner uninstall`](/command/runner-uninstall) — Remove systemd service for miren runner
- [`miren runner upgrade`](/command/runner-upgrade) — Upgrade miren runner to the latest or specified version
---
## miren sandbox delete
Delete a dead sandbox
## Usage
```bash
miren sandbox delete [flags]
```
## Arguments
- `sandboxid` — ID of the sandbox to delete
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--force, -f` — Force delete without confirmation
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Delete a sandbox:**
```bash
miren sandbox delete sb_abc123
```
**Force delete without confirmation:**
```bash
miren sandbox delete sb_abc123 --force
```
## See also
- [`miren sandbox`](/command/sandbox)
---
## miren sandbox exec
Open interactive shell in an existing sandbox
This command connects to an existing sandbox and runs a command inside it. Unlike `miren app run` which creates a new ephemeral sandbox, this connects to a sandbox that's already running (typically one serving production traffic).
## Finding Sandbox IDs
Use `miren sandbox list` to find the ID of a running sandbox:
```bash
$ miren sandbox list
ID APP SERVICE STATUS NODE
sandbox/myapp-web-abc123 myapp web RUNNING node-1
sandbox/myapp-web-def456 myapp web RUNNING node-2
```
:::warning
When you exec into a production sandbox, you're connecting to a live instance that may be serving traffic. Be careful with commands that could affect the running application.
:::
:::tip
For debugging or one-off tasks without affecting production, use `miren app run` to create an isolated ephemeral sandbox instead.
:::
## Usage
```bash
miren sandbox exec [args...] [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--id, -i` — Sandbox ID
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Open a shell in a running sandbox:**
```bash
miren sandbox exec sb_abc123
```
**Run a command in a sandbox:**
```bash
miren sandbox exec sb_abc123 -- ls -la /app
```
## See also
- [`miren sandbox`](/command/sandbox)
---
## miren sandbox list
List sandboxes (excludes dead by default)
## Usage
```bash
miren sandbox list [flags]
```
## Flags
- `--all, -a` — Include dead sandboxes (excluded by default)
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--format` — Output format (text, json) (default: `text`)
- `--json` — Shorthand for --format json
- `--status, -s` — Filter by status (pending, not_ready, running, stopped, dead)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**List running sandboxes:**
```bash
miren sandbox list
```
**Include dead sandboxes:**
```bash
miren sandbox list --all
```
**List as JSON:**
```bash
miren sandbox list --format json
```
## See also
- [`miren sandbox`](/command/sandbox)
---
## miren sandbox-pool list
List all sandbox pools
## Usage
```bash
miren sandbox-pool list [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--format` — Output format (text, json) (default: `text`)
- `--json` — Shorthand for --format json
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**List all pools:**
```bash
miren sandbox-pool list
```
## See also
- [`miren sandbox-pool`](/command/sandbox-pool)
---
## miren sandbox-pool set-desired
Set desired instance count for a sandbox pool
## Usage
```bash
miren sandbox-pool set-desired [flags]
```
## Arguments
- `poolid` — Pool ID (e.g., pool-CUSkT8J58BmgkDeGyPP2e or pool/pool-CUSkT8J58BmgkDeGyPP2e)
- `desired` — Desired instance count (absolute number, +N to increase, or -N to decrease)
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--raw-id` — Use the provided ID as-is without adding the pool/ prefix
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Scale a pool to 3 instances:**
```bash
miren sandbox-pool set-desired web 3
```
## See also
- [`miren sandbox-pool`](/command/sandbox-pool)
---
## miren sandbox-pool
Sandbox pool management commands
## Usage
```bash
miren sandbox-pool [flags]
```
## Subcommands
- [`miren sandbox-pool list`](/command/sandbox-pool-list) — List all sandbox pools
- [`miren sandbox-pool set-desired`](/command/sandbox-pool-set-desired) — Set desired instance count for a sandbox pool
---
## miren sandbox stop
Stop a sandbox
## Usage
```bash
miren sandbox stop [flags]
```
## Arguments
- `sandboxid` — ID of the sandbox to stop
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Stop a sandbox by ID:**
```bash
miren sandbox stop sb_abc123
```
## See also
- [`miren sandbox`](/command/sandbox)
---
## miren sandbox
Sandbox management commands
Sandboxes are the underlying execution environments for your applications. Most of the time you'll work with apps directly, but these commands are useful for debugging and advanced use cases.
## Usage
```bash
miren sandbox [flags]
```
## Subcommands
- [`miren sandbox delete`](/command/sandbox-delete) — Delete a dead sandbox
- [`miren sandbox exec`](/command/sandbox-exec) — Open interactive shell in an existing sandbox
- [`miren sandbox list`](/command/sandbox-list) — List sandboxes (excludes dead by default)
- [`miren sandbox stop`](/command/sandbox-stop) — Stop a sandbox
---
## miren server config generate
Generate a server configuration file from current settings
## Usage
```bash
miren server config generate [flags]
```
## Flags
- `--defaults, -d` — Generate config with default values
- `--mode, -m` — Server mode: standalone (default), distributed (experimental) (default: `standalone`)
- `--output, -o` — Output file path (defaults to stdout)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Generate config with defaults:**
```bash
miren server config generate --defaults
```
**Generate and save to file:**
```bash
miren server config generate --defaults --output server.toml
```
## See also
- [`miren server config`](/command/server-config)
---
## miren server config validate
Validate a server configuration file
## Usage
```bash
miren server config validate [flags]
```
## Flags
- `--file, -f` — Configuration file to validate
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Validate a config file:**
```bash
miren server config validate --file server.toml
```
## See also
- [`miren server config`](/command/server-config)
---
## miren server config
Server configuration management commands
## Usage
```bash
miren server config [flags]
```
## Subcommands
- [`miren server config generate`](/command/server-config-generate) — Generate a server configuration file from current settings
- [`miren server config validate`](/command/server-config-validate) — Validate a server configuration file
## See also
- [`miren server`](/command/server)
---
## miren server docker install
Install miren server using Docker
## Usage
```bash
miren server docker install [flags]
```
## Flags
- `--cluster-name` — Cluster name for cloud registration
- `--force, -f` — Remove existing container if present
- `--host-network` — Use host networking (ignores port mappings)
- `--http-port` — HTTP port mapping (default: `80`)
- `--image, -i` — Docker image to use (default: `oci.miren.cloud/miren:latest`)
- `--labs, -l` — Miren Labs features to enable (e.g. adminapi). Prefix with - to disable.
- `--name, -n` — Container name
- `--url, -u` — Cloud URL for registration (default: `https://miren.cloud`)
- `--without-cloud` — Skip cloud registration setup
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Install with cloud registration:**
```bash
miren server docker install
```
**Install without cloud (local only):**
```bash
miren server docker install --without-cloud
```
**Install with a custom HTTP port:**
```bash
miren server docker install --http-port 8080
```
## See also
- [`miren server docker`](/command/server-docker)
---
## miren server docker status
Show status of miren server Docker container
## Usage
```bash
miren server docker status [flags]
```
## Flags
- `--follow, -f` — Follow logs in real-time
- `--name, -n` — Container name (default: `miren`)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Show status:**
```bash
miren server docker status
```
**Follow logs:**
```bash
miren server docker status --follow
```
## See also
- [`miren server docker`](/command/server-docker)
---
## miren server docker uninstall
Uninstall miren server Docker container
## Usage
```bash
miren server docker uninstall [flags]
```
## Flags
- `--force, -f` — Force removal even if container is running
- `--name, -n` — Container name (default: `miren`)
- `--remove-volume` — Remove the data volume
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Uninstall the container:**
```bash
miren server docker uninstall
```
**Uninstall and remove all data:**
```bash
miren server docker uninstall --remove-volume
```
## See also
- [`miren server docker`](/command/server-docker)
---
## miren server docker
Docker-based server management commands
## Usage
```bash
miren server docker [flags]
```
## Subcommands
- [`miren server docker install`](/command/server-docker-install) — Install miren server using Docker
- [`miren server docker status`](/command/server-docker-status) — Show status of miren server Docker container
- [`miren server docker uninstall`](/command/server-docker-uninstall) — Uninstall miren server Docker container
## See also
- [`miren server`](/command/server)
---
## miren server install
Install systemd service for miren server
## Usage
```bash
miren server install [flags]
```
## Flags
- `--address, -a` — Server address to bind to (default: `0.0.0.0:8443`)
- `--branch, -b` — Branch to download if release not found
- `--force, -f` — Overwrite existing service file
- `--name, -n` — Cluster name for cloud registration
- `--no-start` — Do not start the service after installation
- `--skip-system-check` — Skip minimum system requirements check
- `--url, -u` — Cloud URL for registration (default: `https://miren.cloud`)
- `--verbosity` — Verbosity level (default: `-vv`)
- `--without-cloud` — Skip cloud registration setup
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Install with cloud registration:**
```bash
miren server install
```
**Install without cloud (local only):**
```bash
miren server install --without-cloud
```
## See also
- [`miren server`](/command/server)
---
## miren server register status
Show cluster registration status
## Usage
```bash
miren server register status [flags]
```
## Flags
- `--dir, -d` — Registration directory (default: `/var/lib/miren/server`)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Check registration status:**
```bash
miren server register status
```
## See also
- [`miren server register`](/command/server-register)
---
## miren server register
Register this cluster with miren.cloud
## Usage
```bash
miren server register [flags]
```
## Flags
- `--name, -n` — Cluster name
- `--output, -o` — Output directory for registration (default: `/var/lib/miren/server`)
- `--url, -u` — Cloud URL (default: `https://miren.cloud`)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Register with cloud:**
```bash
miren server register --name my-cluster
```
**Register with a specific cloud URL:**
```bash
miren server register --name my-cluster --url https://cloud.example.com
```
## Subcommands
- [`miren server register status`](/command/server-register-status) — Show cluster registration status
## See also
- [`miren server`](/command/server)
---
## miren server status
Show miren service status
## Usage
```bash
miren server status [flags]
```
## Flags
- `--follow, -f` — Follow logs in real-time
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Show server status:**
```bash
miren server status
```
**Follow server logs:**
```bash
miren server status --follow
```
## See also
- [`miren server`](/command/server)
---
## miren server uninstall
Remove systemd service for miren server
## Usage
```bash
miren server uninstall [flags]
```
## Flags
- `--backup-dir` — Directory to save backup tarball (default: `.`)
- `--remove-data` — Remove /var/lib/miren directory after backing it up
- `--skip-backup` — Skip backup when removing data (dangerous)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Uninstall the server:**
```bash
miren server uninstall
```
**Uninstall and remove all data:**
```bash
miren server uninstall --remove-data
```
## See also
- [`miren server`](/command/server)
---
## miren server upgrade rollback
Rollback server to previous version
## Usage
```bash
miren server upgrade rollback [flags]
```
## Flags
- `--skip-health` — Skip health check after rollback
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Rollback to the previous version:**
```bash
miren server upgrade rollback
```
## See also
- [`miren server upgrade`](/command/server-upgrade)
---
## miren server upgrade
Upgrade miren server
## Usage
```bash
miren server upgrade [flags]
```
## Flags
- `--channel` — Channel to use: 'latest' (stable releases, default) or 'main' (bleeding edge)
- `--check, -c` — Check for available updates only
- `--force, -f` — Force upgrade even if already up to date
- `--health-timeout` — Health check timeout in seconds (default: `60`)
- `--no-auto-rollback` — Disable automatic rollback on failure
- `--release, -r` — Upgrade full release package (not just base)
- `--skip-health` — Skip health check after upgrade
- `--version, -V` — Specific version to upgrade to (e.g., v0.2.0)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Upgrade to the latest version:**
```bash
miren server upgrade
```
**Check for available updates:**
```bash
miren server upgrade --check
```
**Upgrade to a specific version:**
```bash
miren server upgrade --version v0.2.0
```
## Subcommands
- [`miren server upgrade rollback`](/command/server-upgrade-rollback) — Rollback server to previous version
## See also
- [`miren server`](/command/server)
---
## miren server
Start the miren server
## Usage
```bash
miren server [flags]
```
## Flags
- `--acme-dns-provider` — DNS provider for ACME DNS-01 challenges (e.g., cloudflare, route53, exec). When set, uses DNS challenge instead of HTTP challenge. See https://go-acme.github.io/lego/dns/ for available providers.
- `--acme-email` — Email address for ACME account registration (recommended for account recovery and notifications)
- `--address, -a` — Address to listen on (host:port). For IPv6 use brackets, e.g. "[::1]:8443".
- `--buildkit-gc-duration` — How long to keep BuildKit cache entries (e.g., 7d, 24h)
- `--buildkit-gc-storage` — Maximum BuildKit layer cache size (e.g., 10GB, 50GB)
- `--buildkit-socket` — Path to external BuildKit Unix socket (for distributed mode)
- `--buildkit-socket-dir` — Directory for embedded BuildKit Unix socket (defaults to data_path/buildkit/socket)
- `--config` — Path to configuration file
- `--config-cluster-name, -C` — Name of the cluster in client config
- `--containerd-binary` — Path to containerd binary
- `--containerd-socket` — Path to containerd socket
- `--data-path, -d` — Data path
- `--disk-mode` — Disk I/O mode: auto (default, detect from hardware), universal (loop devices), or accelerator (lbd devices)
- `--dns-names` — Additional DNS names assigned to the server cert
- `--etcd, -e` — Etcd endpoints
- `--etcd-client-port` — Etcd client port
- `--etcd-http-client-port` — Etcd HTTP client port
- `--etcd-peer-port` — Etcd peer port
- `--etcd-prefix, -p` — Etcd prefix
- `--http-request-timeout` — HTTP request timeout in seconds
- `--ingress-address` — Optional bind override. Replaces the mode's default bind entirely (interface and port). Rejected by validation in tls-autoprovision (where :443 + :80 is structural). Reserved unix:/path prefix is not yet supported.
- `--ingress-mode` — Ingress mode: tls-autoprovision (default, :443 + :80 with ACME or self-signed), behind-proxy-http (plain HTTP for use behind a TLS-terminating proxy), behind-proxy-https (TLS terminated by Miren; certs come from self-signed or DNS-01 ACME, since :80 isn't bound for HTTP-01)
- `--ips` — Additional IPs assigned to the server cert
- `--labs` — Comma-separated list of Miren Labs features to enable/disable. Prefix with - to disable.
- `--mode, -m` — Server mode: standalone (default), distributed (experimental)
- `--network-backend` — Network backend for sandbox connectivity: vxlan (default) or wireguard
- `--release-path` — Path to release directory containing binaries
- `--runner-address` — Runner address (host:port). For IPv6 use brackets, e.g. "[::1]:8444".
- `--runner-id, -r` — Runner ID
- `--self-signed-tls` — Use self-signed certificates for TLS (for development/testing only)
- `--serve-tls` — Deprecated and ignored. Retained as a no-op so existing systemd unit files, env vars, and config files from pre-RFD-84 installs still parse. Use ingress.mode to pick the deployment shape.
- `--skip-client-config` — Skip writing client config file to clientconfig.d
- `--start-buildkit` — Start embedded BuildKit daemon for container image builds
- `--start-containerd` — Start embedded containerd daemon
- `--start-etcd` — Start embedded etcd server
- `--start-victorialogs` — Start embedded VictoriaLogs server
- `--start-victoriametrics` — Start embedded VictoriaMetrics server
- `--stop-sandboxes-on-shutdown` — Stop all sandboxes when server shuts down (useful in development)
- `--victorialogs-addr` — VictoriaLogs address (when not using embedded)
- `--victorialogs-http-port` — VictoriaLogs HTTP port in embedded mode
- `--victorialogs-retention` — VictoriaLogs retention period (e.g. 30d, 2w, 1y)
- `--victoriametrics-addr` — VictoriaMetrics address (when not using embedded)
- `--victoriametrics-http-port` — VictoriaMetrics HTTP port in embedded mode
- `--victoriametrics-retention` — VictoriaMetrics retention period in months
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Start in standalone mode:**
```bash
miren server --mode standalone
```
## Subcommands
- [`miren server config`](/command/server-config) — Server configuration management commands
- [`miren server docker`](/command/server-docker) — Docker-based server management commands
- [`miren server install`](/command/server-install) — Install systemd service for miren server
- [`miren server register`](/command/server-register) — Register this cluster with miren.cloud
- [`miren server status`](/command/server-status) — Show miren service status
- [`miren server uninstall`](/command/server-uninstall) — Remove systemd service for miren server
- [`miren server upgrade`](/command/server-upgrade) — Upgrade miren server
---
## miren upgrade
Upgrade miren CLI to latest version
## Usage
```bash
miren upgrade [flags]
```
## Flags
- `--channel` — Channel to use: 'latest' (stable releases, default) or 'main' (bleeding edge)
- `--check, -c` — Check for available updates only
- `--force, -f` — Force upgrade even if already up to date or server running
- `--user, -u` — Install to user directory (~/.miren/release/miren) instead of system location
- `--version, -V` — Specific version to upgrade to (e.g., v0.2.0)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Upgrade to latest:**
```bash
miren upgrade
```
**Check for updates without installing:**
```bash
miren upgrade --check
```
**Upgrade to a specific version:**
```bash
miren upgrade --version v0.2.0
```
---
## miren version
Print the version
## Usage
```bash
miren version [flags]
```
## Flags
- `--deps` — Show dependencies
- `--format` — Output format (text, json) (default: `text`)
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Print version:**
```bash
miren version
```
**JSON output:**
```bash
miren version --format json
```
---
## miren whoami
Display information about the current authenticated user
## Usage
```bash
miren whoami [flags]
```
## Flags
- `--cluster, -C` — Cluster name
- `--config` — Path to the config file
- `--format` — Output format (text, json) (default: `text`)
- `--json` — Shorthand for --format json
## Global Options
- `--options` — Path to file containing options
- `--server-address` — Server address to connect to (default: `127.0.0.1:8443`)
- `--verbose, -v` — Enable verbose output
## Examples
**Show current user:**
```bash
miren whoami
```
**JSON output:**
```bash
miren whoami --json
```
---
## Commands
Complete reference for all `miren` CLI commands.
## addon
| Command | Description |
|---------|-------------|
| [`miren addon`](/command/addon) | Addon management commands |
| [`miren addon create`](/command/addon-create) | Attach an addon to an application _(`addons`)_ |
| [`miren addon destroy`](/command/addon-destroy) | Remove an addon from an application _(`addons`)_ |
| [`miren addon list`](/command/addon-list) | List addons attached to an application _(`addons`)_ |
| [`miren addon list-available`](/command/addon-list-available) | List available addons _(`addons`)_ |
| [`miren addon variants`](/command/addon-variants) | Show variants for an addon _(`addons`)_ |
## admin
| Command | Description |
|---------|-------------|
| [`miren admin`](/command/admin) | Call an admin method on an application |
## alias
| Command | Description |
|---------|-------------|
| [`miren alias`](/command/alias) | CLI alias management |
| [`miren alias list`](/command/alias-list) | List configured CLI aliases |
## app
| Command | Description |
|---------|-------------|
| [`miren app`](/command/app) | Get information about an application |
| [`miren app delete`](/command/app-delete) | Delete an application and all its resources |
| [`miren app history`](/command/app-history) | Show deployment history for an application |
| [`miren app list`](/command/app-list) | List all applications |
| [`miren app restart`](/command/app-restart) | Restart an application |
| [`miren app run`](/command/app-run) | Open interactive shell in a new sandbox |
| [`miren app status`](/command/app-status) | Show current status of an application |
| [`miren app versions`](/command/app-versions) | List app versions with status |
## apps
| Command | Description |
|---------|-------------|
| [`miren apps`](/command/apps) | List all applications (alias for 'app list') |
## auth
| Command | Description |
|---------|-------------|
| [`miren auth`](/command/auth) | Authentication commands |
| [`miren auth ci`](/command/auth-ci) | CI authentication binding management |
| [`miren auth ci add`](/command/auth-ci-add) | Add a CI authentication binding to an application |
| [`miren auth ci list`](/command/auth-ci-list) | List CI authentication bindings for an application |
| [`miren auth ci remove`](/command/auth-ci-remove) | Remove a CI authentication binding |
| [`miren auth generate`](/command/auth-generate) | Generate authentication config file |
| [`miren auth provider`](/command/auth-provider) | Identity provider management |
| [`miren auth provider add`](/command/auth-provider-add) | Add an identity provider for route protection |
| [`miren auth provider add github`](/command/auth-provider-add-github) | Add a GitHub identity provider |
| [`miren auth provider add oidc`](/command/auth-provider-add-oidc) | Add an OIDC identity provider |
| [`miren auth provider add password`](/command/auth-provider-add-password) | Add a shared-password identity provider |
| [`miren auth provider list`](/command/auth-provider-list) | List identity providers |
| [`miren auth provider remove`](/command/auth-provider-remove) | Remove an identity provider |
| [`miren auth provider show`](/command/auth-provider-show) | Show an identity provider |
## cluster
| Command | Description |
|---------|-------------|
| [`miren cluster`](/command/cluster) | List configured clusters |
| [`miren cluster add`](/command/cluster-add) | Add a new cluster configuration |
| [`miren cluster current`](/command/cluster-current) | Show the pinned cluster for this app |
| [`miren cluster export-address`](/command/cluster-export-address) | Export cluster address with TLS fingerprint for MIREN_CLUSTER |
| [`miren cluster list`](/command/cluster-list) | List all configured clusters |
| [`miren cluster remove`](/command/cluster-remove) | Remove a cluster from the configuration |
| [`miren cluster switch`](/command/cluster-switch) | Switch to a different cluster |
## config
| Command | Description |
|---------|-------------|
| [`miren config`](/command/config) | Configuration file management |
| [`miren config info`](/command/config-info) | Show configuration file locations and format |
| [`miren config load`](/command/config-load) | Load config and merge it with your current config |
## deploy
| Command | Description |
|---------|-------------|
| [`miren deploy`](/command/deploy) | Deploy an application |
| [`miren deploy cancel`](/command/deploy-cancel) | Cancel an in-progress deployment |
## disk
| Command | Description |
|---------|-------------|
| [`miren disk`](/command/disk) | Disk backup and recovery |
| [`miren disk backup`](/command/disk-backup) | Backup a disk to a snapshot file |
| [`miren disk list-deleted`](/command/disk-list-deleted) | List deleted disks available for recovery |
| [`miren disk restore`](/command/disk-restore) | Restore a disk from a snapshot file |
| [`miren disk undelete`](/command/disk-undelete) | Restore a recently deleted disk |
## doctor
| Command | Description |
|---------|-------------|
| [`miren doctor`](/command/doctor) | Diagnose miren environment and connectivity |
| [`miren doctor auth`](/command/doctor-auth) | Check authentication and user information |
| [`miren doctor config`](/command/doctor-config) | Check configuration files |
| [`miren doctor server`](/command/doctor-server) | Check server health and connectivity |
## download
| Command | Description |
|---------|-------------|
| [`miren download`](/command/download) | Download management commands |
| [`miren download release`](/command/download-release) | Download and extract miren release |
## env
| Command | Description |
|---------|-------------|
| [`miren env`](/command/env) | Environment variable management commands |
| [`miren env delete`](/command/env-delete) | Delete environment variables |
| [`miren env get`](/command/env-get) | Get an environment variable value |
| [`miren env list`](/command/env-list) | List all environment variables |
| [`miren env set`](/command/env-set) | Set environment variables for an application |
## help
| Command | Description |
|---------|-------------|
| [`miren help`](/command/help) | Show help for one or more commands |
## init
| Command | Description |
|---------|-------------|
| [`miren init`](/command/init) | Initialize a new application |
## login
| Command | Description |
|---------|-------------|
| [`miren login`](/command/login) | Authenticate with miren.cloud |
## logout
| Command | Description |
|---------|-------------|
| [`miren logout`](/command/logout) | Remove local authentication credentials |
## logs
| Command | Description |
|---------|-------------|
| [`miren logs`](/command/logs) | View logs (defaults to app logs) |
| [`miren logs app`](/command/logs-app) | View application logs |
| [`miren logs build`](/command/logs-build) | View build logs |
| [`miren logs sandbox`](/command/logs-sandbox) | View sandbox logs |
| [`miren logs system`](/command/logs-system) | View system logs |
## rollback
| Command | Description |
|---------|-------------|
| [`miren rollback`](/command/rollback) | Roll back to a previous version |
## route
| Command | Description |
|---------|-------------|
| [`miren route`](/command/route) | List all HTTP routes |
| [`miren route list`](/command/route-list) | List all HTTP routes |
| [`miren route protect`](/command/route-protect) | Protect an HTTP route with an identity provider |
| [`miren route remove`](/command/route-remove) | Remove an HTTP route |
| [`miren route set`](/command/route-set) | Create or update an HTTP route |
| [`miren route set-default`](/command/route-set-default) | Set an app as the default route |
| [`miren route show`](/command/route-show) | Show details of an HTTP route |
| [`miren route unprotect`](/command/route-unprotect) | Remove identity-provider protection from an HTTP route |
| [`miren route unset-default`](/command/route-unset-default) | Remove the default route |
| [`miren route waf`](/command/route-waf) | Manage WAF protection on an HTTP route |
## runner
| Command | Description |
|---------|-------------|
| [`miren runner`](/command/runner) | Runner management commands |
| [`miren runner install`](/command/runner-install) | Install systemd service for miren runner _(`distributedrunners`)_ |
| [`miren runner join`](/command/runner-join) | Join this machine to a coordinator as a runner _(`distributedrunners`)_ |
| [`miren runner list`](/command/runner-list) | List all registered runners _(`distributedrunners`)_ |
| [`miren runner remove`](/command/runner-remove) | Remove a registered runner and clean up resources _(`distributedrunners`)_ |
| [`miren runner service-status`](/command/runner-service-status) | Show miren-runner systemd service status _(`distributedrunners`)_ |
| [`miren runner start`](/command/runner-start) | Start this machine as a distributed runner _(`distributedrunners`)_ |
| [`miren runner status`](/command/runner-status) | Show runner health and configuration _(`distributedrunners`)_ |
| [`miren runner token`](/command/runner-token) | Manage join tokens |
| [`miren runner token create`](/command/runner-token-create) | Create a join token for a runner _(`distributedrunners`)_ |
| [`miren runner token list`](/command/runner-token-list) | List all join tokens _(`distributedrunners`)_ |
| [`miren runner token revoke`](/command/runner-token-revoke) | Revoke a join token _(`distributedrunners`)_ |
| [`miren runner uninstall`](/command/runner-uninstall) | Remove systemd service for miren runner _(`distributedrunners`)_ |
| [`miren runner upgrade`](/command/runner-upgrade) | Upgrade miren runner to the latest or specified version _(`distributedrunners`)_ |
| [`miren runner upgrade rollback`](/command/runner-upgrade-rollback) | Rollback runner to previous version _(`distributedrunners`)_ |
## sandbox
| Command | Description |
|---------|-------------|
| [`miren sandbox`](/command/sandbox) | Sandbox management commands |
| [`miren sandbox delete`](/command/sandbox-delete) | Delete a dead sandbox |
| [`miren sandbox exec`](/command/sandbox-exec) | Open interactive shell in an existing sandbox |
| [`miren sandbox list`](/command/sandbox-list) | List sandboxes (excludes dead by default) |
| [`miren sandbox stop`](/command/sandbox-stop) | Stop a sandbox |
## sandbox-pool
| Command | Description |
|---------|-------------|
| [`miren sandbox-pool`](/command/sandbox-pool) | Sandbox pool management commands |
| [`miren sandbox-pool list`](/command/sandbox-pool-list) | List all sandbox pools |
| [`miren sandbox-pool set-desired`](/command/sandbox-pool-set-desired) | Set desired instance count for a sandbox pool |
## server
| Command | Description |
|---------|-------------|
| [`miren server`](/command/server) | Start the miren server |
| [`miren server config`](/command/server-config) | Server configuration management commands |
| [`miren server config generate`](/command/server-config-generate) | Generate a server configuration file from current settings |
| [`miren server config validate`](/command/server-config-validate) | Validate a server configuration file |
| [`miren server docker`](/command/server-docker) | Docker-based server management commands |
| [`miren server docker install`](/command/server-docker-install) | Install miren server using Docker |
| [`miren server docker status`](/command/server-docker-status) | Show status of miren server Docker container |
| [`miren server docker uninstall`](/command/server-docker-uninstall) | Uninstall miren server Docker container |
| [`miren server install`](/command/server-install) | Install systemd service for miren server |
| [`miren server register`](/command/server-register) | Register this cluster with miren.cloud |
| [`miren server register status`](/command/server-register-status) | Show cluster registration status |
| [`miren server status`](/command/server-status) | Show miren service status |
| [`miren server uninstall`](/command/server-uninstall) | Remove systemd service for miren server |
| [`miren server upgrade`](/command/server-upgrade) | Upgrade miren server |
| [`miren server upgrade rollback`](/command/server-upgrade-rollback) | Rollback server to previous version |
## upgrade
| Command | Description |
|---------|-------------|
| [`miren upgrade`](/command/upgrade) | Upgrade miren CLI to latest version |
## version
| Command | Description |
|---------|-------------|
| [`miren version`](/command/version) | Print the version |
## whoami
| Command | Description |
|---------|-------------|
| [`miren whoami`](/command/whoami) | Display information about the current authenticated user |
---
## Advanced / Debug Commands
:::caution
These commands are intended for advanced debugging and troubleshooting. They may change without notice.
:::
| Command | Description |
|---------|-------------|
| [`miren debug`](/command/debug) | Debug and troubleshooting commands |
| [`miren debug advertise`](/command/debug-advertise) | Show which addresses the server would advertise and why |
| [`miren debug bundle`](/command/debug-bundle) | Create a support bundle with system debug information |
| [`miren debug colors`](/command/debug-colors) | Print some colors |
| [`miren debug connection`](/command/debug-connection) | Test connectivity and authentication with a server |
| [`miren debug ctr`](/command/debug-ctr) | Run ctr with miren defaults |
| [`miren debug ctr nuke`](/command/debug-ctr-nuke) | Nuke a containerd namespace |
| [`miren debug disk`](/command/debug-disk) | Disk entity debug commands |
| [`miren debug disk create`](/command/debug-disk-create) | Create a disk entity for testing |
| [`miren debug disk delete`](/command/debug-disk-delete) | Delete a disk entity |
| [`miren debug disk lease`](/command/debug-disk-lease) | Create a disk lease for testing |
| [`miren debug disk lease-delete`](/command/debug-disk-lease-delete) | Delete a disk lease entity |
| [`miren debug disk lease-list`](/command/debug-disk-lease-list) | List all disk lease entities |
| [`miren debug disk lease-release`](/command/debug-disk-lease-release) | Release a disk lease |
| [`miren debug disk lease-status`](/command/debug-disk-lease-status) | Show detailed status of a disk lease |
| [`miren debug disk list`](/command/debug-disk-list) | List all disk entities |
| [`miren debug disk migrate`](/command/debug-disk-migrate) | Migrate LSVD volume to raw disk image |
| [`miren debug disk mounts`](/command/debug-disk-mounts) | List all mounted disks from /proc/mounts |
| [`miren debug disk status`](/command/debug-disk-status) | Show status of a disk entity |
| [`miren debug entity`](/command/debug-entity) | Entity store debug commands |
| [`miren debug entity create`](/command/debug-entity-create) | Create a new entity |
| [`miren debug entity delete`](/command/debug-entity-delete) | Delete an entity |
| [`miren debug entity ensure`](/command/debug-entity-ensure) | Ensure an entity exists |
| [`miren debug entity get`](/command/debug-entity-get) | Get an entity |
| [`miren debug entity list`](/command/debug-entity-list) | List entities |
| [`miren debug entity patch`](/command/debug-entity-patch) | Patch an existing entity |
| [`miren debug entity put`](/command/debug-entity-put) | Put an entity |
| [`miren debug entity replace`](/command/debug-entity-replace) | Replace an existing entity |
| [`miren debug netdb`](/command/debug-netdb) | Network database debug commands |
| [`miren debug netdb gc`](/command/debug-netdb-gc) | Find and release orphaned IP leases |
| [`miren debug netdb list`](/command/debug-netdb-list) | List all IP leases from netdb |
| [`miren debug netdb release`](/command/debug-netdb-release) | Manually release IP leases |
| [`miren debug netdb status`](/command/debug-netdb-status) | Show IP allocation status by subnet |
| [`miren debug rbac`](/command/debug-rbac) | Fetch and display RBAC rules from miren.cloud |
| [`miren debug rbac test`](/command/debug-rbac-test) | Test RBAC evaluation with fetched rules |
| [`miren debug reindex`](/command/debug-reindex) | Rebuild all entity indexes from scratch |
| [`miren debug test`](/command/debug-test) | Debug test commands |
| [`miren debug test load`](/command/debug-test-load) | Loadtest a URL |
---
## Code of Conduct
# Miren Community Code of Conduct
## Our Pledge
We at Miren pledge to make our community welcoming, safe, and equitable for all.
We are committed to fostering an environment that respects and promotes the dignity, rights, and contributions of all individuals, regardless of characteristics including race, ethnicity, caste, color, age, physical characteristics, neurodiversity, disability, sex or gender, gender identity or expression, sexual orientation, language, philosophy or religion, national or social origin, socio-economic position, level of education, or other status. The same privileges of participation are extended to everyone who participates in good faith and in accordance with this Code of Conduct.
## Encouraged Behaviors
While acknowledging differences in social norms, we all strive to meet our community's expectations for positive behavior. We also understand that our words and actions may be interpreted differently than we intend based on culture, background, or native language.
With these considerations in mind, we agree to behave mindfully toward each other and act in ways that center our shared values, including:
1. Respecting the **purpose of our community**, our activities, and our ways of gathering.
2. Engaging **kindly and honestly** with others.
3. Respecting **different viewpoints** and experiences.
4. **Taking responsibility** for our actions and contributions.
5. Gracefully giving and accepting **constructive feedback**.
6. Committing to **repairing harm** when it occurs.
7. Behaving in other ways that promote and sustain the **well-being of our community**.
## Restricted Behaviors
Miren agrees to restrict the following behaviors in our community. Instances, threats, and promotion of these behaviors are violations of this Code of Conduct.
1. **Harassment.** Violating explicitly expressed boundaries or engaging in unnecessary personal attention after any clear request to stop.
2. **Character attacks.** Making insulting, demeaning, or pejorative comments directed at a community member or group of people.
3. **Stereotyping or discrimination.** Characterizing anyone's personality or behavior on the basis of immutable identities or traits.
4. **Sexualization.** Behaving in a way that would generally be considered inappropriately intimate in the context or purpose of the community.
5. **Violating confidentiality.** Sharing or acting on someone's personal or private information without their permission.
6. **Endangerment.** Causing, encouraging, or threatening violence or other harm toward any person or group.
7. Behaving in other ways that **threaten the well-being** of our community.
### Other Restrictions
1. **Misleading identity.** Impersonating someone else for any reason, or pretending to be someone else to evade enforcement actions.
2. **Failing to credit sources.** Not properly crediting the sources of content you contribute.
3. **Promotional materials.** Sharing marketing or other commercial content in a way that is outside the norms of the community.
4. **Irresponsible communication.** Failing to responsibly present content which includes, links or describes any other restricted behaviors.
## Reporting an Issue
Tensions can occur between community members even when they are trying their best to collaborate. Not every conflict represents a code of conduct violation, and this Code of Conduct reinforces encouraged behaviors and norms that can help avoid conflicts and minimize harm.
When an incident does occur, it is important to report it promptly. To report a possible violation, **you can private message any member of the Miren moderation team or staff.**
Community Moderators take reports of violations seriously and will make every effort to respond in a timely manner. They will investigate all reports of code of conduct violations, reviewing messages, logs, and recordings, or interviewing witnesses and other participants. Community Moderators will keep investigation and enforcement actions as transparent as possible while prioritizing safety and confidentiality. In order to honor these values, enforcement actions are carried out in private with the involved parties, but communicating to the whole community may be part of a mutually agreed upon resolution.
## Addressing and Repairing Harm
If an investigation by the Community Moderators finds that this Code of Conduct has been violated, the following enforcement ladder may be used to determine how best to repair harm, based on the incident's impact on the individuals involved and the community as a whole. Depending on the severity of a violation, lower rungs on the ladder may be skipped.
### 1. Warning
- **Event:** A violation involving a single incident or series of incidents.
- **Consequence:** A private, written warning from the Community Moderators.
- **Repair:** Examples of repair include a private written apology, acknowledgement of responsibility, and seeking clarification on expectations.
### 2. Temporarily Limited Activities
- **Event:** A repeated incidence of a violation that previously resulted in a warning, or the first incidence of a more serious violation.
- **Consequence:** A private, written warning with a time-limited cooldown period designed to underscore the seriousness of the situation and give the community members involved time to process the incident. The cooldown period may be limited to particular communication channels or interactions with particular community members.
- **Repair:** Examples of repair may include making an apology, using the cooldown period to reflect on actions and impact, and being thoughtful about re-entering community spaces after the period is over.
### 3. Temporary Suspension
- **Event:** A pattern of repeated violation which the Community Moderators have tried to address with warnings, or a single serious violation.
- **Consequence:** A private written warning with conditions for return from suspension. In general, temporary suspensions give the person being suspended time to reflect upon their behavior and possible corrective actions.
- **Repair:** Examples of repair include respecting the spirit of the suspension, meeting the specified conditions for return, and being thoughtful about how to reintegrate with the community when the suspension is lifted.
### 4. Permanent Ban
- **Event:** A pattern of repeated code of conduct violations that other steps on the ladder have failed to resolve, or a violation so serious that the Community Moderators determine there is no way to keep the community safe with this person as a member.
- **Consequence:** Access to all community spaces, tools, and communication channels is removed. In general, permanent bans should be rarely used, should have strong reasoning behind them, and should only be resorted to if working through other remedies has failed to change the behavior.
- **Repair:** There is no possible repair in cases of this severity.
This enforcement ladder is intended as a guideline. It does not limit the ability of Community Managers to use their discretion and judgment, in keeping with the best interests of our community.
## Scope
This Code of Conduct applies to all Miren community spaces, including:
- GitHub repositories (issues, pull requests, discussions)
- Discord server
- Official social media accounts
- Community events (online and offline)
It also applies when an individual is officially representing the community in public spaces, such as using an official email address or acting as an appointed representative.
## Attribution
This Code of Conduct is adapted from the Contributor Covenant, version 3.0, available at [contributor-covenant.org/version/3/0](https://www.contributor-covenant.org/version/3/0/).
Contributor Covenant is stewarded by the Organization for Ethical Source and licensed under [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/).
For answers to common questions about Contributor Covenant, see the [FAQ](https://www.contributor-covenant.org/faq). Translations are available at [contributor-covenant.org/translations](https://www.contributor-covenant.org/translations). Additional enforcement and community guideline resources can be found at [contributor-covenant.org/resources](https://www.contributor-covenant.org/resources). The enforcement ladder was inspired by the work of [Mozilla's code of conduct team](https://github.com/mozilla/inclusion).
---
## Deployment
Deployment is the core workflow of Miren — it takes your application code, builds a container image, and runs it on your server.
## How Deployment Works
When you run `miren deploy`, Miren:
1. **Uploads your files** — sends your source code to the server (after your first deploy, only changed files are transferred)
2. **Detects and builds on the server** — the server inspects your source code to detect the [language, framework](/languages), and [services](/services), then builds a container image using the detected stack (or your Dockerfile)
3. **Activates the new version** — rolls out the new version, replacing the previous one
Every deployment is tracked with a unique version ID, its current status, and the git commit it came from. You can inspect, roll back, or redeploy any previous version at any time.
## Deploying from a Project Directory
The most common workflow — run `miren deploy` from the root of your project:
```miren
cd ~/myapp
miren deploy
```
You can also deploy from a different directory with `-d`:
```miren
miren deploy -d path/to/app
```
Miren reads the app name from `.miren/app.toml`. If you haven't set up your project yet, Miren offers to run `miren init` for you. `miren init` creates `app.toml` with the app name derived from your directory, then scans the project for required environment variables and stages whatever it can — generated secrets, read-from-file values, and sensible defaults — on the app's initial config so they're available on the first deploy. See [What `miren init` Does for You](/app-configuration#what-miren-init-does-for-you) for the full picture. If this is the first deploy of the app, Miren creates it automatically on the server.
### Confirmation Prompt
If your cluster config includes multiple clusters, Miren asks you to confirm which cluster to deploy to. Skip the prompt with `--force`:
```miren
miren deploy --force
```
The prompt is also skipped automatically when only one cluster is configured or when stdin is not a terminal (e.g., in CI).
## Build Detection
Miren automatically detects how to build your application. It inspects your project files and identifies the language, framework, package manager, and entry points. See [Languages](/languages) for details on supported stacks.
Use `--analyze` to see what Miren detects without actually building or deploying:
```miren
miren deploy --analyze
```
You'll see the detected stack, services, entrypoint, and what files and frameworks influenced the result. Handy when a build isn't doing what you expect.
If a build fails, Miren displays the build errors and the deployment is marked as failed in history (visible via `miren app history`).
Use `--explain` (or `-x`) to watch each build step as it runs. This is the default in non-interactive environments; in interactive terminals, Miren shows a compact progress UI instead.
## Deploy-Time Environment Variables
Set environment variables at deploy time with `-e` and `-s`. They're applied to the app before the new version activates.
```miren
# Regular variables
miren deploy -e RELEASE_SHA=abc123 -e LOG_LEVEL=debug
# Sensitive variables (masked in output)
miren deploy -s DATABASE_URL=postgres://user:pass@host/db
# Read value from a file
miren deploy -s API_KEY=@secrets/api-key.txt
# Prompt for the value (sensitive vars mask input)
miren deploy -s SECRET_KEY
```
## Redeploying an Existing Version
Skip the build entirely and redeploy a previously built version:
```miren
miren deploy --version myapp-vCVkjR6u7744AsMebwMjGU
```
Useful for rolling forward to a known-good version without waiting for a new build.
Find version IDs with `miren app history` (see [Deployment History](#deployment-history) below).
## Rollback
`miren rollback` provides an interactive way to revert to a previous version:
```miren
miren rollback -a myapp
```
This presents a picker showing your recent successful deployments:
| Column | Description |
|--------|-------------|
| VERSION | The version ID |
| STATUS | Deployment status (active, succeeded) |
| WHEN | Relative timestamp |
| GIT SHA | Short commit hash |
| BRANCH | Git branch at the time of deploy |
Select a version and Miren redeploys it immediately. The currently active version is excluded from the list since rolling back to the current version would be a no-op.
Rollback creates a new deployment record — it doesn't erase history.
## Deployment History
View the history of deployments for an app:
```miren
miren app history -a myapp
```
### Output
```text
STATUS VERSION WHEN DEPLOYED BY
✓ myapp-vCVkjR6u7744AsMebwMjGU 2m ago paul@miren.dev
✓ myapp-vCVkjJSe4fydvxEHfhsKfA 1h ago paul@miren.dev
✗ myapp-vCVmuoeQCzjoNN9hGsu14c 3h ago paul@miren.dev
↩ myapp-vCVkjTGJhRddyZDVq9CmnN 1d ago paul@miren.dev
```
Status icons:
- **✓** — active (currently running) or succeeded
- **✗** — failed
- **↩** — rolled back
- **⟳** — in progress
- **⊘** — cancelled
You can filter by status, show full git provenance with `--detailed`, or get JSON output for scripting. See the [`miren app history` reference](/command/app-history) for all options.
## Cancelling a Deployment
Cancel an in-progress deployment by its deployment ID:
```miren
miren deploy cancel -d
```
Get the deployment ID from `miren app history --detailed`. If a CLI session is watching that deployment, it'll detect the cancellation and exit cleanly.
Only one deployment can run per app at a time. If you attempt to deploy while another is in progress, Miren tells you who started it and when. You can wait for it to finish or cancel it and start a new one.
## Git Provenance
Miren automatically captures git metadata (commit, branch, author, dirty state) from your working directory at deploy time. This information appears in `miren app history --detailed` and in the `miren rollback` picker. No configuration is needed — if you deploy from a git repo, provenance is captured automatically.
## Next Steps
- [Build & Language Detection](/languages) — How Miren detects and builds different languages and frameworks
- [App Configuration](/app-configuration) — Configure your app with `.miren/app.toml`
- [Services](/services) — Define multiple processes in your app
- [CI/CD Deployment](/ci-deploy) — Deploy from CI pipelines with OIDC authentication
- [Pull Request Environments](/pr-environments) — Deploy labeled, time-boxed previews per PR
- [app.toml Reference](/app-toml) — Complete field reference
---
## Persistent Storage
Miren provides two options for persistent storage: **Local Storage** (simple, node-local) and **Miren Disks** (managed persistent volumes). Both are configured as disks in your `app.toml`.
Both storage options are node-local — your data lives on the server where your app runs. See each section below for backup options.
## Local Storage
Local storage gives your app a persistent directory on the server's filesystem. Data survives container restarts and redeployments.
### Configuration
Add a disk with `provider = "local"` to your service in `.miren/app.toml`:
```toml
[services.web]
command = "node server.js"
[[services.web.disks]]
name = "data"
provider = "local"
mount_path = "/miren/data/local"
```
You can mount to any path — for example, directly to a database's data directory:
```toml
[services.db]
image = "postgres:16"
[[services.db.disks]]
name = "pgdata"
provider = "local"
mount_path = "/var/lib/postgresql/data"
```
### How It Works
- **Persistent**: Data survives container restarts and redeployments
- **Shared**: All containers within your app share the same storage
- **Host-local**: Data lives on the server's filesystem
- **Node-pinned**: Apps with local storage are scheduled to the coordinator node
### When to Use Local Storage
- SQLite databases
- File uploads and user content
- Application cache
- Session storage
- Any data that needs to persist across restarts
### Limitations
- **Host-local**: Data is tied to the server. If you move your app to a different server, you'll need to migrate the data manually.
- **No managed backups**: Back up your data by copying the host directory, or use your own backup tooling.
- **Shared access**: All containers in your app can read/write simultaneously—your application needs to handle concurrent access (SQLite handles this well when configured with `PRAGMA journal_mode=WAL`).
- **Node affinity**: Apps with any disk (local or miren) are pinned to the coordinator and won't be scheduled to distributed runners.
### Migrating from Automatic Local Storage
Previously, Miren automatically mounted `/miren/data/local` for every app. This is now opt-in via the disk config above.
If any of your environment variables reference `/miren/data/local`, Miren will automatically add the local storage volume for you — so most apps will keep working without changes. You'll see a log message when this happens, and we recommend adding the explicit disk config when convenient.
---
## Miren Disks
:::note Backups
Miren Disks live on your server. Back up important data with `miren disk backup` and restore it with `miren disk restore`. Cloud backup is on the [roadmap](#roadmap-cloud-backup--sync).
:::
Miren Disks provide managed persistent storage for your applications. Disks are provisioned with a specific size and filesystem, support exclusive leasing for data consistency, and persist across app restarts and redeployments.
### Why Use Disks?
- **Managed lifecycle**: Miren handles disk creation, formatting, and attachment automatically
- **Configurable size and filesystem**: Specify exactly what you need
- **Thin provisioning**: Storage is allocated as needed, not all at once
- **Persist across redeployments**: Disks survive app deletion — reattach by name
### How Disks Work
When you configure a disk for your application:
1. **Miren creates the disk** with the size and filesystem you specify
2. **Your app instance acquires a lease** on the disk (exclusive access)
3. **The disk is mounted** at the path you specified in your container
When your app stops or restarts:
- The lease is released
- Data remains on the disk
- Your next instance can acquire the lease and continue where it left off
### How Much Storage Does Miren Provide?
During the Developer Preview, we're providing unmetered storage. The intention is to implement a free tier
and usage-based pricing on the storage. We'll be sure to communicate often and clearly how we intend
to proceed.
The feature is designed to keep our costs low, and our intention is to pass that low cost on to our users.
### Configuring Disks
Add a disk to your application by including a `disks` section in your service configuration in `.miren/app.toml`:
```toml
[services.web]
image = "myapp:latest"
[[services.web.disks]]
name = "my-app-data"
mount_path = "/data"
size_gb = 10
filesystem = "ext4"
```
#### Configuration Options
| Option | Required | Description |
|--------|----------|-------------|
| `name` | Yes | Unique name for the disk (alphanumeric, hyphens allowed) |
| `mount_path` | Yes | Where to mount the disk in your container |
| `size_gb` | Yes* | Size in gigabytes (required for auto-creation) |
| `filesystem` | No | Filesystem type: `ext4` (default), `xfs`, or `btrfs` |
| `read_only` | No | Mount as read-only (default: false) |
*`size_gb` is required when the disk doesn't already exist. If the disk exists, this field is ignored.
### Example: PostgreSQL with Persistent Storage
```toml
[services.db]
image = "postgres:16"
[[services.db.env]]
key = "POSTGRES_PASSWORD"
value = "secret"
[[services.db.env]]
key = "PGDATA"
value = "/var/lib/postgresql/data/pgdata"
[[services.db.disks]]
name = "myapp-postgres"
mount_path = "/var/lib/postgresql/data"
size_gb = 20
filesystem = "ext4"
```
### Example: File Upload Storage
```toml
[services.web]
image = "myapp:latest"
[[services.web.disks]]
name = "myapp-uploads"
mount_path = "/app/uploads"
size_gb = 50
```
### Disk Lifecycle
#### Creation
Disks are automatically created when your app first deploys with a volume configuration that includes `size_gb`. The disk is provisioned with the specified size and filesystem.
#### Reuse
If you deploy an app with a `name` that matches an existing disk, Miren will attach that disk instead of creating a new one. This allows you to:
- Share data between app versions
- Preserve data across complete redeployments
- Reference disks created by other apps
#### Deletion
Disks are **not** automatically deleted when you delete an app. This is intentional - your data is precious. To delete a disk:
```miren
miren debug disk delete -i
```
### Inspecting Disks
List all disks:
```miren
miren debug disk list
```
Check a specific disk's status:
```miren
miren debug disk status -i
```
View active disk leases:
```miren
miren debug disk lease-list
```
See [CLI Reference - Disk Commands](/command/debug-disk) for complete command documentation.
### Important Considerations
#### One Instance per Disk
Disks use exclusive leasing - only one app instance can mount a disk at a time. This ensures data consistency but means:
- Multiple replicas of your app cannot share the same disk
- If you need shared storage, use separate disks per instance or external storage
#### Disk Sizing
- Disks use thin provisioning, so storage is only allocated as needed
- Choose a size that accommodates growth
#### Filesystem Choice
- **ext4**: Best general-purpose choice, widely compatible
- **xfs**: Better for large files and high-throughput workloads
**NOTE:** Your server must have the mkfs tools to format the disk types.
### Roadmap: Cloud Backup & Sync
We're building toward cloud-connected storage for Miren Disks. Here's what's planned:
- **Remote backup & restore** (next up): Trigger backups of your disks to Miren Cloud and restore them on any cluster. This extends the existing local backup/restore functionality to work remotely.
- **Automatic cloud sync**: Background replication of disk data to Miren Cloud, enabling seamless portability across clusters.
We'll update this page and the [changelog](https://miren.md/changelog) as these capabilities land.
### Next Steps
- [app.toml Reference — Disks](/app-toml#disks) — Complete field reference for disk configuration (including `lease_timeout`)
- [Services](/services) — Define services that use persistent storage
- [Getting Started](/getting-started) — Deploy your first app
- [CLI Reference - Disk Commands](/command/debug-disk) — Complete disk CLI reference
- [Miren Cloud](/miren-cloud/overview) — Set up cloud features
---
## Firewall Configuration
:::info
Miren automatically configures firewall rules during setup. Most users won't need to think about firewalls at all. This page is primarily useful for troubleshooting networking issues or understanding what Miren does under the hood.
:::
Miren automatically configures iptables rules to enable container networking. This page explains how Miren's firewall rules work and how to troubleshoot networking issues.
## How Miren Configures Firewall Rules
When Miren sets up the network bridge, it installs iptables rules in two chains:
### FORWARD Chain
The FORWARD chain controls traffic passing through the host (container-to-container and container-to-internet traffic). Miren adds rules to accept all traffic to and from the bridge interface:
```
-A FORWARD -i miren0 -j ACCEPT
-A FORWARD -o miren0 -j ACCEPT
```
### INPUT Chain
The INPUT chain controls traffic destined for the host itself. Miren adds rules to allow containers to reach host services:
| Port | Protocol | Purpose |
|------|----------|---------|
| 53 | UDP/TCP | DNS resolution (containers query host DNS) |
| 5000 | TCP | Local container registry (buildkit pushes images here) |
## Rule Ordering
Miren inserts rules at position 1 (the beginning of each chain) to ensure they take precedence over any existing restrictive rules. This is important because some cloud providers ship default firewall configurations with blanket REJECT rules.
For example, Oracle Cloud Ubuntu images have a REJECT rule at the start of the FORWARD chain by default. If Miren appended its ACCEPT rules to the end, they would never be evaluated.
## Required External Ports
If you're running Miren on a cloud provider, you'll need to configure security groups or network ACLs to allow external traffic to reach the Miren server.
### Inbound Ports
| Port | Protocol | Purpose | Required |
|------|----------|---------|----------|
| 8443 | UDP | Miren API (QUIC) - CLI and client connections | Yes |
| 80 | TCP | HTTP traffic to your applications (redirects to HTTPS) | Yes |
| 443 | TCP | HTTPS traffic to your applications | Yes |
| NodePorts | TCP/UDP | Direct L4 traffic to non-HTTP services (see [Traffic Routing](/traffic-routing)) | If using TCP/UDP services |
**Miren API (UDP 8443):** The Miren API uses QUIC (HTTP/3) over UDP. This is how the CLI communicates with the server and how remote clients connect to your cluster.
**HTTP Ingress (TCP 80/443):** Application traffic uses standard HTTP/HTTPS. Port 80 handles ACME certificate challenges and redirects to HTTPS. Port 443 serves your applications over TLS.
**NodePorts:** If your app exposes non-HTTP services with `node_port` in the port configuration, those ports must also be open. For example, an IRC server with `node_port = 6667` requires TCP port 6667 to be reachable.
### Outbound Connectivity
Miren requires outbound internet access for several operations. Most cloud providers allow all outbound traffic by default, but if you have restrictive egress rules, ensure the following destinations are reachable:
| Destination | Port | Purpose |
|-------------|------|---------|
| oci.miren.cloud | 443 | Miren's container registry (base images for builds) |
| api.miren.cloud | 443 | Miren Cloud API (authentication, cluster registration) |
| registry-1.docker.io | 443 | Docker Hub (if your app references Docker Hub images) |
| Package registries | 443 | Language-specific package managers (see below) |
| Let's Encrypt | 80/443 | ACME certificate issuance |
**During builds**, Miren pulls base images from `oci.miren.cloud` for supported language stacks (Python, Ruby, Node.js, Go, Bun). If your application references other container images, those registries must also be reachable.
**Package managers** used during builds need access to their respective registries:
- **Ruby**: rubygems.org
- **Python**: pypi.org
- **Node.js/Bun**: registry.npmjs.org
- **Go**: proxy.golang.org (and any private module sources)
- **System packages**: debian/ubuntu apt repositories, Alpine apk repositories
**Miren Cloud** connectivity is required for authentication (`miren login`) and cluster registration (`miren server install`, `miren server docker install`, `miren server register`). If you're running Miren in standalone mode without cloud features, this isn't required.
## Cloud Provider Considerations
### Oracle Cloud
Oracle Cloud instances come with restrictive default iptables rules. Miren handles this automatically by inserting rules at position 1, but be aware that:
- The default FORWARD chain has a REJECT rule that blocks all forwarding
- The default INPUT chain may not allow traffic from container networks
- You must also configure the VCN Security List to allow inbound traffic (see above)
### AWS
AWS security groups operate at the network layer outside the instance. Miren's iptables rules handle traffic inside the instance, but you still need to configure security groups to allow external traffic to reach your services (see above).
### Other Providers
Most cloud providers have some form of default firewall. Miren's rule insertion strategy should work out of the box, but if you encounter networking issues, check for conflicting rules. Remember to configure both:
1. **Cloud-level firewalls** (security groups, network ACLs) - allow external traffic to reach the instance
2. **Host-level firewalls** (iptables) - Miren handles this automatically
## Troubleshooting
### Symptoms of Firewall Issues
- "no route to host" errors during deploys
- Containers unable to resolve DNS
- Buildkit failing to push to local registry
- Intermittent network failures from sandboxes
### Diagnostic Commands
Check current iptables rules:
```bash
# View FORWARD chain
sudo iptables -L FORWARD -n -v --line-numbers
# View INPUT chain
sudo iptables -L INPUT -n -v --line-numbers
# Look for Miren's bridge interface rules (usually miren0)
sudo iptables -L -n -v | grep miren
```
Check if the bridge interface exists:
```bash
ip link show miren0
```
Verify containers can reach the host:
```bash
# From inside a sandbox, try to reach host DNS
ping 10.8.0.1 # (adjust IP to your bridge gateway)
```
### Common Issues
**Rules inserted in wrong order**: If you see REJECT rules before Miren's ACCEPT rules, the firewall may have been configured after Miren started. Restart Miren to re-insert rules at position 1.
**IPv6 issues**: Miren configures both IPv4 and IPv6 rules. If your environment doesn't support IPv6, you may see errors during bridge setup. This is usually harmless if IPv4 works.
**Conflicting firewall managers**: Tools like `ufw`, `firewalld`, or cloud-specific agents may conflict with Miren's iptables rules. If possible, configure these tools to allow Miren's traffic or disable them in favor of direct iptables management.
---
## Getting Started
Get up and running with Miren in minutes.
## Two Contexts: Server and Client
Miren runs in two places. The **server** is the Linux machine that hosts your applications. The **client** is wherever you work: your laptop, a CI runner, or even the server itself. Most `miren` commands are client commands that talk to a remote server, but a few (like `miren server install`) run directly on the server to set it up.
Throughout these docs, CLI examples are labeled SERVER or CLIENT so you always know which machine a command belongs on. When both labels appear, the command works in either context.
## Installation
Install Miren on both your server and your local machine. Head to [miren.dev/get-started](https://miren.dev/get-started) for platform-specific instructions and options.
Once installed, verify it's working on either machine:
```miren
miren version
```
### System Requirements (Server)
- **Operating System**: Linux (kernel 5.10+)
- **Architecture**: x86_64 or arm64
- **Memory**: 4GB minimum, 8GB recommended
- **Storage**: 50GB minimum, 100GB recommended
See [System Requirements](/system-requirements) for details on why these numbers matter.
## Set Up Your Server
(Skip this section if you are using our [demo cluster](#using-our-demo-cluster))
On your server, run the install command to set up the Miren runtime:
```miren
sudo miren server install
```
This will download required components, register your cluster with [miren.cloud](/miren-cloud/overview) (follow the prompts), install and start the Miren systemd service, and configure the local CLI to talk to it.
To skip cloud registration and run standalone:
```miren
sudo miren server install --without-cloud
```
### Using Our Demo Cluster
Ask for access to our demo cluster in #miren-club on [Discord](https://miren.dev/discord). Once you have access, log in and add the cluster:
```miren
miren login
```
Follow the prompts to authenticate with Miren Cloud, then bind the cluster to your local CLI:
```miren
miren cluster add
Select a cluster to bind:
NAME ORGANIZATION ADDRESS
▸ club Miren Club 34.27.122.56:8443 (+6)
```
## Deploy Your First App
Let's deploy a real app so you can see the full flow. Clone the [sample apps repo](https://github.com/mirendev/sample-apps) and navigate to the demo app:
```bash
git clone https://github.com/mirendev/sample-apps.git
cd sample-apps/demo
```
This is a small Bun web app that's already configured for Miren. Before deploying, you can preview what Miren detects:
```miren
miren deploy --analyze
```
This shows the detected stack, services, and how each service will be started. When you're ready, deploy it:
```miren
miren deploy
✓ Deploying: demo → club
✓ Upload artifacts (0.1s) - 13.4 KB at 180.5 KB/s
✓ Build & push image (7.8s) - 9 steps completed
Updated version demo-vCZpcrAAaU6mULzMSSBwc4 deployed. All traffic moved to new version.
No routes configured for this app.
To set a hostname, try: miren route set demo.cluster-jwomf2l0tn8z.miren.systems demo
```
Your app is deployed and running! Miren suggests a hostname on your cluster's `.miren.systems` subdomain. If your cluster is publicly accessible, go ahead and set that route:
```miren
miren route set demo.cluster-jwomf2l0tn8z.miren.systems demo
```
Then open the URL in your browser to see the demo app.

:::note Not publicly accessible?
If your server is behind a firewall or on a private network, `miren route set` will still configure the route, but you won't be able to reach it from outside. You can verify your app is running with `miren app list` and `miren logs` instead. See [Firewall](/firewall) for details on making your cluster reachable.
:::
## See It Running
Check that your app deployed successfully:
```miren
miren app list
NAME VERSION SCALE ROUTE
demo demo-vCZpcrAAaU6mULzMSSBwc4 1 demo.cluster-jwomf2l0tn8z.miren.systems
```
You can also tail the logs to see requests coming in:
```miren
miren logs
S 2026-04-07 10:08:12: Server running at http://localhost:3000
S 2026-04-07 10:08:15: GET / 200 2ms
S 2026-04-07 10:08:15: GET /images/Miren-Logo-Secondary.svg 200 1ms
```
That's it! You have an app running on Miren.
## Next Steps
Now that you've got something deployed, here's where to go depending on what you need.
**Deploy your own app.** Miren auto-detects Python, Node, Bun, Go, Ruby, and Rust projects. Run `miren init` in your project to create a [`.miren/app.toml`](/app-configuration) config, then `miren deploy`. You can always provide a `Dockerfile` if you need full control over the build.
**Manage multiple clusters.** If you have more than one server, use [`miren cluster`](/command/cluster) to list your clusters and `miren cluster switch` to change which one you're targeting before deploying.
**Configure your app.** The [App Configuration](/app-configuration) guide covers `.miren/app.toml` in depth: setting commands, ports, environment variables, concurrency, and more.
**Set up routes.** Your first app gets a default route, but additional apps need explicit routing. See [Routes](/traffic-routing) for custom domains and path-based routing.
**Scale your app.** Miren autoscales by default (like Cloud Run), spinning instances up and down with traffic. If you need fixed instance counts for things like databases or workers, see [Application Scaling](/scaling).
**Add persistent storage.** [Disks](/disks) let you attach storage volumes to services, with automatic backup to Miren Cloud.
**Explore the CLI.** The full [CLI Reference](/commands) documents every command and flag.
**Get help.** If something isn't working, check [Troubleshooting](/troubleshooting) or ask in #miren-club on [Discord](https://miren.dev/discord).
---
## Miren
:::tip[Working with an AI coding agent?]
Point it at [llms.txt](https://miren.md/llms.txt) or [llms-full.txt](https://miren.md/llms-full.txt) for LLM-friendly docs, or install [Miren agent skills](https://github.com/mirendev/miren-skills) so it can deploy and manage your apps directly.
:::
# Miren
Miren is a container platform for small teams. You install it on a Linux server, point your CLI at it, and deploy with `miren deploy`. It handles builds, scaling, routing, TLS, and backups so you don't have to stitch together a platform from parts.
## How it works
Miren has two sides. The **server** runs on your Linux machine and manages containers, networking, and storage. The **client** is the `miren` CLI on your laptop (or CI runner), which talks to the server over a secure connection.
You deploy apps by running `miren deploy` from your project directory. Miren detects your language (Python, Node, Bun, Go, Ruby, Rust, or a Dockerfile), builds a container image, and runs it. Your first app gets a route automatically. Scaling is automatic by default, adjusting instance counts based on traffic.
Configuration lives in `.miren/app.toml` in your project. Environment variables, secrets, services, scaling behavior, and persistent disks are all managed through the CLI and this config file.
## Get started
**[Getting Started](/getting-started)** walks you through installation, server setup, and deploying your first app in a few minutes.
## Learn more
- [App Configuration](/app-configuration) - `.miren/app.toml` and how to configure your app
- [Deployment](/deployment) - Deploy workflows, rollbacks, and CI integration
- [Pull Request Environments](/pr-environments) - Time-boxed preview deploys, one per PR
- [Services](/services) - Run multiple processes (web, workers, databases) in one app
- [Scaling](/scaling) - Autoscaling and fixed instance modes
- [Traffic Routing](/traffic-routing) - Custom domains, TLS, and path-based routing
- [Disks](/disks) - Persistent storage with automatic cloud backup
- [Logs](/logs) - Application, build, and system logs
- [CLI Reference](/commands) - Every command and flag
---
## Miren Labs
Miren Labs is where we ship experimental features that aren't quite ready for prime time. These are capabilities we're actively developing and want to get into your hands early for feedback.
## What to Expect
Labs features are:
- **Experimental** — APIs and behavior may change based on feedback
- **Opt-in** — Disabled by default, you choose when to try them
- **Supported** — We want to hear about bugs and rough edges
- **On a path** — Most labs features are headed toward stable release
## Enabling Labs Features
Labs features are controlled via the `--labs` flag or `MIREN_LABS` environment variable when starting the Miren server.
```miren
# Enable a single labs feature
miren server --labs adminapi
# Enable multiple features
miren server --labs adminapi --labs distributedrunners
# Via environment variable
MIREN_LABS=adminapi miren server
# Multiple features via environment variable (comma-separated)
MIREN_LABS=adminapi,distributedrunners miren server
```
## Giving Feedback
We'd love to hear how labs features work for you:
- **What's working well** — Helps us know we're on the right track
- **What's confusing** — Documentation gaps, unclear behavior
- **What's broken** — Bugs, crashes, unexpected behavior
- **What's missing** — Features that would make it useful for your use case
Reach out via [Discord](https://miren.dev/discord) or open an issue on [GitHub](https://github.com/mirendev/runtime/issues).
## Current Labs Features
Individual labs features are documented alongside their related functionality. Look for the "Labs Feature" callout in the docs.
---
## Supported Languages
Miren automatically detects your application's language and configures the build environment. When you run `miren deploy`, your project files are analyzed to determine the appropriate build stack.
## Ruby
**Detection:** Presence of `Gemfile`
**Default Version:** 3.2
Miren detects Ruby applications by looking for a `Gemfile`. Dependencies are installed using Bundler with production settings.
### Build Process
1. Installs system dependencies (build-essential, libpq-dev, nodejs, libyaml-dev, postgresql-client)
2. Runs `bundle install` with `BUNDLE_WITHOUT=development`
3. If Bootsnap is detected, precompiles the cache
4. If a Rakefile exists, runs `rake assets:precompile` (if available)
### Entrypoint Detection
Miren automatically detects and configures the appropriate web server:
| Framework | Entrypoint |
|-----------|------------|
| Rails | `bundle exec rails server -b 0.0.0.0 -p $PORT` |
| Puma (with config) | `bundle exec puma -C config/puma.rb` |
| Puma (without config) | `bundle exec puma -b tcp://0.0.0.0 -p $PORT` |
| Rack | `bundle exec rackup -p $PORT` |
### Environment Variables
The following environment variables are set automatically:
- `BUNDLE_PATH=/usr/local/bundle`
- `BUNDLE_WITHOUT=development`
- `RACK_ENV=production`
- `RAILS_ENV=production` (for Rails apps)
### Example Procfile
```
# Rails application
web: bundle exec rails server -b 0.0.0.0 -p $PORT
# Puma with config file
web: bundle exec puma -C config/puma.rb
# Sidekiq background worker
worker: bundle exec sidekiq
```
---
## Python
**Detection:** Presence of `requirements.txt`, `Pipfile`, `pyproject.toml`, or `uv.lock`
**Default Version:** 3.11
Miren supports four Python dependency management systems, detected in priority order.
### Dependency Management
| File | Package Manager | Install Command |
|------|-----------------|-----------------|
| `Pipfile` | pipenv | `pipenv install --deploy` |
| `uv.lock` | uv | `uv sync --frozen` |
| `pyproject.toml` | poetry | `poetry install --no-root` |
| `requirements.txt` | pip | `pip install -r requirements.txt` |
### Framework Detection
Miren automatically detects popular Python web frameworks and configures the start command:
| Framework | Detection | Start Command |
|-----------|-----------|---------------|
| FastAPI | `fastapi` in dependencies | `fastapi run` |
| Django | `django` in dependencies | `gunicorn` or `uvicorn` |
| Flask | `flask` in dependencies | `gunicorn` |
| Gunicorn | `gunicorn` in dependencies | `gunicorn` |
| Uvicorn | `uvicorn` in dependencies | `uvicorn` |
### Example Procfile
```
# pip with gunicorn
web: gunicorn app:app --bind 0.0.0.0:$PORT
# pip with uvicorn (FastAPI/Starlette)
web: uvicorn main:app --host 0.0.0.0 --port $PORT
# FastAPI (auto-detected)
web: fastapi run
# uv
web: uv run gunicorn app:app --bind 0.0.0.0:$PORT
# Pipenv
web: pipenv run gunicorn app:app --bind 0.0.0.0:$PORT
# Poetry
web: poetry run gunicorn app:app --bind 0.0.0.0:$PORT
# Celery worker
worker: celery -A tasks worker --loglevel=info
```
---
## Node.js
**Detection:** `package.json` AND (`package-lock.json` OR `yarn.lock` OR Procfile with `web: node|npm|yarn`)
**Default Version:** 20
Miren detects Node.js applications and automatically uses the appropriate package manager.
### Package Manager Detection
| Lock File | Package Manager | Install Command |
|-----------|-----------------|-----------------|
| `yarn.lock` | yarn | `yarn install` |
| `package-lock.json` | npm | `npm install` |
### Example Procfile
```
# Direct node execution
web: node server.js
# Using npm scripts
web: npm start
# Using yarn
web: yarn start
# Express.js
web: node dist/index.js
# Next.js
web: npm run start
# Background worker
worker: node worker.js
```
---
## Bun
**Detection:** `package.json` AND (`bun.lock` OR Procfile with `web: bun`)
**Default Version:** 1
Miren detects Bun applications by the presence of `bun.lock` or a Bun command in the Procfile.
### Example Procfile
```
# Run TypeScript directly
web: bun run src/index.ts
# Run JavaScript
web: bun run src/index.js
# Using bun scripts from package.json
web: bun run start
# Elysia framework
web: bun run src/server.ts
# Background worker
worker: bun run worker.ts
```
---
## Go
**Detection:** Presence of `go.mod`
**Default Version:** Parsed from `go.mod`, or 1.23
Miren builds Go applications to a single binary at `/bin/app`.
### Build Process
1. Installs git and ca-certificates (for private dependencies)
2. Downloads modules (or uses vendor directory if present)
3. Builds the binary to `/bin/app`
### Command Directory Detection
Miren looks for your main package in the `cmd/` directory:
1. If `cmd/` contains a single subdirectory, that's used
2. If `cmd/` contains a subdirectory matching the app name, that's used
3. Otherwise, builds from the project root
### Vendored Dependencies
If your project has a `vendor/` directory, Miren uses `-mod=vendor` for faster builds.
### Example Procfile
```
# Run the compiled binary
web: /bin/app
# With flags
web: /bin/app -addr=0.0.0.0:$PORT
# Background worker
worker: /bin/app -mode=worker
# Scheduler
scheduler: /bin/app -mode=scheduler
```
---
## Rust
**Detection:** Presence of `Cargo.toml`
**Default Version:** 1.83
Miren builds Rust applications using Cargo and produces a single binary at `/bin/app`.
### Build Process
1. Uses the official Rust base image
2. Runs `cargo build --release`
3. Copies the binary to `/bin/app`
### Binary Name Detection
Miren reads your `Cargo.toml` to determine the binary name:
1. Uses the `[[bin]]` name if specified
2. Falls back to the package name from `[package]`
### Example Procfile
```
# Run the compiled binary
web: /bin/app
# With environment-based port
web: /bin/app
# Background worker
worker: /bin/app --mode worker
```
### Example Cargo.toml
```toml
[package]
name = "myapp"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
```
---
## Using Dockerfile.miren
For applications that require custom build steps or don't fit the automatic detection, you can provide a `Dockerfile.miren` in your project root.
### When to Use Dockerfile.miren
- Your application requires custom system dependencies
- You need a multi-stage build
- You're using a language not listed above
- You need specific build-time configurations
### Example
```dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/index.js"]
```
### Build Priority
1. `build.dockerfile` setting in `app.toml` (if specified)
2. `Dockerfile.miren` in project root
3. Automatic language detection
### Build Arguments
The following build arguments are available in your Dockerfile.miren:
- `MIREN_VERSION` - The version identifier for this build
---
## Specifying Language Version
Override the default language version in `.miren/app.toml`:
```toml
[build]
version = "3.12" # e.g., Python 3.12
```
For Go, the version is automatically parsed from your `go.mod` file.
---
## Build Customization
The `[build]` section in `.miren/app.toml` supports additional options:
```toml
[build]
version = "3.2" # Language/runtime version
dockerfile = "Dockerfile" # Use a specific Dockerfile
onbuild = [ # Commands to run after dependencies install
"npm run build",
"npm prune --production"
]
```
The `onbuild` commands run in the `/app` directory after the main build steps complete.
---
## Next Steps
- [app.toml Reference — Build](/app-toml#build) — Complete field reference for build settings
- [Services](/services) — Configure multiple processes
- [Scaling](/scaling) — Set up autoscaling
- [Getting Started](/getting-started) — Deploy your first app
---
## Logs
Miren captures logs from your applications, sandboxes, builds, and the system itself. Logs are stored in [VictoriaLogs](https://docs.victoriametrics.com/victorialogs/) and queryable through the `miren logs` command and its subcommands.
## Subcommands
| Subcommand | Description |
|------------|-------------|
| `miren logs app` | Application logs (default when no subcommand is given) |
| `miren logs sandbox` | Logs from a specific sandbox instance |
| `miren logs build` | Build output for a specific app version |
| `miren logs system` | Miren server internal logs |
Running `miren logs` without a subcommand shows app logs (backward compatible).
## Viewing app logs
App logs are the most common use case. By default, `miren logs` shows the last 100 lines from your current app:
```miren
# Show recent logs for the current app
miren logs
# Show logs for a specific app
miren logs app -a myapp
# Filter by service
miren logs app -a myapp --service web
# Show logs from the last 30 minutes
miren logs app --last 30m
# Follow logs in real-time
miren logs app -a myapp -f
```
## Build logs
View the output from a specific build to debug deployment failures:
```miren
# Show build logs for the latest version
miren logs build -a myapp
# Show build logs for a specific version
miren logs build -a myapp VERSION
```
Replace `VERSION` with the version from `miren app history`.
## Sandbox logs
View logs from a specific sandbox instance, useful for debugging issues in individual containers:
```miren
# Show logs for a specific sandbox
miren logs sandbox -s SANDBOX_ID
```
Use `miren sandbox list` to find sandbox IDs.
## System logs
View Miren server internal logs for debugging server-level behavior:
```miren
miren logs system
```
System logs contain output from the Miren server process itself — controller reconciliation, RPC calls, sandbox lifecycle events, and other internal operations. Use these when debugging server behavior rather than application issues.
## Filtering with grep
Use `--grep` (or `-g`) to filter log output. The filter supports multiple syntax options:
| Syntax | Description | Example |
|--------|-------------|---------|
| `word` | Match logs containing "word" (case-insensitive) | `error` |
| `"phrase"` | Match logs containing exact phrase | `"connection failed"` |
| `'phrase'` | Match logs containing exact phrase (alternate) | `'connection failed'` |
| `/regex/` | Match logs matching regex pattern | `/err(or)?/` |
| `-term` | Exclude logs matching term | `-debug` |
| `term1 term2` | Match logs containing ALL terms (AND) | `error timeout` |
- **Case-insensitive**: All word and phrase matches are case-insensitive
- **AND logic**: Multiple terms must all match for a log line to be included
- **Negation**: Prefix any term with `-` to exclude matching lines
- **Quotes**: Use double (`"`) or single (`'`) quotes for phrases with spaces
- **Regex**: Enclose patterns in forward slashes (`/pattern/`) for regex matching
```miren
# Filter for errors
miren logs -g error
# Exclude debug lines
miren logs -g "-debug"
# Match a phrase
miren logs -g '"connection refused"'
# Combine filters (must match both)
miren logs -g "error timeout"
```
## Time ranges
By default, logs show the last 100 lines. Use `--last` to specify a time range:
```miren
# Last 5 minutes
miren logs --last 5m
# Last hour
miren logs --last 1h
# Last 24 hours
miren logs --last 24h
```
## Following logs
Use `--follow` (or `-f`) to stream logs in real-time:
```miren
# Follow all logs
miren logs -f
# Follow logs for a specific app
miren logs app -a myapp -f
```
## Output format
Log entries are displayed with the following format:
```
S 2024-01-15 10:30:45: [source] Log message here
```
| Prefix | Meaning |
|--------|---------|
| `S` | stdout |
| `E` | stderr |
| `ERR` | error |
| `U` | user out-of-band |
Each entry includes a timestamp and an optional source identifier (such as a truncated sandbox ID) to help you trace which instance produced the log line.
---
## Cloud Updates
# Updates
What's new in Miren Cloud.
## March 2026
- **App Subdomains** - Claim custom subdomains (e.g. `mycluster.run.garden`, `mycluster.miren.app`) and assign them to your clusters with automatic DNS provisioning. See [Subdomains](/miren-cloud/subdomains).
## January 2026
- Fixed registration expiration display showing incorrect dates
## December 2025
*Developer Preview launch!*
- **Organizations and RBAC** - Clusters are organized under organizations with role-based access control
- **Cluster DNS hostnames** - Clusters get auto-provisioned DNS hostnames with easy copy-to-clipboard in the UI
- **Cluster registration** - Register your Miren cluster with the cloud for centralized management
---
## Miren Cloud
Miren Cloud is a central control plane that connects and manages your Miren clusters. While Miren runs fully standalone on your own infrastructure, connecting to Miren Cloud gives you:
- Team management and access control
- Automatic data backup and sync
- Multi-environment workflows
- [Custom subdomains](/miren-cloud/subdomains) for your apps (e.g. `mycluster.run.garden`)
## Miren Server Installation (with Cloud)
When you run `miren server install`, it will automatically register a new cluster to Miren Cloud and redirect you to create your miren.cloud organization and account:
**NOTE:** The install requires systemd at present.
```bash
sudo miren server install
```
By default, you will have full access to your new cluster. Permissions can be tweaked using RBAC rules if needed.
### Miren Server Installation within Docker
If you're on a platform other than Linux (or a Linux platform without systemd available), you can install
the server into a docker container:
```bash
miren server docker install
```
### Install Standalone
To skip cloud registration and run standalone:
```bash
sudo miren server install --without-cloud
```
## Login
Authenticate with miren.cloud:
```bash
miren login
```
This will open a browser window to complete authentication.
## Check Your Identity
See who you're logged in as:
```bash
miren whoami
```
## Register Your Cluster
Connect your local cluster to miren.cloud:
```bash
miren server register -n my-cluster
```
This registers your cluster and enables cloud features.
**NOTE**: By default, servers are already registered when doing `miren server install`.
## View Your Clusters
List all clusters associated with your account:
```bash
miren cluster list
```
## Switch Clusters
If you have multiple clusters, switch between them:
```bash
miren cluster switch my-other-cluster
```
---
## Subdomains
You've got a Miren cluster running your stuff — now give it an address people can actually visit. You can always [bring your own domain](/traffic-routing#custom-domains), but if you'd rather skip the DNS busywork, Miren Cloud lets you claim a subdomain like `mycluster.run.garden`, point it at your cluster, and you're live.
## Available Base Domains
You can claim subdomains under two base domains:
| Domain | Notes |
|--------|-------|
| `run.garden` | Great for running your [garden server](https://miren.dev/blog/garden-server) |
| `miren.app` | A clean, professional option for your projects |
Both come with wildcard DNS — once you claim `mycluster.run.garden`, requests to `*.mycluster.run.garden` are routed to your cluster too. That means each of your apps can get its own hostname without any extra setup.
## Claiming a Subdomain
Head to your organization in [miren.cloud](https://miren.cloud), find the **Subdomains** section, and pick a name.

Names are 3–63 characters — lowercase letters, numbers, and hyphens. A handful of common names like `www` and `api` are reserved.
## Assigning to a Cluster
Once you've claimed a subdomain, click on it and choose **Assign to Cluster**. Pick one of your active clusters and Miren takes care of the rest — CNAME and wildcard DNS records are provisioned automatically. Propagation usually takes a few minutes.
## Routing Traffic to Your App
With the subdomain assigned to your cluster, the last step is telling Miren which app should handle the traffic:
```bash
miren route set mycluster.run.garden myapp
```
You can also route wildcard subdomains to an app — handy if you want each app on its own sub-subdomain:
```bash
miren route set '*.mycluster.run.garden' myapp
```
See [Traffic Routing](/traffic-routing) for more on how routes work.
## Good to Know
**Wildcard DNS** — When you assign a subdomain, Miren provisions both the base name and a wildcard (`*.mycluster.run.garden`), so you can give every app its own hostname or build multi-tenant setups without touching DNS again.
**TLS certificates** — Miren provisions certificates automatically for your subdomains via [Let's Encrypt](/tls). This is especially relevant for `miren.app` subdomains, since the `.app` TLD requires HTTPS in all browsers.
**DNS propagation** — Records are provisioned on assignment and usually resolve within a few minutes, though in rare cases it can take up to an hour.
**Limits** — During Developer Preview, each organization can claim up to 10 subdomains.
## Next Steps
- [Traffic Routing](/traffic-routing) — Set up routes to direct traffic to your apps
- [TLS Certificates](/tls) — How Miren handles HTTPS
- [Miren Cloud Overview](/miren-cloud/overview) — Cluster registration, login, and team management
---
## Observability
Miren instruments the request lifecycle with [OpenTelemetry](https://opentelemetry.io/) distributed tracing. Traces flow from the initial HTTP request through internal routing, sandbox management, and container operations, giving you visibility into what's happening at every layer.
## What Miren Traces
Every HTTP request that arrives at Miren generates a trace with spans covering:
- **httpingress** — The full request lifecycle: routing, lease acquisition, and proxying to your app
- **httpingress.lease** — Sandbox lease management, including whether a cached lease was used or a cold start was required
- **RPC calls** — Internal service-to-service communication within Miren
- **containerd gRPC** — Container operations like image pulls, container creation, and task management
The most useful spans for app developers are `httpingress` (overall request latency) and `httpingress.lease` (cold start visibility). The RPC and containerd spans are primarily useful for operators debugging Miren itself.
## Trace Context Propagation
Miren participates in [W3C Trace Context](https://www.w3.org/TR/trace-context/) propagation in both directions:
**Inbound:** If your request includes a `traceparent` header, Miren continues that trace rather than starting a new one. This means requests from an instrumented frontend or upstream service produce a single connected trace that includes Miren's processing.
**Outbound:** When Miren forwards a request to your app, it injects a `traceparent` header. Your app can pick this up to create child spans that appear in the same trace as the Miren infrastructure spans.
## Connecting Your App's Traces
Miren's tracing and your app's tracing are configured independently — Miren handles its own trace export, and your app handles its own. The `traceparent` header is what connects them: when both sides send traces to the same backend, they show up as one unified trace because they share the same trace ID.
To participate, add an OpenTelemetry SDK to your app and point it at your OTLP-compatible backend. The SDK will automatically read the `traceparent` header from incoming requests and create child spans.
Set these environment variables on your app in `.miren/app.toml`:
```toml
[[env]]
key = "OTEL_EXPORTER_OTLP_ENDPOINT"
value = "https://your-otel-collector:4318"
[[env]]
key = "OTEL_EXPORTER_OTLP_HEADERS"
value = "Authorization=Bearer your-api-key"
[[env]]
key = "OTEL_SERVICE_NAME"
value = "my-app"
```
This works with any OTel-compatible backend: Grafana Tempo, Honeycomb, Datadog, Jaeger, and others. You can use the same backend as your Miren cluster or a different one — as long as traces with the same trace ID end up in the same place, they'll be correlated.
### Python Example
Using the OpenTelemetry auto-instrumentation for Flask:
```bash
pip install opentelemetry-distro opentelemetry-exporter-otlp
opentelemetry-bootstrap -a install
```
```toml
[[env]]
key = "OTEL_EXPORTER_OTLP_ENDPOINT"
value = "https://your-otel-collector:4318"
[[env]]
key = "OTEL_SERVICE_NAME"
value = "my-flask-app"
```
```text
# Procfile
web: opentelemetry-instrument flask run --host 0.0.0.0 --port 3000
```
The `opentelemetry-instrument` wrapper automatically reads the `traceparent` header from incoming requests and creates spans for your Flask routes.
### Node.js Example
Using the OpenTelemetry auto-instrumentation for Node.js:
```bash
npm install @opentelemetry/auto-instrumentations-node
```
```toml
[[env]]
key = "OTEL_EXPORTER_OTLP_ENDPOINT"
value = "https://your-otel-collector:4318"
[[env]]
key = "OTEL_SERVICE_NAME"
value = "my-node-app"
[[env]]
key = "NODE_OPTIONS"
value = "--require @opentelemetry/auto-instrumentations-node/register"
```
The `--require` flag loads the auto-instrumentation before your app starts, automatically instrumenting HTTP, Express, and other common libraries.
## What a Trace Looks Like
A typical request trace shows the full path through Miren:
```text
httpingress [350ms]
├─ httpingress.lease [200ms] (cold start)
│ ├─ rpc.call.AcquireLease [195ms]
│ │ ├─ containerd...Images/Pull [150ms]
│ │ └─ containerd...Tasks/Create [40ms]
├─ [proxy to app] [150ms]
│ └─ my-app: GET /api/users [145ms] (your app's span)
```
On a warm request where a sandbox is already running:
```text
httpingress [15ms]
├─ httpingress.lease [0.1ms] (cached lease)
├─ [proxy to app] [14ms]
│ └─ my-app: GET /api/users [12ms]
```
## Next Steps
- [Logs](/logs) — View and filter application, build, and system logs
- [Services](/services) — Configure your app's services
- [Application Scaling](/scaling) — Understand cold starts and autoscaling
---
## Pull Request Environments
A pull request environment is a labeled build of your app — called an **ephemeral version** in Miren — that runs alongside the active version on its own subdomain. It runs separately from your normal deploys and is deleted automatically when its TTL expires. The typical use is one preview per PR, reachable at something like `pr-123.myapp.example.com`.
## How It Works
When you run `miren deploy --ephemeral `:
1. Miren builds the version but doesn't activate it. The active version keeps serving production traffic.
2. The new version is reachable at `.`. A request for any subdomain of an existing route is looked up against ephemeral labels for that route — no separate route entity needed.
3. After the TTL elapses (default 24 hours), a background controller deletes the version.
Ephemeral deploys don't create deployment history records, don't take the deployment lock, and don't block normal deploys.
## Quick Start
**Step 1: Point DNS at your cluster for the subdomains you'll use.** A wildcard CNAME is the usual choice:
```text
*.myapp.example.com. CNAME cluster-jwomf2l0tn8z.miren.systems.
```
You don't need to configure a wildcard route on your server. Any existing route for `myapp.example.com` will pick up `pr-123.myapp.example.com` as an ephemeral lookup. See [Custom Domains](/traffic-routing#custom-domains) for the full DNS setup.
**Step 2: Deploy with `--ephemeral` and an optional TTL.**
```miren
miren deploy --ephemeral pr-123 --ttl 48h
```
Miren builds the version and prints the access URL:
```text
Ephemeral version myapp-vXYZ created.
Label: pr-123
TTL: 48h
URL: https://pr-123.myapp.example.com
```
Open the URL — your preview is live. TLS provisions on first request.
## What Runs in an Ephemeral Version
**Only the `web` service runs.** Workers, background jobs, scheduled tasks, and any other services defined in `.miren/app.toml` aren't started for ephemeral versions — HTTP traffic to the subdomain is the only thing wired up. If reviewing your PR requires a worker too, use a separate staging app instead.
**Configuration is shared with the active version.** Env vars, secrets, addons, and build config all come from the app's current settings. There's no per-ephemeral override:
- Your preview connects to the same database, queues, and external services as production.
- You can't change `RAILS_ENV`, feature flags, or service URLs just for the preview.
- Updates to app config (via `miren config set`, addons, etc.) affect both active and ephemeral versions.
If you need isolation from production data, run ephemeral deploys against a separate staging app.
## Using a Staging App
Because ephemeral versions share config with the active version, a preview deployed against your production app talks to your production database, queues, and external services. That's fine for read-mostly UI changes, but risky as soon as a PR writes data, runs migrations, or fires background jobs.
Set up a second app — typically `myapp-staging` — that points at a staging database and any other backing services you want isolated, then run all PR previews against that app instead of production.
**Step 1: Create the staging app.** Deploy your main branch to it with whatever staging-specific config you want:
```miren
miren deploy -a myapp-staging \
-e DATABASE_URL=postgres://staging-db.internal/myapp \
-e RAILS_ENV=staging
```
**Step 2: Add a route for the staging app and wildcard DNS for its subdomains.**
```miren
miren route set staging.myapp.example.com myapp-staging
```
```text
*.staging.myapp.example.com. CNAME cluster-jwomf2l0tn8z.miren.systems.
```
**Step 3: Run PR previews against the staging app.**
```miren
miren deploy -a myapp-staging --ephemeral pr-123 --ttl 48h
```
The preview is reachable at `pr-123.staging.myapp.example.com`, isolated from production data. Redeploy the staging app's active version periodically (or on every push to `main`) to keep its baseline fresh — ephemeral previews inherit the staging app's current config and addons, not production's.
In CI, set `MIREN_APP=myapp-staging` (or pass `app: myapp-staging` to the deploy action) so PR workflows always target staging.
### Using a Staging Cluster
Another pattern is to setup a separate staging cluster that you deploy the PRs to, rather than your production cluster. This lets you isolate the preview even more.
## Labels
Labels are used as DNS subdomains, so they must be DNS-compliant (RFC 1123):
- Lowercase alphanumeric characters and hyphens only
- Must start and end with an alphanumeric character
- Max 63 characters
Miren normalizes common separators for you — underscores, slashes, and dots become hyphens, uppercase is lowercased, and other characters are stripped:
| Input | Normalized |
|-------|------------|
| `feat/login` | `feat-login` |
| `My_Branch.v2` | `my-branch-v2` |
| `PR-123` | `pr-123` |
So a Git branch name usually works as-is:
```miren
miren deploy --ephemeral "$(git rev-parse --abbrev-ref HEAD)"
```
**Redeploying with the same label replaces the prior version.** Push a new commit, redeploy with `--ephemeral pr-123`, and the previous `pr-123` version is deleted before the new one becomes reachable. The TTL resets on each deploy.
## TTL and Cleanup
`--ttl` takes a Go duration string (`30m`, `2h`, `48h`) and defaults to `24h`. Expiration is fixed at deploy time.
Requests to an expired label return 404 immediately — the ephemeral lookup filters expired versions itself, so the cutoff is enforced as soon as the timestamp passes. A background controller sweeps the actual entities every five minutes to free their resources. There's no extend command — redeploy with the same label to refresh the TTL.
**Per-app limit.** Each app can have at most 10 ephemeral versions at once. Deploying an 11th evicts the version nearest to expiry. Replacing an existing label doesn't count against the limit, since the old version is deleted before the new one is created.
## Listing Ephemeral Versions
Show only ephemeral versions for an app:
```miren
miren app versions --ephemeral
```
```text
VERSION LABEL CREATED EXPIRES
myapp-vCVkjR6u7744AsMebwMjGU pr-123 2m ago 2026-05-28 14:00:00
myapp-vCVkjJSe4fydvxEHfhsKfA pr-118 3h ago 2026-05-28 11:30:00
```
`--format json` is supported for scripting. Drop `--ephemeral` to see all versions, with ephemeral ones marked.
Ephemeral deploys don't appear in `miren app history` — that command shows only tracked deployments of the active version.
## GitHub Actions: Per-PR Previews
To deploy a preview per pull request from GitHub Actions, pair this with [CI/CD Deployment with OIDC](/ci-deploy) so no secrets land in your repo. The example below targets a staging app — see [Using a Staging App](#using-a-staging-app) for why that's the recommended setup.
**Step 1: Allow `pull_request` events on the OIDC binding.**
`miren auth ci --github` permits `push` and `workflow_dispatch` by default. Add `pull_request`:
```miren
miren auth ci myapp-staging --github acme/web-app \
--allowed-events push,workflow_dispatch,pull_request
```
**Step 2: Add the workflow.**
```yaml
name: PR Preview
on:
pull_request:
types: [opened, synchronize, reopened]
permissions:
id-token: write
contents: read
jobs:
preview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy preview
id: deploy
uses: mirendev/actions/deploy@main
with:
cluster: ${{ secrets.MIREN_CLUSTER }}
app: myapp-staging
ephemeral: pr-${{ github.event.pull_request.number }}
ttl: 48h
```
Each push replaces the previous preview at the same label. When the PR is merged or closed, the version expires on its own — no teardown step needed.
The deploy action exposes the preview URL as a step output, so a follow-up step can post it as a PR comment:
```yaml
- name: Comment preview URL
uses: actions/github-script@v7
with:
script: |
const url = '${{ steps.deploy.outputs.url }}';
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `Preview deployed: ${url}`,
});
```
## Limitations
- **`web` service only** — workers and other services from your app config don't start (see [What Runs in an Ephemeral Version](#what-runs-in-an-ephemeral-version)).
- **Shared configuration** — env vars, secrets, and addons all come from the app. No per-preview overrides.
- **No deployment history** — `miren app history`, `miren rollback`, and the deployment lock all ignore ephemeral deploys.
- **10 per app** — older versions are evicted by expiry as new ones arrive.
- **DNS must cover the subdomains** — without a wildcard CNAME (or per-label records) pointing at your cluster, the URL won't resolve.
## Command Reference
The flags introduced on this page are `--ephemeral` and `--ttl` on [`miren deploy`](/command/deploy), and `--ephemeral` on [`miren app versions`](/command/app-versions). Those reference pages have the full flag listings.
## Next Steps
- [Deployment](/deployment) — How normal deploys work
- [Traffic Routing](/traffic-routing) — Routes, wildcard DNS, and custom domains
- [CI/CD Deployment with OIDC](/ci-deploy) — Deploy from GitHub Actions without stored secrets
- [TLS Certificates](/tls) — How HTTPS works for ephemeral subdomains
---
## Protecting Routes
:::tip Looking for CI/CD OIDC?
If you want to **deploy from GitHub Actions or other CI systems** using OIDC tokens (no stored secrets), see [CI/CD Deployment with OIDC](/ci-deploy). This page covers a different feature — protecting your app's HTTP routes with single sign-on.
:::
Route protection lets you put single sign-on in front of an application at the routing layer. Unauthenticated requests are redirected to an OIDC identity provider for login, and after authentication, JWT claims are injected as HTTP headers before the request reaches your app.
Your app receives identity information as plain HTTP headers (e.g., `X-User-Email`) — no in-app auth code required. OIDC is the underlying mechanism, so any standards-compliant identity provider works.
## Quick Start
**Step 1: Add an identity provider**
```miren
miren auth provider add oidc my-google \
--provider-url https://accounts.google.com \
--client-id $GOOGLE_CLIENT_ID \
--client-secret $GOOGLE_CLIENT_SECRET \
--scope email --scope profile
```
**Step 2: Protect a route**
```miren
miren route protect myapp.example.com \
--provider my-google \
--claim-header email:X-User-Email \
--claim-header name:X-User-Name
```
That's it. Unauthenticated requests to `myapp.example.com` are now redirected to Google for login. After authentication, your app receives `X-User-Email` and `X-User-Name` headers.
## Authentication vs Authorization
This feature handles **authentication** — verifying _who_ a user is — not **authorization** — deciding _what_ they can access.
After authentication, your app receives claims as headers and decides what to do with them. For example, if you configure Google as your identity provider, your app receives the user's email address and can check the domain for basic access control.
## Trust Model
Your app trusts the claim headers (e.g., `X-User-Email`) because Miren is the only network path into the sandbox — external clients cannot reach your app directly. Miren validates the JWT from the identity provider and sets the headers before proxying the request.
This "trust the proxy" model is the standard approach used by tools like OAuth2 Proxy, Traefik ForwardAuth, and nginx `auth_request`. It's simple and works well when the platform controls the network topology, which Miren does via sandbox isolation.
Your app does not need to verify signatures or validate tokens — it can treat the claim headers as trusted input from the platform.
## How Claim Mappings Work
The `--claim-header` option maps JWT claims from the identity provider to HTTP headers that your app receives:
```miren
miren route protect myapp.example.com \
--provider my-google \
--claim-header email:X-User-Email \
--claim-header sub:X-User-ID \
--claim-header name:X-User-Name
```
- Each `--claim-header` takes the form `CLAIM:HEADER`
- Multiple mappings can be specified
- Claims not present in the JWT are silently skipped
- Your app receives these as regular request headers
## Example Provider Setups
### Google
Google's identity provider works well when you want to restrict access by email domain.
```miren
miren auth provider add oidc my-google \
--provider-url https://accounts.google.com \
--client-id $GOOGLE_CLIENT_ID \
--client-secret $GOOGLE_CLIENT_SECRET \
--scope email --scope profile
```
Your app can then check the `X-User-Email` header's domain for basic authorization (e.g., only allow `@yourcompany.com`).
### GitHub
GitHub doesn't expose a native OIDC endpoint, so Miren talks to it through a connector instead of the OIDC discovery path. You still get the same end result: identity arrives at your app as plain HTTP headers.
Register an OAuth App in GitHub (Settings → Developer settings → OAuth Apps) with the callback URL `https://your-route.example.com/.well-known/miren/oidc/callback`, then add the provider with `add github` and the orgs you want to allow:
```miren
miren auth provider add github my-github \
--client-id $GITHUB_CLIENT_ID \
--client-secret $GITHUB_CLIENT_SECRET \
--org mirendev
```
To restrict by team membership, suffix the org with one or more team slugs:
```miren
miren auth provider add github my-github \
--client-id $GITHUB_CLIENT_ID \
--client-secret $GITHUB_CLIENT_SECRET \
--org mirendev:engineering,platform
```
Repeat `--org` for multiple organizations. Attach the provider to a route as usual:
```miren
miren route protect myapp.example.com \
--provider my-github \
--claim-header email:X-User-Email \
--claim-header preferred_username:X-User-Login \
--claim-header groups:X-User-Groups
```
The connector exposes the user's GitHub login as `preferred_username`, and any org/team memberships as `groups` (in the form `org:team`). The full set of claims available is `sub`, `email`, `email_verified`, `name`, `preferred_username`, and `groups`. Mix and match `--claim-header` mappings to surface the bits your app cares about.
A wrinkle worth knowing: the `groups` claim only carries entries for *teams* the user belongs to within configured orgs. If you set `--org NAME` with no team suffix, the user is authorized (they must be in NAME to get in) but `groups` is empty because there are no team memberships to surface. For org membership signal, rely on the fact that the request reached your app at all, or configure team filters with `--org NAME:team1,team2` to populate `groups`.
### GitLab
GitLab has a built-in OIDC provider — no federation layer needed. Register an application in your GitLab instance and point directly at it.
```miren
miren auth provider add oidc my-gitlab \
--provider-url https://gitlab.com \
--client-id $GITLAB_CLIENT_ID \
--client-secret $GITLAB_CLIENT_SECRET \
--scope email --scope profile
```
### Keycloak (self-hosted)
[Keycloak](https://www.keycloak.org/) is an open-source identity provider you can run yourself. It supports user federation, identity brokering, and fine-grained claim configuration.
```miren
miren auth provider add oidc my-keycloak \
--provider-url https://keycloak.example.com/realms/myrealm \
--client-id $KEYCLOAK_CLIENT_ID \
--client-secret $KEYCLOAK_CLIENT_SECRET \
--scope email --scope profile
```
## Reusing Providers Across Routes
Once you've added a provider, you can use it to protect any number of routes:
```miren
miren route protect app1.example.com --provider my-google --claim-header email:X-User-Email
miren route protect app2.example.com --provider my-google --claim-header email:X-User-Email
```
## Default Route Support
`route protect` and `route unprotect` support the `--default` flag for the default route (which has no static hostname). When used with the default route, the OAuth2 redirect URL is derived from the request's `Host` header at runtime.
```miren
miren route protect --default \
--provider my-google \
--claim-header email:X-User-Email
```
## Managing Identity Providers
```miren
# List all providers
miren auth provider list
# Show details of a provider
miren auth provider show my-google
# Remove a provider
miren auth provider remove my-google
```
## Removing Route Protection
```miren
# Remove protection from a route (provider is preserved for reuse)
miren route unprotect myapp.example.com
# Check route status including protection info
miren route show myapp.example.com
```
See the [CLI reference](/command/route-protect) for the full list of options.
---
## Application Scaling
Miren automatically scales your application instances based on traffic. This page explains how scaling works and how to configure it for your needs.
## How Autoscaling Works
By default, Miren uses **autoscaling** for web services. As traffic to your application increases, Miren automatically launches additional instances to handle the load. When traffic decreases, instances are scaled back down.
This approach is similar to Google Cloud Run's scaling model: instead of guessing how many replicas you need, Miren observes actual demand and adjusts automatically.
### Why Autoscaling by Default?
Guessing instance counts is error-prone:
- Too few instances and your app can't handle traffic spikes
- Too many instances waste resources when traffic is low
- Manual scaling requires constant monitoring and adjustment
With autoscaling, Miren handles this automatically so you can focus on your application.
### Scale to Zero
Miren can scale your application all the way down to zero instances when there's no traffic. This is particularly valuable for self-hosted deployments where resource efficiency matters:
- **Better utilization**: Run dozens of apps on a single server—only active apps consume resources
- **Lower costs**: Development, staging, and low-traffic production apps don't waste memory or CPU
- **No idle tax**: Internal tools, webhooks, and scheduled tasks don't need dedicated instances waiting around
When a request arrives for a scaled-to-zero app, Miren quickly spins up an instance to handle it. The first request may have slightly higher latency, but subsequent requests are served immediately.
## Scaling Modes
Miren supports two scaling modes:
| Mode | Description | Use Case |
|------|-------------|----------|
| `auto` | Scales instances based on traffic | Stateless web services, APIs |
| `fixed` | Runs a set number of instances | Databases, workers, stateful services |
## Default Behavior
When you deploy without explicit configuration:
- **`web` service**: Auto mode with 10 concurrent requests per instance, 15-minute scale-down delay
- **All other services**: Fixed mode with 1 instance
## Configuring Scaling
Configure scaling in your `.miren/app.toml` file under `[services..concurrency]`.
### Auto Mode (Default for Web)
Auto mode scales instances based on concurrent requests:
```toml
[services.web.concurrency]
mode = "auto"
requests_per_instance = 10
scale_down_delay = "15m"
```
#### Auto Mode Options
| Option | Description | Default |
|--------|-------------|---------|
| `mode` | Must be `"auto"` | `"auto"` for web |
| `requests_per_instance` | Target concurrent requests per instance | 10 |
| `scale_down_delay` | How long to wait before scaling down idle instances | 15m |
#### How Auto Mode Calculates Instances
Miren targets `requests_per_instance` **concurrent** requests per instance, the number of in-flight requests being handled simultaneously, not requests over a period of time. For example, with `requests_per_instance = 10`:
- 5 concurrent requests → 1 instance
- 15 concurrent requests → 2 instances
- 100 concurrent requests → 10 instances
This means a single instance handling fast requests (e.g., 10ms each) can serve thousands of requests per second while staying under the concurrency limit.
The `scale_down_delay` prevents thrashing when traffic fluctuates. An instance won't be terminated until it has been idle for this duration.
### Fixed Mode
Fixed mode runs a specific number of instances regardless of traffic:
```toml
[services.worker.concurrency]
mode = "fixed"
num_instances = 3
```
#### Fixed Mode Options
| Option | Description | Default |
|--------|-------------|---------|
| `mode` | Must be `"fixed"` | `"fixed"` for non-web |
| `num_instances` | Exact number of instances to run | 1 |
## Examples
### High-Traffic API
For an API that handles many concurrent requests:
```toml
[services.web.concurrency]
mode = "auto"
requests_per_instance = 50
scale_down_delay = "5m"
```
This configuration:
- Allows 50 concurrent requests per instance (higher density)
- Scales down after 5 minutes of reduced traffic (faster scale-down)
### Background Worker
For a background job processor:
```toml
[services.worker.concurrency]
mode = "fixed"
num_instances = 2
```
This runs exactly 2 worker instances at all times.
### Database Service
For a database that should always be running:
```toml
[services.db]
image = "postgres:16"
[services.db.concurrency]
mode = "fixed"
num_instances = 1
```
### Complete Multi-Service App
```toml
name = "myapp"
# Web service: autoscales based on traffic
[services.web.concurrency]
mode = "auto"
requests_per_instance = 20
scale_down_delay = "10m"
# Worker service: fixed at 3 instances
[services.worker.concurrency]
mode = "fixed"
num_instances = 3
# Database: single instance with persistent storage
[services.db]
image = "postgres:16"
[services.db.concurrency]
mode = "fixed"
num_instances = 1
[[services.db.disks]]
name = "postgres-data"
mount_path = "/var/lib/postgresql/data"
size_gb = 20
```
## Scaling and Disks
Services with persistent disks must use fixed mode with exactly 1 instance:
```toml
[services.db.concurrency]
mode = "fixed"
num_instances = 1
[[services.db.disks]]
name = "db-data"
mount_path = "/data"
size_gb = 10
```
This restriction exists because disks use exclusive leasing where only one instance can mount a disk at a time.
## Tuning Tips
### Choosing `requests_per_instance`
- **Lower values** (5-10): More responsive scaling, higher resource usage
- **Higher values** (50-100): More efficient resource usage, may have slower response to traffic spikes
Start with the default (10) and adjust based on your application's characteristics.
### Choosing `scale_down_delay`
- **Shorter delays** (2m-5m): Faster resource reclamation, but may cause more scaling churn
- **Longer delays** (15m-30m): More stable instance counts, but holds resources longer
Consider your traffic patterns:
- Bursty traffic: Use longer delays to avoid constant scaling
- Predictable traffic: Shorter delays work well
## Monitoring Scaling
View your current instance counts:
```miren
# See app status including instance counts
miren app
# Watch instance counts in real-time
miren app --watch
# List all running sandboxes
miren sandbox list
```
## Next Steps
- [app.toml Reference — Concurrency](/app-toml#concurrency) — Complete field reference for concurrency settings
- [Services](/services) — Define multiple processes in your app
- [Getting Started](/getting-started) — Deploy your first app
---
## Server Configuration Reference
Complete reference for Miren server configuration. Settings can be specified via config file, environment variables, or CLI flags.
## Configuration Precedence
Settings are resolved in this order (highest priority first):
1. **CLI flags** — e.g. `--address :9443`
2. **Environment variables** — e.g. `MIREN_SERVER_ADDRESS=:9443`
3. **Config file** — `server.toml`
4. **Defaults**
## Config File
The server reads its config from the first file found:
1. Path specified via `--config`
2. `/etc/miren/server.toml`
3. `{data_path}/config/server.toml` (default: `/var/lib/miren/config/server.toml`)
### Example
```toml
mode = "standalone"
[server]
address = ":8443"
data_path = "/var/lib/miren"
network_backend = "vxlan"
http_request_timeout = 60
[ingress]
mode = "tls-autoprovision"
[tls]
acme_email = "admin@example.com"
[etcd]
start_embedded = true
[buildkit]
gc_keep_storage = "20GB"
gc_keep_duration = "14d"
```
## Server Modes
Miren has two operating modes:
| Mode | Description |
|------|-------------|
| `standalone` | All components (etcd, containerd, buildkit, logs, metrics) run embedded within a single process. **This is the default.** |
| `distributed` | Components run as separate services. Experimental. |
In standalone mode, embedded services start automatically unless explicitly disabled.
## Top-Level Fields
| Field | Type | Default | Env Var | CLI Flag |
|-------|------|---------|---------|----------|
| `mode` | string | `standalone` | `MIREN_MODE` | `--mode`, `-m` |
| `labs` | string[] | `[]` | `MIREN_LABS` | `--labs` |
## `[server]` — Core Settings {#server}
| Field | Type | Default | Description | Env Var | CLI Flag |
|-------|------|---------|-------------|---------|----------|
| `address` | string | `:8443` | Address to listen on (`host:port`) | `MIREN_SERVER_ADDRESS` | `--address`, `-a` |
| `runner_address` | string | `localhost:8444` | Runner address (`host:port`) | `MIREN_SERVER_RUNNER_ADDRESS` | `--runner-address` |
| `data_path` | string | `/var/lib/miren` | Root data directory | `MIREN_SERVER_DATA_PATH` | `--data-path`, `-d` |
| `runner_id` | string | `miren` | Runner identifier | `MIREN_SERVER_RUNNER_ID` | `--runner-id`, `-r` |
| `release_path` | string | — | Path to release directory containing binaries | `MIREN_SERVER_RELEASE_PATH` | `--release-path` |
| `config_cluster_name` | string | `local` | Cluster name in client config | `MIREN_SERVER_CONFIG_CLUSTER_NAME` | `--config-cluster-name`, `-C` |
| `skip_client_config` | bool | `false` | Skip writing client config to `clientconfig.d` | `MIREN_SERVER_SKIP_CLIENT_CONFIG` | `--skip-client-config` |
| `http_request_timeout` | int | `60` | HTTP request timeout in seconds (minimum: 1) | `MIREN_SERVER_HTTP_REQUEST_TIMEOUT` | `--http-request-timeout` |
| `stop_sandboxes_on_shutdown` | bool | `false` | Stop all sandboxes when server shuts down (useful in development) | `MIREN_SERVER_STOP_SANDBOXES_ON_SHUTDOWN` | `--stop-sandboxes-on-shutdown` |
| `network_backend` | string | `vxlan` | Network backend: `vxlan` or `wireguard` | `MIREN_SERVER_NETWORK_BACKEND` | `--network-backend` |
## `[ingress]` — Ingress Settings {#ingress}
Selects the deployment shape for Miren's HTTP/HTTPS ingress. The mode determines where Miren listens and whether it terminates TLS. See [TLS](/tls) for cert sourcing under each mode.
| Field | Type | Default | Description | Env Var | CLI Flag |
|-------|------|---------|-------------|---------|----------|
| `mode` | string | `tls-autoprovision` | Ingress mode: `tls-autoprovision`, `behind-proxy-http`, or `behind-proxy-https` | `MIREN_INGRESS_MODE` | `--ingress-mode` |
| `address` | string | — | Optional bind override (full `host:port`). Replaces the mode's default bind entirely. Ignored under `tls-autoprovision`. | `MIREN_INGRESS_ADDRESS` | `--ingress-address` |
### Modes
| Mode | Default bind | TLS terminated | Cert source |
|------|--------------|----------------|-------------|
| `tls-autoprovision` (default) | `0.0.0.0:443` plus `:80` for redirect / HTTP-01 ACME | yes | `[tls]` (ACME or self-signed) |
| `behind-proxy-http` | `127.0.0.1:80` | no | n/a |
| `behind-proxy-https` | `127.0.0.1:443` | yes | `[tls]` (self-signed or DNS-01 ACME) |
The `behind-proxy-*` modes default to localhost to keep accidental misconfigurations from quietly exposing an internal endpoint to the network. Set `ingress.address = "0.0.0.0:80"` (or similar) explicitly when the proxy is on a different host.
`unix:/path` is reserved for a future release and rejected today with a clear error.
## `[tls]` — TLS Settings {#tls}
Settings under `[tls]` cover two kinds of certs. `acme_email`, `acme_dns_provider`, and `self_signed` configure the ingress cert and only apply when Miren terminates TLS (`tls-autoprovision` or `behind-proxy-https`); they're rejected at startup under `behind-proxy-http`. `additional_names` and `additional_ips` are different: they extend the SANs on the API server and etcd certs, which exist regardless of ingress mode, so they're valid under any mode. See [TLS](/tls) for setup guides.
| Field | Type | Default | Description | Env Var | CLI Flag |
|-------|------|---------|-------------|---------|----------|
| `additional_names` | string[] | `[]` | Extra DNS names for the server certificate | `MIREN_TLS_ADDITIONAL_NAMES` | `--dns-names` |
| `additional_ips` | string[] | `[]` | Extra IPs for the server certificate | `MIREN_TLS_ADDITIONAL_IPS` | `--ips` |
| `acme_dns_provider` | string | — | DNS provider for ACME DNS-01 challenges (e.g. `cloudflare`, `route53`). Required under `behind-proxy-https` if not using `self_signed`. | `MIREN_TLS_ACME_DNS_PROVIDER` | `--acme-dns-provider` |
| `acme_email` | string | — | Email for ACME account registration | `MIREN_TLS_ACME_EMAIL` | `--acme-email` |
| `self_signed` | bool | `false` | Use self-signed certificates (development only, or behind a TLS-terminating proxy that doesn't verify) | `MIREN_TLS_SELF_SIGNED` | `--self-signed-tls` |
## `[etcd]` — Etcd Settings {#etcd}
Miren uses etcd as its entity store. In standalone mode, an embedded etcd server starts automatically.
| Field | Type | Default | Description | Env Var | CLI Flag |
|-------|------|---------|-------------|---------|----------|
| `endpoints` | string[] | `[]` | Etcd endpoints (auto-configured when embedded) | `MIREN_ETCD_ENDPOINTS` | `--etcd`, `-e` |
| `prefix` | string | `/miren` | Key prefix in etcd | `MIREN_ETCD_PREFIX` | `--etcd-prefix`, `-p` |
| `start_embedded` | bool | `true`\* | Start embedded etcd server | `MIREN_ETCD_START_EMBEDDED` | `--start-etcd` |
| `client_port` | int | `12379` | Embedded etcd client port | `MIREN_ETCD_CLIENT_PORT` | `--etcd-client-port` |
| `peer_port` | int | `12380` | Embedded etcd peer port | `MIREN_ETCD_PEER_PORT` | `--etcd-peer-port` |
| `http_client_port` | int | `12381` | Embedded etcd HTTP client port | `MIREN_ETCD_HTTP_CLIENT_PORT` | `--etcd-http-client-port` |
\* Defaults to `true` in standalone mode only.
## `[containerd]` — Containerd Settings {#containerd}
| Field | Type | Default | Description | Env Var | CLI Flag |
|-------|------|---------|-------------|---------|----------|
| `start_embedded` | bool | `true`\* | Start embedded containerd daemon | `MIREN_CONTAINERD_START_EMBEDDED` | `--start-containerd` |
| `binary_path` | string | `containerd` | Path to containerd binary | `MIREN_CONTAINERD_BINARY_PATH` | `--containerd-binary` |
| `socket_path` | string | — | Path to containerd socket | `MIREN_CONTAINERD_SOCKET_PATH` | `--containerd-socket` |
\* Defaults to `true` in standalone mode only.
## `[buildkit]` — BuildKit Settings {#buildkit}
Controls the BuildKit daemon used for building container images.
| Field | Type | Default | Description | Env Var | CLI Flag |
|-------|------|---------|-------------|---------|----------|
| `start_embedded` | bool | `true`\* | Start embedded BuildKit daemon | `MIREN_BUILDKIT_START_EMBEDDED` | `--start-buildkit` |
| `socket_path` | string | — | Path to external BuildKit socket (distributed mode) | `MIREN_BUILDKIT_SOCKET_PATH` | `--buildkit-socket` |
| `socket_dir` | string | — | Directory for embedded BuildKit socket | `MIREN_BUILDKIT_SOCKET_DIR` | `--buildkit-socket-dir` |
| `gc_keep_storage` | string | `10GB` | Maximum BuildKit layer cache size | `MIREN_BUILDKIT_GC_KEEP_STORAGE` | `--buildkit-gc-storage` |
| `gc_keep_duration` | string | `7d` | How long to keep cache entries | `MIREN_BUILDKIT_GC_KEEP_DURATION` | `--buildkit-gc-duration` |
\* Defaults to `true` in standalone mode only.
## `[victorialogs]` — Log Storage Settings {#victorialogs}
Controls the embedded VictoriaLogs instance used for application log storage.
| Field | Type | Default | Description | Env Var | CLI Flag |
|-------|------|---------|-------------|---------|----------|
| `start_embedded` | bool | `true`\* | Start embedded VictoriaLogs server | `MIREN_VICTORIALOGS_START_EMBEDDED` | `--start-victorialogs` |
| `http_port` | int | `9428` | HTTP port in embedded mode | `MIREN_VICTORIALOGS_HTTP_PORT` | `--victorialogs-http-port` |
| `retention_period` | string | `30d` | Retention period (e.g. `30d`, `2w`, `1y`) | `MIREN_VICTORIALOGS_RETENTION_PERIOD` | `--victorialogs-retention` |
| `address` | string | `victorialogs:9428` | Address when not using embedded | `MIREN_VICTORIALOGS_ADDRESS` | `--victorialogs-addr` |
\* Defaults to `true` in standalone mode only.
## `[victoriametrics]` — Metrics Storage Settings {#victoriametrics}
Controls the embedded VictoriaMetrics instance used for application metrics.
| Field | Type | Default | Description | Env Var | CLI Flag |
|-------|------|---------|-------------|---------|----------|
| `start_embedded` | bool | `true`\* | Start embedded VictoriaMetrics server | `MIREN_VICTORIAMETRICS_START_EMBEDDED` | `--start-victoriametrics` |
| `http_port` | int | `8428` | HTTP port in embedded mode | `MIREN_VICTORIAMETRICS_HTTP_PORT` | `--victoriametrics-http-port` |
| `retention_period` | string | `1` | Retention period in months | `MIREN_VICTORIAMETRICS_RETENTION_PERIOD` | `--victoriametrics-retention` |
| `address` | string | `victoriametrics:8428` | Address when not using embedded | `MIREN_VICTORIAMETRICS_ADDRESS` | `--victoriametrics-addr` |
\* Defaults to `true` in standalone mode only.
---
## Services
A Miren app can run multiple **services**—separate processes that work together as part of the same application. Each service can have its own command, image, environment variables, and scaling configuration.
## What is a Service?
A service is a named process within your app. Common patterns include:
- **web**: Your main HTTP server (receives external traffic)
- **worker**: Background job processor
- **postgres**: A database running alongside your app
Services share the same deployment lifecycle—when you deploy your app, all services are updated together. But each service scales independently and can run different code.
## Defining Services
Services can be defined in two ways:
1. **Procfile** — Simple format for defining commands per service
2. **`.miren/app.toml`** — Full configuration with scaling, env vars, and images
### Service Detection
Miren detects services in this order:
1. **`.miren/app.toml`** — Services defined in the `[services.*]` sections
2. **`Procfile`** — Services inferred from Procfile entries
3. **Dockerfile `CMD`/`ENTRYPOINT`** — If no services are defined above, but your image has a default command, Miren creates a `web` service using that command
If none of these provide a service definition, the deploy will fail with an error.
### Using a Procfile
If your app has a `Procfile`, Miren automatically infers services from it:
```text
web: npm start
worker: npm run worker
```
Each line defines a service: the name before the colon, and the command after. This is compatible with Heroku's Procfile format.
For more control (scaling, environment variables, different images), use `.miren/app.toml`:
```toml
[services.web]
command = "npm start"
[services.worker]
command = "npm run worker"
[services.worker.concurrency]
mode = "fixed"
num_instances = 2
```
### Same Image, Different Commands
The most common pattern is running multiple processes from the same codebase. Define a command for each service:
```toml
name = "myapp"
[services.web]
command = "npm start"
[services.worker]
command = "npm run worker"
```
Both services use your app's built image. The `web` service runs your HTTP server, while `worker` runs a background processor.
#### Example: Rails with Sidekiq
```toml
name = "railsapp"
[services.web]
command = "bundle exec puma -C config/puma.rb"
[services.worker]
command = "bundle exec sidekiq"
[services.worker.concurrency]
mode = "fixed"
num_instances = 2
```
#### Example: Python with Celery
```toml
name = "djangoapp"
[services.web]
command = "gunicorn myapp.wsgi:application --bind 0.0.0.0:8000"
port = 8000
[services.worker]
command = "celery -A myapp worker --loglevel=info"
[services.beat]
command = "celery -A myapp beat --loglevel=info"
```
### Different Images
:::tip Use addons for databases
If you just need a PostgreSQL database, consider using an [addon](/addons) instead of running it as a service. Addons are fully managed — Miren provisions the database, injects credentials, and handles cleanup. Use a service when you need full control over the database configuration.
:::
For services that need entirely different software—like a database—specify an `image`:
```toml
name = "myapp"
[services.web]
command = "npm start"
[services.postgres]
image = "postgres:16"
[[services.postgres.env]]
key = "PGDATA"
value = "/miren/data/local/pgdata"
[services.postgres.concurrency]
mode = "fixed"
num_instances = 1
```
When you specify an `image`, Miren pulls that container image instead of using your app's built image. This lets you run standard database images alongside your application code.
#### Example: Full Stack with PostgreSQL and Redis
```toml
name = "fullstack"
# Your application code
[services.web]
command = "node server.js"
[services.worker]
command = "node worker.js"
[services.worker.concurrency]
mode = "fixed"
num_instances = 2
# PostgreSQL database
[services.postgres]
image = "postgres:16"
[[services.postgres.env]]
key = "POSTGRES_PASSWORD"
value = "secret"
[[services.postgres.env]]
key = "PGDATA"
value = "/miren/data/local/pgdata"
[services.postgres.concurrency]
mode = "fixed"
num_instances = 1
# Redis cache (data stored in memory, no persistence needed for caching)
[services.redis]
image = "redis:7-alpine"
[services.redis.concurrency]
mode = "fixed"
num_instances = 1
```
## Service Configuration Reference
Each service can configure:
| Option | Description | Default |
|--------|-------------|---------|
| `command` | Command to run | Image's default entrypoint |
| `image` | Container image to use | App's built image |
| `port` | Port the service listens on (single-port shorthand) | 3000 (web only) |
| `ports` | Port configuration array (multi-port, see [Traffic Routing](/traffic-routing)) | (none) |
| `port_timeout` | Time to wait for the service to bind its port at startup (e.g. `"60s"`, `"2m"`) | `15s` |
| `env` | Service-specific environment variables | (none) |
| `concurrency` | Scaling configuration | See [Scaling](/scaling) |
| `concurrency.shutdown_timeout` | Time to wait for graceful shutdown during redeploy | `10s` |
| `disks` | Persistent disk attachments (experimental, see [Disks](/disks)) | (none) |
### Environment Variables
Services inherit global environment variables from your app, and can add their own:
```toml
name = "myapp"
# Global env vars - available to all services
[[env]]
key = "LOG_LEVEL"
value = "info"
# Service-specific env vars
[services.web]
command = "npm start"
[[services.web.env]]
key = "NODE_ENV"
value = "production"
[services.worker]
command = "npm run worker"
[[services.worker.env]]
key = "WORKER_CONCURRENCY"
value = "5"
```
## Service Communication
Services within the same app can communicate using internal DNS. Each service is discoverable at `.app.miren`:
```toml
name = "myapp"
[[env]]
key = "DATABASE_URL"
value = "postgres://user:pass@postgres.app.miren:5432/mydb"
[[env]]
key = "REDIS_URL"
value = "redis://redis.app.miren:6379"
[services.web]
command = "npm start"
[services.postgres]
image = "postgres:16"
[[services.postgres.env]]
key = "POSTGRES_PASSWORD"
value = "pass"
[[services.postgres.env]]
key = "PGDATA"
value = "/miren/data/local/pgdata"
[services.postgres.concurrency]
mode = "fixed"
num_instances = 1
[services.redis]
image = "redis:7-alpine"
[services.redis.concurrency]
mode = "fixed"
num_instances = 1
```
Connect to other services using their DNS name and standard port—`postgres.app.miren:5432` for PostgreSQL, `redis.app.miren:6379` for Redis. The container images listen on their standard ports by default; Miren doesn't manage these ports.
## Traffic Routing
The `web` service receives external HTTP traffic through Miren's HTTP ingress. Create a route to make your app reachable:
```miren
miren route add myapp.example.com --app myapp
```
The web service defaults to port 3000. Override it if your app listens elsewhere:
```toml
[services.web]
command = "gunicorn app:app --bind 0.0.0.0:8000"
port = 8000
```
For non-HTTP services (TCP/UDP), you can expose ports directly using the `ports` array and `node_port`. See [Traffic Routing](/traffic-routing) for the full picture — HTTP ingress, L4 routing, multi-port services, and the `PORT` environment variable.
## Service Scaling
Each service scales independently. By default:
- **`web` service**: Autoscales based on traffic (scale-to-zero enabled)
- **All other services**: Fixed at 1 instance
Configure scaling per-service:
```toml
[services.web.concurrency]
mode = "auto"
requests_per_instance = 20
scale_down_delay = "10m"
[services.worker.concurrency]
mode = "fixed"
num_instances = 3
```
For detailed scaling configuration, see [Application Scaling](/scaling).
## Persistent Storage
For stateful services like databases, use [Local Storage](/disks#local-storage)—persistent storage automatically available at `/miren/data/local`. Configure your database to store data there:
```toml
[services.postgres]
image = "postgres:16"
[[services.postgres.env]]
key = "PGDATA"
value = "/miren/data/local/pgdata"
[services.postgres.concurrency]
mode = "fixed"
num_instances = 1
```
The `PGDATA` environment variable tells PostgreSQL where to store its data. Using a subdirectory (`pgdata`) under `/miren/data/local` is required because PostgreSQL expects to own its data directory.
For cloud-synced storage that travels with your app, see [Miren Disks](/disks#miren-disks) (experimental).
## Sandbox Pools
When you deploy an app, Miren creates a **sandbox pool** for each service. The pool manages the desired number of instances (sandboxes) for that service.
The hierarchy is:
- **App** → has one active deployment (version)
- **Sandbox Pool** → one per service, manages instance count
- **Sandbox** → individual running container
### Inspecting What's Running
Use these commands to drill down from apps to running instances:
```miren
# List all apps and their current versions
miren app list
```
```text
NAME VERSION DEPLOYED COMMIT
demo demo-vCVkjR6u7744AsMebwMjGU 1d ago 5f4dd55
conference conference-vCVkjJSe4fydvxEHfhsKfA 1d ago 5f4dd55
```
```miren
# List sandbox pools (one per service per version)
miren sandbox-pool list
```
```text
ID VERSION SERVICE DESIRED CURRENT READY
pool-CVkjTGJhRddyZDVq9CmnN demo-vCVkjR6u7744AsMebwMjGU web 1 1 1
pool-CVkjMv2R2VwcLdHJUoGKD conference-vCVkjJSe4fydvxEHfhsKfA web 3 3 3
pool-CVmuoeQCzjoNN9hGsu14c conference-vCVkjJSe4fydvxEHfhsKfA worker 2 2 2
```
```miren
# List individual sandboxes (instances)
miren sandbox list
```
```text
ID SERVICE POOL ADDRESS STATUS
demo-web-CVok1wptmHEsJ6DmTRy7g web pool-CVkjTGJhRddyZDVq9CmnN 10.8.32.9/24 running
conference-web-CVnbNhSjUbGEAC5L web pool-CVkjMv2R2VwcLdHJUoGKD 10.8.32.12/24 running
conference-web-CVnbNhVDNcqapDcX web pool-CVkjMv2R2VwcLdHJUoGKD 10.8.32.19/24 running
```
```miren
# View logs for a specific sandbox
miren logs -s demo-web-CVok1wptmHEsJ6DmTRy7g
```
## Complete Examples
### Node.js API with Worker
```toml
name = "api"
[[env]]
key = "DATABASE_URL"
value = "postgres://user:pass@postgres.app.miren:5432/api"
[services.web]
command = "node dist/server.js"
[services.web.concurrency]
mode = "auto"
requests_per_instance = 50
[services.worker]
command = "node dist/worker.js"
[services.worker.concurrency]
mode = "fixed"
num_instances = 2
[services.postgres]
image = "postgres:16"
[[services.postgres.env]]
key = "POSTGRES_PASSWORD"
value = "pass"
[[services.postgres.env]]
key = "PGDATA"
value = "/miren/data/local/pgdata"
[services.postgres.concurrency]
mode = "fixed"
num_instances = 1
```
### Go Service with PostgreSQL
```toml
name = "goapp"
[[env]]
key = "DATABASE_URL"
value = "postgres://goapp:changeme@postgres.app.miren:5432/goapp"
[services.web]
command = "./server"
[services.web.concurrency]
mode = "auto"
requests_per_instance = 100
scale_down_delay = "5m"
[services.postgres]
image = "postgres:16-alpine"
[[services.postgres.env]]
key = "POSTGRES_USER"
value = "goapp"
[[services.postgres.env]]
key = "POSTGRES_PASSWORD"
value = "changeme"
[[services.postgres.env]]
key = "POSTGRES_DB"
value = "goapp"
[[services.postgres.env]]
key = "PGDATA"
value = "/miren/data/local/pgdata"
[services.postgres.concurrency]
mode = "fixed"
num_instances = 1
```
### Python App with Redis Queue
```toml
name = "taskqueue"
[[env]]
key = "REDIS_URL"
value = "redis://redis.app.miren:6379"
[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 = "rq worker --url redis://redis.app.miren:6379"
[services.worker.concurrency]
mode = "fixed"
num_instances = 3
[services.redis]
image = "redis:7-alpine"
[services.redis.concurrency]
mode = "fixed"
num_instances = 1
```
## Next Steps
- [App Configuration](/app-configuration) — Overview of the configuration model
- [app.toml Reference](/app-toml) — Complete field reference for `.miren/app.toml`
- [Traffic Routing](/traffic-routing) — HTTP ingress, TCP/UDP routing, multi-port services
- [Persistent Storage](/disks) — Local storage and disk options for databases
- [Application Scaling](/scaling) — Configure how each service scales
- [Getting Started](/getting-started) — Deploy your first app
---
## System Requirements
Miren needs a Linux server with enough memory and disk space to run its components and build your applications.
| Resource | Minimum | Recommended |
|----------|---------|-------------|
| **Operating System** | Linux (kernel 5.10+) | |
| **Architecture** | x86_64 or arm64 | |
| **Memory** | 4 GB | 8 GB |
| **Storage** | 50 GB | 100 GB |
## Why these numbers?
### Memory
Miren runs several components — containerd, etcd, buildkit, metrics, and logging — that together use around 600 MB of memory at idle. During builds, memory usage spikes as buildkit compiles your application. A single Rails app with Postgres can push total usage past 1.3 GB during deployment, which is why we set the minimum at 4 GB.
With 8 GB, you'll have comfortable headroom for running multiple apps and handling concurrent builds without things getting tight.
### Storage
Container images and build caches add up quickly. Base images for languages like Ruby or Python are 50-80 MB compressed but expand on disk, and BuildKit caches intermediate build layers aggressively — keeping up to 10 GB by default. A single Rails deployment can use 15-20 GB between base images, build cache, and the container registry. With multiple apps and their version history, usage grows from there.
Starting with 50 GB gives you enough room to get going. With 100 GB you'll have space to grow without worrying about "no space left on device" errors during builds.
## What happens if my system is too small?
The `miren server install` command checks your system against these requirements before installing. If your machine doesn't meet the minimums, the installer will let you know what's short and point you here.
If you're below the recommended thresholds but above the minimums, you'll see a heads-up but installation will proceed normally.
If you know what you're doing and want to install anyway (say, for testing), you can bypass the check:
```miren
sudo miren server install --skip-system-check
```
## We'd love to hear from you
We're still learning about system requirements as more people deploy Miren in different contexts. If you have an interesting deployment scenario or resource constraints you'd like to discuss, come chat with us on [Discord](https://miren.dev/discord)!
---
## Terminology
Common terms used in Miren.
## App
An application deployed to Miren. Each app has a name, configuration, and one or more deployments.
## Cluster
A Miren server instance that runs your applications. A cluster can be standalone or connected to Miren Cloud for team management and multi-environment workflows.
## Miren Cloud
A central control plane that connects and manages your Miren clusters. Provides team management, access control, automatic data backup, and multi-environment workflows. See [Miren Cloud](/miren-cloud/overview).
## Miren Runtime
The core container orchestration system that powers Miren. Built on containerd, it handles building, deploying, and running your applications in isolated sandboxes.
## Miren Runtime Client
The `miren` CLI tool used to interact with your cluster. Manages apps, deployments, routes, and cluster configuration.
## Miren Server
The background service that runs on your cluster and manages applications, sandboxes, and networking. Installed as a systemd service via `miren server install`.
## Deployment
A specific version of your app that has been built and deployed. Each deployment creates a new container image and can be rolled back if needed.
## Disk
Persistent storage attached to your application. Miren disks survive restarts and redeployments, making them suitable for databases and stateful workloads.
## Route
Maps a hostname to an application. Routes determine how HTTP traffic reaches your apps. Your first app gets a default route automatically. Routes can use wildcard patterns (e.g., `*.example.com`) to match all subdomains of a domain.
## Sandbox
An isolated execution environment where your app runs. Sandboxes use gvisor for security isolation and have their own network namespace.
## Service
A named process within an app. An app can have multiple services, each with its own command, image, port, and scaling configuration. Common services include `web` (HTTP server), `worker` (background jobs), and database services like `postgres`. See [Services](/services).
## Client Config
The configuration file (`~/.config/miren/clientconfig.yaml`) that stores your CLI settings, including cluster connections and the active cluster. Managed automatically by commands like `miren server install` and `miren cluster add`.
---
## TLS Certificates
Miren automatically provisions TLS certificates for your applications using [Let's Encrypt](https://letsencrypt.org/). By default, no configuration is needed — when you set a route for your app, Miren obtains a certificate automatically.
## How It Works
When a request arrives for a hostname with a configured route, Miren provisions a TLS certificate from Let's Encrypt using the ACME protocol. Certificates are cached on disk and renewed automatically before they expire.
Hostnames without a configured route are served with a self-signed fallback certificate (browsers will show a warning).
For wildcard routes (e.g., `*.myapp.example.com`), TLS certificates are provisioned for each matching subdomain as requests arrive. See [Wildcard Routes](/traffic-routing#wildcard-routes) for details.
## Challenge Types
Let's Encrypt needs to verify that you control the domain before issuing a certificate. Miren supports two verification methods.
### HTTP-01 Challenge (Default)
The default method. Let's Encrypt makes an HTTP request to your server on port 80 to verify domain ownership.
**Requirements:**
- Your server must be reachable from the public internet on ports 80 and 443
- DNS for your domain must point to the server's public IP
This is the zero-configuration option — it works out of the box with `miren server install`.
### DNS-01 Challenge
If your server isn't publicly reachable (e.g., it's behind a VPN like Tailscale, on a private network, or doesn't have ports 80/443 open to the internet), you can use DNS-01 challenges instead. This method proves domain ownership by creating a DNS TXT record, so it works regardless of whether your server is reachable from the internet.
Miren uses [lego](https://go-acme.github.io/lego/) for DNS challenges, which supports [90+ DNS providers](https://go-acme.github.io/lego/dns/).
#### Configuration
Add the DNS provider and ACME email to your server config file:
```toml title="/var/lib/miren/server/config.toml"
[tls]
acme_dns_provider = "dnsimple"
acme_email = "you@example.com"
```
Each DNS provider requires credentials passed via environment variables. Create an environment file for the Miren service:
```bash title="/var/lib/miren/server/env"
DNSIMPLE_OAUTH_TOKEN=your-token-here
```
Then update the systemd service to load the environment file and config:
```bash
sudo systemctl edit miren --force
```
Add the environment file:
```ini
[Service]
EnvironmentFile=/var/lib/miren/server/env
```
Restart to pick up the changes:
```bash
sudo systemctl restart miren
```
#### Provider Examples
Each provider needs different environment variables. Here are a few common ones:
**DNSimple:**
```toml
acme_dns_provider = "dnsimple"
```
```bash
DNSIMPLE_OAUTH_TOKEN=your-token
```
**Cloudflare:**
```toml
acme_dns_provider = "cloudflare"
```
```bash
CF_DNS_API_TOKEN=your-token
```
**Route 53 (AWS):**
```toml
acme_dns_provider = "route53"
```
```bash
AWS_ACCESS_KEY_ID=your-key
AWS_SECRET_ACCESS_KEY=your-secret
AWS_REGION=us-east-1
```
See the [lego DNS provider documentation](https://go-acme.github.io/lego/dns/) for the full list of supported providers and their required environment variables.
## Ingress Modes and TLS
Whether Miren terminates TLS at all (and on which ports) is set by `ingress.mode`. The default `tls-autoprovision` mode is what this page has been describing: TLS on `:443`, plus `:80` for the HTTPS redirect and HTTP-01 ACME challenges.
Two other modes are available for deployments where Miren sits behind a TLS-terminating proxy (nginx, Caddy, Cloudflare Tunnel, ALB):
| Mode | What Miren does | Cert source |
|------|-----------------|-------------|
| `tls-autoprovision` (default) | Binds `:443` for TLS and `:80` for redirect / HTTP-01 ACME | `[tls]` (ACME or self-signed) |
| `behind-proxy-http` | Plain HTTP at the configured address (default `127.0.0.1:80`); TLS lives at the proxy | n/a (proxy terminates TLS) |
| `behind-proxy-https` | TLS terminated at the configured address (default `127.0.0.1:443`); no `:80` listener, so no HTTP-01 ACME | `[tls]` self-signed or DNS-01 ACME only |
See [Server Configuration Reference → `[ingress]`](/server-config#ingress) for the full schema. The HTTP-01 ACME flow described above only applies under `tls-autoprovision`; under `behind-proxy-https`, certs must come from DNS-01 ACME or be self-signed because Miren doesn't bind `:80` in that mode (and the public DNS for the hostname points at the proxy anyway, not at Miren).
## TLS Settings Reference
All TLS settings live under the `[tls]` section of the server config file (typically `/var/lib/miren/server/config.toml`). The three ingress-cert settings below are consulted only under TLS-terminating ingress modes:
| Setting | CLI Flag | Description |
|---------|----------|-------------|
| `acme_email` | `--acme-email` | Email for Let's Encrypt account registration and expiry notifications |
| `acme_dns_provider` | `--acme-dns-provider` | DNS provider name for DNS-01 challenges (e.g., `cloudflare`, `route53`, `dnsimple`) |
| `self_signed` | `--self-signed-tls` | Use a self-signed cert instead of ACME (development, or behind a non-verifying TLS proxy) |
Two additional `[tls]` settings apply to the API server and etcd certs rather than ingress, so they're valid under any ingress mode:
| Setting | CLI Flag | Description |
|---------|----------|-------------|
| `additional_names` | `--dns-names` | Extra DNS names appended to the API server and etcd cert SANs |
| `additional_ips` | `--ips` | Extra IPs appended to the API server and etcd cert SANs |
## Troubleshooting
### Certificate Not Provisioning
Check the server logs for ACME errors:
```bash
sudo journalctl -u miren | grep -i acme
```
**HTTP-01 challenges:** Ensure ports 80 and 443 are reachable from the public internet. See [Firewall Configuration](/firewall) for cloud provider-specific guidance.
**DNS-01 challenges:** Verify your DNS provider credentials are correct and the environment file is loaded:
```bash
sudo systemctl show miren | grep EnvironmentFile
```
### Wrong Certificate / Self-Signed Warning
If you're seeing a self-signed certificate warning for a domain that should have a real certificate, check that a route is configured for that hostname:
```miren
miren route
```
Miren only provisions ACME certificates for hostnames with explicitly configured routes. All other hostnames get the self-signed fallback.
---
## Traffic Routing
Miren handles getting traffic to your app. For HTTP apps, this works automatically — deploy and add a route. For non-HTTP services like IRC servers, game servers, or custom TCP/UDP protocols, you configure ports explicitly.
## Web Traffic (HTTP)
When you deploy an app, the `web` service automatically receives HTTP traffic. No port configuration is needed — Miren handles TLS, routing, and load balancing.
### Adding a Route
Map a hostname to your app:
```miren
miren route set myapp.example.com myapp
```
Requests to that hostname are forwarded to your `web` service. TLS certificates are provisioned automatically (see [TLS Certificates](/tls)).
### Wildcard Routes
Route all subdomains of a domain to a single app using a wildcard:
```miren
miren route set '*.myapp.example.com' myapp
```
A wildcard route `*.myapp.example.com` matches any subdomain like `foo.myapp.example.com` or `bar.myapp.example.com`. It does **not** match the bare domain `myapp.example.com` — add a separate route for that if needed.
If an exact route also exists for a specific subdomain, the exact route takes priority. For example, if you have both `*.myapp.example.com` and `api.myapp.example.com`, requests to `api.myapp.example.com` use the exact route while all other subdomains use the wildcard.
The wildcard `*` must be the first label — patterns like `foo.*.example.com` are not supported. The domain must have at least two labels after the wildcard (e.g., `*.example.com` is valid, `*.com` is not).
For [Pull Request Environments](/pr-environments), you don't need a wildcard route — any subdomain of an existing route automatically resolves to its ephemeral label. You only need wildcard *DNS* pointing at your cluster.
### Custom Domains
To put your own domain in front of an app on Miren, point DNS at your cluster and set a route. TLS provisions automatically.
**Find your cluster's hostname.** Clusters registered with Miren Cloud get a hostname under `miren.systems` — something like `cluster-jwomf2l0tn8z.miren.systems`. You'll find yours on the cluster page in [miren.cloud](https://miren.cloud). If you're running standalone without cloud, use your server's public hostname or IP.
**Point DNS at your cluster.** For most setups, point both the apex and a wildcard so you can add or change routes later without touching DNS again:
```text
yourdomain.com. ALIAS cluster-jwomf2l0tn8z.miren.systems.
*.yourdomain.com. CNAME cluster-jwomf2l0tn8z.miren.systems.
```
Most DNS providers don't allow a CNAME at the apex (`yourdomain.com` with no subdomain). Use an ALIAS or ANAME record where your provider supports it, or an A record pointing at your cluster's IP. The wildcard record can always be a CNAME.
If you only need one hostname on Miren, a single CNAME for that name is fine on its own:
```text
app.yourdomain.com. CNAME cluster-jwomf2l0tn8z.miren.systems.
```
**Then set a route.** Map any host that resolves to your cluster to an app:
```miren
miren route set app.yourdomain.com myapp
```
If you set up wildcard DNS above, you can also send every subdomain to the same app with a [wildcard route](#wildcard-routes):
```miren
miren route set '*.yourdomain.com' myapp
```
You don't have to do anything else for HTTPS — Miren provisions Let's Encrypt certificates as requests arrive. See [TLS Certificates](/tls) for the details.
### Choosing a Port
Miren sets the `PORT` environment variable to tell your app which port to listen on. Your app should bind to `PORT`:
```toml
[services.web]
command = "node server.js"
# App reads process.env.PORT and listens there
```
To choose a specific port, set it in `.miren/app.toml`:
```toml
[services.web]
command = "gunicorn app:app --bind 0.0.0.0:8000"
port = 8000
```
Miren sets `PORT=8000` and routes traffic there.
## Non-HTTP Services (TCP/UDP)
To expose services that don't speak HTTP — an IRC server, a game server, a custom protocol — use the `ports` array in `.miren/app.toml` with a `node_port` to make the port reachable from outside the cluster.
```toml
[services.irc]
command = "./ircd"
[[services.irc.ports]]
port = 6667
name = "irc"
type = "tcp"
node_port = 6667
[[services.irc.ports]]
port = 6697
name = "irc-tls"
type = "tcp"
node_port = 6697
```
With this config:
- The IRC server listens on ports 6667 and 6697 inside its container
- Clients connect to those same ports on your Miren host
- Miren forwards the traffic and load-balances across instances
### Port Configuration
Each entry in `ports` accepts:
| Field | Required | Description | Default |
|-------|----------|-------------|---------|
| `port` | Yes | Port your process listens on (1–65535) | — |
| `name` | Yes | A unique name for this port | — |
| `type` | No | `"http"` for web traffic, `"tcp"` for raw TCP, `"udp"` for UDP | `"http"` |
| `node_port` | No | Port to expose on the host machine (1–65535) | (none) |
### Node Ports
The `node_port` is what makes a non-HTTP service reachable from outside. Without it, the port is only accessible to other services within the app.
Node port constraints:
- Must be unique across all apps on the cluster — Miren checks for conflicts at deploy time
- Cannot overlap with ports Miren uses (80, 443, 8443)
If your cloud provider uses security groups or network ACLs, you'll need to allow traffic on your node ports (see [Firewall Configuration](/firewall)).
## Multiple Ports per Service
A single service can expose a mix of HTTP and non-HTTP ports. This is useful when a service needs an HTTP endpoint for health checks or an API alongside a raw protocol port:
```toml
[services.app]
command = "./server"
# HTTP endpoint for health checks and API
[[services.app.ports]]
port = 3000
name = "http"
type = "http"
# TCP port for the data protocol
[[services.app.ports]]
port = 7000
name = "data"
type = "tcp"
node_port = 7000
```
The HTTP port is routed through Miren's web traffic pipeline (routes, TLS, autoscaling). The TCP port is exposed directly via the node port. Both reach the same running process.
### Same Port, Different Protocols
Some protocols need both TCP and UDP on the same port number. Declare them as separate entries:
```toml
[[services.dns.ports]]
port = 53
name = "dns-udp"
type = "udp"
node_port = 53
[[services.dns.ports]]
port = 53
name = "dns-tcp"
type = "tcp"
node_port = 5353
```
Port numbers can repeat as long as each port/type pair is unique within the service (since `"tcp"` and `"udp"` use different transport protocols).
### The `PORT` Environment Variable
When using the `ports` array, Miren sets `PORT` to:
1. The first port with `type = "http"`, if one exists
2. Otherwise, the first port in the array
`PORT` is managed by Miren and can't be overridden.
## Service-to-Service Communication
Services within the same app can talk to each other using internal DNS. Each service is reachable at `.app.miren`:
```toml
name = "myapp"
[[env]]
key = "DATABASE_URL"
value = "postgres://user:pass@postgres.app.miren:5432/mydb"
[services.web]
command = "node server.js"
[services.postgres]
image = "postgres:16"
[services.postgres.concurrency]
mode = "fixed"
num_instances = 1
```
Internal traffic goes directly between containers — it doesn't pass through Miren's routing layer. Services connect on their standard ports (5432 for PostgreSQL, 6379 for Redis) without any Miren port configuration needed.
:::note
Internal DNS only works between services in the same app. Cross-app communication is not currently supported.
:::
## Simple Port Configuration
If your service only has one port, you can use the simpler single-field syntax instead of the `ports` array:
```toml
[services.web]
port = 8000
port_name = "http"
port_type = "http"
```
| Field | Description | Default |
|-------|-------------|---------|
| `port` | Port your process listens on | Set by `PORT` env var |
| `port_name` | Name for the port | Service name |
| `port_type` | `"http"` or `"tcp"` | `"http"` |
You can't mix these fields with the `ports` array on the same service.
## How It Works Under the Hood
:::info
You don't need to understand these details to use Miren. This section is for the curious.
:::
**HTTP traffic** flows through Miren's HTTP ingress — a reverse proxy that listens on ports 80 and 443. When a request arrives, the ingress looks up the hostname to find the matching app, ensures a sandbox is running, and proxies the request to the sandbox's HTTP port.
**Non-HTTP traffic** is routed by the kernel using nftables NAT rules. When you deploy a service with non-HTTP ports, Miren allocates an internal IP for the service and programs firewall rules that forward matching traffic (by port and protocol) to the running sandboxes. As instances scale up and down, these rules are updated automatically to load-balance across available endpoints.
## Examples
### IRC Server
```toml
name = "irc"
[services.irc]
command = "./ircd"
[[services.irc.ports]]
port = 6667
name = "irc"
type = "tcp"
node_port = 6667
[[services.irc.ports]]
port = 6697
name = "irc-tls"
type = "tcp"
node_port = 6697
```
### Game Server with HTTP Admin Panel
```toml
name = "gameserver"
[services.game]
command = "./server"
[[services.game.ports]]
port = 3000
name = "admin"
type = "http"
[[services.game.ports]]
port = 27015
name = "game"
type = "udp"
node_port = 27015
[services.game.concurrency]
mode = "fixed"
num_instances = 1
```
### TCP Echo Server
```toml
name = "tcp-echo"
[services.echo]
command = "./tcp-echo"
[[services.echo.ports]]
port = 3000
name = "health"
type = "http"
[[services.echo.ports]]
port = 7000
name = "echo"
type = "tcp"
node_port = 7000
[services.echo.concurrency]
mode = "fixed"
num_instances = 1
```
## Next Steps
- [app.toml Reference](/app-toml#ports) — Complete field reference for port configuration
- [Services](/services) — Defining services, commands, images, and scaling
- [TLS Certificates](/tls) — How HTTPS works for HTTP services
- [Firewall Configuration](/firewall) — Host-level firewall rules and cloud provider setup
- [Application Scaling](/scaling) — How services scale up and down
---
## Troubleshooting
A step-by-step guide to diagnosing issues with your Miren applications and server.
## Quick health check
Start with `miren doctor` to verify your environment is set up correctly:
```miren
miren doctor
```
This checks your configuration, server connectivity, and authentication. It provides context-aware suggestions when it detects issues. You can also run the subcommands individually:
```miren
miren doctor config # Check cluster configuration
miren doctor server # Check server connectivity
miren doctor auth # Check authentication
```
## App not starting
If your app is deployed but not responding:
**1. Check the app status**
```miren
miren app status -a myapp
```
This shows the current deployment state, version, configuration, and any error messages.
**2. Check the logs**
```miren
# Recent logs
miren logs -a myapp
# Live tail
miren logs -a myapp -f
# Filter for errors
miren logs -a myapp -g error
# Logs from a specific service
miren logs -a myapp --service web
```
**3. Check sandbox state**
```miren
miren sandbox list
```
Look for sandboxes that are stuck in `pending` or `not_ready`, or that have gone `dead`. Use `--all` to include dead sandboxes in the output.
## Deploy failed
**1. Find the failed deployment**
```miren
miren app history -a myapp
```
Failed deployments are marked with a red `✗`. Use `--detailed` for more info including error messages and git SHAs.
**2. Check build logs**
```miren
miren logs build -a myapp VERSION
```
Replace `VERSION` with the version from the deployment history. This shows the build output so you can see where things went wrong. See [Logs](/logs) for more on filtering and following logs.
## Server-level issues
If you suspect the Miren server itself is having problems:
**1. Check server logs on the host**
For systemd installations:
```bash
sudo journalctl -u miren -f
```
For Docker installations:
```bash
docker logs -f miren
```
**2. Test connectivity**
```miren
miren debug connection
```
This tests RPC and HTTP connectivity to the server and reports the server version and auth status.
## Gathering a debug bundle
If you've worked through the steps above and need further help, collect a debug bundle to share:
```miren
sudo miren debug bundle
```
This creates a `miren-debug.tar.gz` archive containing system info, container state, process lists, and server logs.
:::warning Review bundles before sharing
Debug bundles collect diagnostic data that may include sensitive information:
- **Process command lines** — arguments passed to running processes may contain tokens or credentials
- **Application logs** — error messages and stack traces can include request data or internal details
Environment variable values are automatically redacted from container inspect output, but logs and process arguments are included as-is. Review the bundle contents and remove anything sensitive before sharing, especially in public channels like GitHub Issues.
:::
:::tip Use sudo for a complete bundle
Without sudo, the command still runs but produces a partial bundle. Root access is needed for:
- **Containerd socket** — the primary source of container state
- **System journal** — miren server logs via journalctl
- **Docker socket** — unless your user is in the `docker` group
:::
### Bundle options
| Flag | Description | Default |
|------|-------------|---------|
| `-o, --output` | Output file path | `miren-debug.tar.gz` |
| `-s, --since` | Include logs since this time | `1 day ago` |
| `-d, --docker-container` | Docker container name | `miren` |
```miren
# Include logs from the last week
sudo miren debug bundle --since "7 days ago"
# Save to a specific path
sudo miren debug bundle -o /tmp/miren-debug.tar.gz
```
## Getting help
If you're stuck, share your debug bundle and what you've tried:
- **Discord** — [miren.dev/discord](https://miren.dev/discord) for community help and questions
- **GitHub Issues** — [File a bug report](https://github.com/mirendev/runtime/issues/new?template=bug_report.yml) and attach your debug bundle
- **Feature Requests** — [Miren Roadmap](https://github.com/mirendev/roadmap/issues) for ideas and suggestions
Remember to review debug bundles for sensitive data before attaching them to public issues.
---
## Web Application Firewall (WAF)
Miren includes a built-in WAF that filters malicious HTTP requests before they reach your app. It uses the [OWASP Core Rule Set](https://coreruleset.org/) (CRS) to detect common attacks like SQL injection, cross-site scripting (XSS), and path traversal.
WAF is configured per route. Enable it and all requests to that route are inspected — malicious requests get a `403 Forbidden` response, clean requests pass through normally. No changes to your app are needed.
:::note What this doesn't cover
The WAF inspects request content for attack payloads (SQL injection, XSS, command injection, path traversal). It does **not** rate-limit, fingerprint bots, or block reconnaissance scans (e.g., probes for `/wp-admin/` or `/xmlrpc.php` on non-WordPress sites). For that kind of filtering, use an upstream proxy like Cloudflare in front of your route.
:::
## Enabling WAF
```miren
miren route waf myapp.example.com
```
This enables WAF at paranoia level 1 (the default), which catches well-known attack patterns with minimal false positives.
To use a higher paranoia level:
```miren
miren route waf myapp.example.com --level 2
```
## Disabling WAF
```miren
miren route waf myapp.example.com --disable
```
## Paranoia Levels
The paranoia level controls how aggressively the WAF inspects requests. Higher levels catch more attacks but may also flag legitimate requests as malicious (false positives).
| Level | Description | Use case |
|-------|-------------|----------|
| **1** | Baseline — catches obvious attacks (SQL injection, XSS, etc.) with very few false positives | Most apps (recommended default) |
| **2** | Elevated — additional rules for less common attack patterns | Apps handling sensitive data |
| **3** | High — stricter matching, may flag unusual but legitimate requests | High-security environments |
| **4** | Maximum — strictest rule set, highest false positive rate | Specialized security requirements |
Start with level 1. If you need tighter security, increase the level and monitor for false positives.
## What Gets Blocked
The OWASP CRS protects against the most common web attack categories:
- **SQL injection** — `?id=1 OR 1=1--`
- **Cross-site scripting (XSS)** — `?q=`
- **Path traversal** — `/../../etc/passwd`
- **Remote code execution** — shell command injection in parameters
- **Protocol violations** — malformed HTTP requests
- **Request smuggling** — ambiguous content-length/transfer-encoding
When a request is blocked, the client receives a `403 Forbidden` response. The original request never reaches your app.
## Checking WAF Status
WAF status appears in both `route show` and `route list`:
```miren
miren route show myapp.example.com
```
The output includes a `WAF Level` field when WAF is enabled. `route list` shows a `WAF` column with the level number or `-` when disabled.
## Default Route
WAF works with the default route too:
```miren
miren route waf --default --level 1
miren route waf --default --disable
```
## JSON Output
Both `route waf` and `route show` support `--format json` for scripting:
```miren
miren route waf myapp.example.com --format json
miren route show myapp.example.com --format json
```
The JSON output includes a `waf_level` field (integer, 0 when disabled).
## How It Works
WAF inspection runs in the HTTP ingress layer, before any other middleware (including OIDC authentication). The processing order for an incoming request is:
1. **WAF** — inspect the request against OWASP CRS rules
2. **OIDC** — authenticate the user (if route protection is configured)
3. **Proxy** — forward the request to the app sandbox
Miren uses [Coraza](https://coraza.io/), an open-source WAF engine compatible with ModSecurity rules, with the full OWASP Core Rule Set embedded. WAF engines are created per paranoia level and cached — there's no per-request initialization overhead.
Request bodies up to 10 MB are inspected. Requests with bodies exceeding this limit are rejected with HTTP 413.
## See Also
- [CLI: `miren route waf`](/command/route-waf) — enable or disable WAF on a route
- [Traffic Routing](/traffic-routing) — how routes work
- [Protecting Routes](/route-protect) — OIDC authentication for routes