Define multiple processes in a single file and start them all with one command.
An ecosystem config file lets you define all your application processes in one place — their scripts, arguments, environment variables, restart policies, and more. Both TOML and JSON formats are supported.
# Start all apps defined in the config
alter start alter.config.toml
alter start alter.config.json# alter.config.toml
[[apps]]
name = "api"
script = "python"
args = ["-m", "uvicorn", "main:app", "--port", "8000"]
cwd = "C:\\projects\\api"
autorestart = true
max_restarts = 10
restart_delay_ms = 2000
namespace = "web"
max_log_size_mb = 25
[apps.env]
PORT = "8000"
DATABASE_URL = "postgres://localhost/mydb"
DEBUG = "false"
[[apps]]
name = "worker"
script = "node"
args = ["dist/worker.js"]
cwd = "C:\\projects\\worker"
autorestart = true
max_restarts = 5
restart_delay_ms = 5000
namespace = "workers"
watch = true
watch_paths = ["dist/"]
watch_ignore = ["node_modules", "*.log", "*.map"]
[apps.env]
NODE_ENV = "production"
QUEUE = "default"
[[apps]]
name = "scheduler"
script = "go"
args = ["run", "cmd/scheduler/main.go"]
cwd = "C:\\projects\\scheduler"
namespace = "workers"
autorestart = true
[apps.env]
TZ = "UTC"{
"apps": [
{
"name": "api",
"script": "python",
"args": ["-m", "uvicorn", "main:app", "--port", "8000"],
"cwd": "C:\\projects\\api",
"autorestart": true,
"max_restarts": 10,
"namespace": "web",
"env": {
"PORT": "8000",
"DATABASE_URL": "postgres://localhost/mydb"
}
},
{
"name": "worker",
"script": "node",
"args": ["dist/worker.js"],
"watch": true,
"watch_paths": ["dist/"],
"namespace": "workers",
"env": {
"NODE_ENV": "production"
}
}
]
}| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name |
string | yes | — | Display name. Used as process identifier in CLI commands |
script |
string | yes | — | Executable to run (python, node, go, dotnet, etc.) |
args |
string[] | no | [] |
Arguments passed to the script |
cwd |
string | no | current dir | Working directory. Use absolute paths. On Windows, use double backslashes or forward slashes |
env |
object | no | {} |
Environment variables as key-value string pairs |
autorestart |
bool | no | true |
Automatically restart if the process exits with a non-zero code |
max_restarts |
integer | no | 10 |
Maximum number of restart attempts before marking as errored |
restart_delay_ms |
integer | no | 1000 |
Base delay in milliseconds before the first restart. Doubles on each attempt (capped at 60s) |
namespace |
string | no | "default" |
Logical group for organizing processes in the dashboard |
watch |
bool | no | false |
Enable watch mode — restart the process when files change |
watch_paths |
string[] | no | [] |
Directories or files to watch. Relative paths resolved from cwd |
watch_ignore |
string[] | no | [] |
Glob patterns to exclude from watching |
max_log_size_mb |
integer | no | 10 |
Rotate log file when it exceeds this size (in megabytes) |
cron |
string | no | null | Cron schedule; when set, the process sleeps between scheduled runs |
cron_last_run |
timestamp | no | null | Runtime metadata persisted by RunDock; normally omit from authored configs |
cron_next_run |
timestamp | no | null | Runtime metadata persisted by RunDock; normally omit from authored configs |
instances |
integer | no | 1 |
Reserved — parsed but not yet active |
log_file |
string | no | auto | Custom path for stdout log. Defaults to %APPDATA%\alter-pm2\logs\<name>\out.log |
error_file |
string | no | auto | Custom path for stderr log. Defaults to %APPDATA%\alter-pm2\logs\<name>\err.log |
notify |
object | no | null | Per-process notification config override (see Notifications) |
env_file |
string | no | null | Path to a .env file — loaded and merged with env (explicit env wins on conflict) |
health_check_url |
string | no | null | HTTP (http://...) or TCP (host:port) probe URL for health checks |
health_check_interval_secs |
integer | no | 30 |
Seconds between health probes |
health_check_timeout_secs |
integer | no | 5 |
Seconds before a probe times out |
health_check_retries |
integer | no | 3 |
Consecutive failures before marking as unhealthy and firing a notification |
pre_start |
string | no | null | Shell command to run before the process starts (blocks start on failure) |
post_start |
string | no | null | Shell command to run after the process starts (non-blocking, failures are logged) |
pre_stop |
string | no | null | Shell command to run before the process is killed (failures are logged) |
The executable to run. RunDock handles the following automatically:
- Python:
python,python3,py - Node.js:
node - Go:
go(e.g.args = ["run", "main.go"]) - Rust:
cargo(e.g.args = ["run", "--release"]) - .NET:
dotnet - PHP:
php - Ruby:
ruby - Any
.exeon Windows — spawned directly - Batch scripts (
.cmd) — automatically wrapped incmd /C
Windows note: Tools like
npm,yarn,npx,tsc,nodemonare.cmdbatch files. RunDock wraps them incmd /Cautomatically, so you can use them directly as thescriptvalue.
# These all work on Windows:
script = "npm"
args = ["run", "start"]
script = "npx"
args = ["nodemon", "index.js"]
script = "dotnet"
args = ["MyApp.dll"]Working directory for the process. If not specified, the daemon's working directory is used.
# Windows (double backslash)
cwd = "C:\\Users\\me\\projects\\api"
# Windows (forward slash also works)
cwd = "C:/Users/me/projects/api"Environment variables are merged with the system environment. The process inherits all existing environment variables, plus any you define here.
[apps.env]
NODE_ENV = "production"
PORT = "3000"
DATABASE_URL = "postgres://localhost/mydb"
API_KEY = "secret"autorestart = true # restart on crash
max_restarts = 10 # give up after 10 attemptsA clean exit (code 0) is not treated as a crash — the process stays in stopped state and is not restarted.
Exponential backoff delay:
attempt 0: 1000ms (base)
attempt 1: 2000ms
attempt 2: 4000ms
attempt 3: 8000ms
...
attempt 8+: 60000ms (capped)
Setting a higher base (e.g. 5000) extends all delays proportionally.
watch = true
watch_paths = ["src/", "config/"]
watch_ignore = ["node_modules", "__pycache__", "*.log", "*.pyc"]Watch mode is ideal for development. The process restarts automatically after a 500ms debounce whenever watched files change.
Namespaces appear as collapsible groups in the web dashboard. They have no effect on process behavior.
namespace = "web" # dashboard shows under "WEB" group
namespace = "workers" # dashboard shows under "WORKERS" group
namespace = "default" # (default if omitted)# API server
[[apps]]
name = "api"
script = "python"
args = ["-m", "uvicorn", "app.main:app", "--reload"]
cwd = "C:/Users/me/projects/api"
namespace = "web"
[apps.env]
PYTHONPATH = "."
# Node backend
[[apps]]
name = "node-api"
script = "node"
args = ["dist/index.js"]
cwd = "C:/Users/me/projects/node-api"
namespace = "web"
[apps.env]
NODE_ENV = "production"
PORT = "3001"
# .NET service
[[apps]]
name = "grpc-service"
script = "dotnet"
args = ["MyService.dll"]
cwd = "C:/Users/me/projects/service/bin/Release/net8.0"
namespace = "services"
# Background worker (npm script)
[[apps]]
name = "queue-worker"
script = "npm"
args = ["run", "worker"]
cwd = "C:/Users/me/projects/worker"
namespace = "workers"
autorestart = true
max_restarts = 20
restart_delay_ms = 3000
[apps.env]
NODE_ENV = "production"
REDIS_URL = "redis://localhost:6379"# Start all apps
alter start alter.config.toml
# Also works
alter start C:\projects\alter.config.toml
alter start ./alter.config.jsonRunDock detects config files by their extension (
.tomlor.json). Any other value is treated as a script to run directly.
After loading, each app appears as a separate process in alter list and the RunDock dashboard, with its own logs, restart counter, and controls.
Use env_file to load environment variables from a .env file. Values are merged with explicit env — explicit env keys always win on conflict.
[[apps]]
name = "api"
script = "python"
args = ["-m", "uvicorn", "main:app"]
cwd = "C:/projects/api"
env_file = ".env" # relative to cwd, or absolute path
[apps.env]
PORT = "9000" # this overrides PORT from .env if both existThe .env file uses standard KEY=VALUE format with comment support:
DATABASE_URL=postgres://localhost/mydb
SECRET_KEY=supersecret
PORT=8000
# This is a comment
DEBUG=falseUse health_check_url to probe your process after it starts. Supports HTTP/HTTPS (checks for 2xx) and raw TCP (checks for successful connection).
[[apps]]
name = "api"
script = "python"
args = ["-m", "uvicorn", "main:app", "--port", "8000"]
health_check_url = "http://localhost:8000/health"
health_check_interval_secs = 30 # probe every 30s (default)
health_check_timeout_secs = 5 # timeout per probe (default)
health_check_retries = 3 # failures before marking unhealthy (default)TCP probe example (useful for databases, Redis, etc.):
health_check_url = "localhost:5432" # just host:port for TCPWhen health_check_retries consecutive probes fail, the process is marked unhealthy and a notification is fired (if configured). The status recovers automatically when probes succeed again.
Run shell commands at key lifecycle events. On Windows, hooks run via cmd /C. On Linux/macOS via sh -c.
[[apps]]
name = "api"
script = "node"
args = ["dist/index.js"]
cwd = "C:/projects/api"
pre_start = "npm run db:migrate" # blocks start — failure aborts launch
post_start = "echo 'api is up' >> app.log" # non-blocking after process starts
pre_stop = "npm run cleanup" # runs before process is killed| Hook | When | Failure behaviour |
|---|---|---|
pre_start |
Before spawning the process | Aborts the start — process is not launched |
post_start |
After process reaches running state |
Logged as a warning, process keeps running |
pre_stop |
Before killing the process | Logged as a warning, process is killed anyway |
Use the notify field to override notification settings for a specific process. It takes priority over namespace-level and global notification configs.
[[apps]]
name = "api"
script = "python"
args = ["-m", "uvicorn", "main:app"]
namespace = "web"
[apps.notify.slack]
webhook_url = "https://hooks.slack.com/services/..."
enabled = true
channel = "#api-alerts"
[apps.notify.events]
on_crash = true
on_restart = true
on_start = false
on_stop = falseYou can also use a generic webhook:
[apps.notify.webhook]
url = "https://your-service.example.com/alter-hook"
enabled = true
[apps.notify.events]
on_crash = trueNote: If
notifyis omitted on a process, the namespace config applies. If the namespace has no config, the global config applies. Configure global and namespace defaults via the REST API — see Notification Endpoints.
Naming conventions:
- Keep names short and lowercase:
api,web,worker,scheduler - Names must be unique — duplicate names will overwrite each other
Iterating quickly:
- Edit the config file, then
alter start alter.config.tomlagain - Already-running processes with the same name will be updated and restarted
Organizing large projects:
# Group everything by role using namespaces
[[apps]]
namespace = "web" # api, frontend, proxy
[[apps]]
namespace = "workers" # queue consumers, schedulers
[[apps]]
namespace = "infra" # redis proxy, health checks