Host, supervise, monitor, and operate all your Python bots from a single Docker container with a built-in web dashboard, REST API, crash recovery, webhook alerts, and graceful shutdown.
- Overview
- What's New in v2
- Architecture
- Quick Start (5 minutes)
- Configuration Reference
- Web Dashboard
- REST API Reference
- Environment Variables
- Docker Deployment
- Production Hardening
- Security Model
- Monitoring & Alerting
- Troubleshooting
- Testing
- Migration Guide (v1 → v2)
- Contributing
- License
MultiBots runs multiple Python bots (typically Telegram bots, but any long-running Python script works) inside a single Docker container. Each bot runs as its own subprocess with its own environment, working directory, and lifecycle — supervised by a single watchdog process that restarts crashed bots, drains their logs, and reports state to a password-protected web dashboard.
Why one container for many bots? Most Telegram bots idle at <50 MB RAM. Running each in its own container wastes 300–500 MB of overhead per bot. MultiBots lets you colocate 5–20 bots in a single 512 MB container — cutting hosting costs by 80–95% on platforms like Render, Heroku, Scalingo, Fly.io, or a $5 VPS.
Who is this for?
- Indie developers running multiple side-project bots
- Small teams that want one ops surface for several bots
- Students learning process supervision / Docker / Flask
- Anyone who has ever been paged at 3am because a free-host container slept and their bot stopped responding
v2 is a full enterprise rewrite of v1, with 100% backward compatibility
for the original config.json schema. The original 5 files / 90 lines of
Python became a 9-section modular supervisor + a separate dashboard module.
| Capability | v1.0 | v2.0 |
|---|---|---|
| Multi-bot hosting in one container | ✅ | ✅ |
| Private repo cloning | ✅ | ✅ |
| Per-bot env vars | ✅ | ✅ |
| Keep-alive pinger | ✅ fixed (127.0.0.1, no race) | |
| Process supervision (auto-restart) | ❌ | ✅ exponential backoff |
| Max-restart cap (give up after N) | ❌ | ✅ |
| Graceful shutdown (SIGTERM/SIGINT) | ❌ orphans children | ✅ drains cleanly |
| Web dashboard (HTML) | ❌ gif only | ✅ live CPU/RAM/logs/controls |
| Password auth on dashboard | ❌ | ✅ PBKDF2 + CSRF + rate limit |
| REST API for bot control | ❌ | ✅ start/stop/restart/enable/disable |
| Per-bot metrics (CPU/RAM history) | ❌ | ✅ 5s sampling, 180pt window |
| System metrics (load/disk/swap) | ❌ | ✅ |
| Webhook crash alerts (Slack/Discord) | ❌ | ✅ |
| Structured logging w/ per-bot tagging | ❌ | ✅ |
| Config validation | ❌ crashes weirdly | ✅ fail-fast, skip-and-continue |
| Env-var overrides for every setting | ❌ | ✅ |
| Healthcheck endpoint | ❌ | ✅ /healthz + /readyz |
| Non-root Docker user | ❌ | ✅ uid 1000 |
| Multi-arch Docker image (amd64+arm64) | ❌ | ✅ |
| Hot-reload-friendly config | ❌ | ✅ |
| Test suite | ❌ | ✅ 117 tests (unit + integration + e2e) |
| Python version | 3.9 (EOL Oct 2025) | 3.11 |
Any config.json that worked with v1 still works with v2 — no migration
required. New optional fields (enabled, max_restarts, restart_delay_base,
health_url, args, etc.) default to safe values when omitted. See
§15. Migration Guide.
┌─────────────────────────────────────────────────────────────────┐
│ Docker Container │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ dashboard.py (Flask + gunicorn) │ │
│ │ │ │
│ │ /login ───────► PBKDF2 verify + HMAC session cookie │ │
│ │ / ──► HTML dashboard (auto-refresh) │ │
│ │ /api/status ──► JSON system + bot snapshot │ │
│ │ /api/bots/X/<action> ──► start/stop/restart/... │ │
│ │ /healthz, /readyz ──► unauth liveness probes │ │
│ └─────────────┬─────────────────────────────────────────────┘ │
│ │ imports │
│ ┌─────────────▼─────────────────────────────────────────────┐ │
│ │ main.py (supervisor) │ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌────────────────┐ │ │
│ │ │ ConfigLoader │ │ BotSuperv. │ │MetricsCollector│ │ │
│ │ │ (validate) │ │ (watchdog) │ │ (psutil) │ │ │
│ │ └──────────────┘ └──────┬───────┘ └────────────────┘ │ │
│ │ │ start/stop/restart │ │
│ │ ┌──────────────────┐ │ │ │
│ │ │ WebhookNotifier │◄───┤ on crash │ │
│ │ └──────────────────┘ │ │ │
│ │ ┌──────────────────┐ │ │ │
│ │ │ KeepAlivePinger │ │ │ │
│ │ └──────────────────┘ │ │ │
│ │ ┌──────────────────┐ │ │ │
│ │ │ GracefulShutdown │◄───┤ SIGTERM/SIGINT │ │
│ │ └──────────────────┘ │ │ │
│ └──────────────────────────┼───────────────────────────────┘ │
│ │ subprocess.Popen │
│ ┌───────────────┐ ┌────────▼───────┐ ┌───────────────────┐ │
│ │ bot1/ │ │ bot2/ │ │ bot3/ │ │
│ │ └── main.py │ │ └── bot.py │ │ └── main.py │ │
│ └───────────────┘ └────────────────┘ └───────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
| File | Purpose | Lines |
|---|---|---|
main.py |
Supervisor core (config + supervisor + metrics + hooks) | ~700 |
dashboard.py |
Web UI + REST API + auth | ~700 |
config.json |
Bot definitions (your bots) | varies |
run.sh |
Build-time: clone bot repos + install their deps | ~90 |
Dockerfile |
Image build (multi-arch, non-root, healthcheck) | ~55 |
requirements.txt |
MultiBots runtime deps (Flask, psutil, requests, gunicorn) | 5 |
tests/ |
Unit + integration + e2e tests (117 tests) | ~700 |
scripts/ |
Live smoke tests + backward-compat tests | ~250 |
Main thread ─────► dashboard.py gunicorn workers (HTTP)
│
├─► multibots.supervisor (daemon) — watchdog loop
├─► multibots.metrics (daemon) — 5s sampler
├─► multibots.pinger (daemon) — keep-alive
├─► multibots.shutdown (signal handler)
└─► multibots.bot.<name> (daemon per bot) — log drainer
All daemon threads die when the main thread exits; the SIGTERM handler
calls os._exit(0) after draining to bypass any hung subprocess.
- Docker 20+ (or Python 3.9+ if running without Docker)
- A GitHub account (for cloning bot repos)
- Bot tokens (e.g. from @BotFather for Telegram)
git clone https://github.com/maruf009sultan/MultiBots.git
cd MultiBotsEdit config.json:
{
"my_first_bot": {
"source": "https://github.com/your-username/your-bot-repo.git",
"run": "main.py",
"env": {
"TOKEN": "123456:ABC-DEF_your_telegram_bot_token",
"OTHER_VAR": "value"
},
"enabled": true
},
"another_bot": {
"source": "https://github.com/your-username/another-bot.git",
"run": "bot.py",
"env": {"TOKEN": "another-token"},
"max_restarts": 10
}
}Each bot repo must have its own
requirements.txtat the root. MultiBots installs it automatically during the Docker build.
docker build -t multibots .
docker run -p 10000:10000 multibotsVisit http://localhost:10000 in your browser. On first boot, MultiBots
auto-generates a password and prints it to the Docker logs:
docker logs multibots 2>&1 | grep "Generated:"
# → No dashboard password configured. Generated: river-frost-42Log in with that password. You'll see:
- Live CPU / memory / disk / load averages for the container
- A card per bot showing status, PID, uptime, restart count, CPU%, memory
- Start / Stop / Restart buttons for each bot
- A live supervisor log tail
config.json is the single source of truth for bot definitions. It supports
three kinds of top-level keys:
_dashboard— optional dashboard settings (password, session secret)_global— optional default values inherited by every bot<bot_name>— bot definitions (any name not starting with_)
{
"_dashboard": {
"password_hash": "pbkdf2_sha256$240000$<salt>$<hash>",
"session_secret": "any-random-string"
},
"_global": {
"max_restarts": 5,
"restart_delay_base": 2.0
},
"my_bot": {
"source": "https://github.com/user/repo.git",
"run": "main.py",
"env": {"TOKEN": "...", "DEBUG": "true"},
"enabled": true,
"max_restarts": 5,
"restart_delay_base": 2.0,
"health_url": "",
"health_timeout": 5.0,
"cwd": null,
"python": "python3",
"args": ["--worker-count", "4"],
"timeout_graceful": 10.0
}
}| Field | Type | Required | Default | Description |
|---|---|---|---|---|
source |
string | ✅ | — | Git URL. Supports https://, http://, git@, ssh://. For private repos, embed the token: https://user:token@github.com/user/repo.git |
run |
string | ✅ | — | Entry filename inside the bot directory (e.g. main.py). Must be a bare filename — no path separators. |
env |
object | ❌ | {} |
Environment variables passed to the bot. All values coerced to strings. |
enabled |
bool | ❌ | true |
If false, the bot is never started and shows as "disabled" in the dashboard. |
max_restarts |
int | null | ❌ | inherits _global.max_restarts (default 5) |
Max restart attempts before giving up. null = inherit; 0 = never restart. |
restart_delay_base |
float | null | ❌ | inherits (default 2.0) |
Base delay in seconds for exponential backoff. Actual delay = min(base * 2^(attempt-1), restart_delay_max). |
health_url |
string | ❌ | "" |
Optional HTTP health probe URL. (Reserved for future use; not yet auto-polled.) |
health_timeout |
float | ❌ | 5.0 |
Timeout in seconds for the health probe. |
cwd |
string | null | ❌ | <bots_dir>/<bot_name> |
Override the working directory. If absolute, used as-is; if relative, joined with bots_dir. |
python |
string | ❌ | sys.executable |
Python interpreter to use (e.g. python3.11). |
args |
array | ❌ | [] |
Extra command-line arguments passed after the run file. |
timeout_graceful |
float | ❌ | 10.0 |
Seconds to wait after SIGTERM before sending SIGKILL. |
| Key | Purpose |
|---|---|
_dashboard.password_hash |
Pre-set the dashboard password (avoids auto-generation). Generate with: python -c "import dashboard; print(dashboard.hash_password('yourpw'))" |
_dashboard.session_secret |
Secret used to sign session cookies. If omitted, a random one is generated per process (sessions don't survive restart). |
_global.max_restarts |
Default max_restarts for bots that don't specify one. |
_global.restart_delay_base |
Default restart_delay_base for bots that don't specify one. |
Bot names must match ^[A-Za-z][A-Za-z0-9_-]{0,63}$ (1–64 chars, starts
with a letter, only letters/digits/underscore/hyphen). Invalid bot names
are skipped at load time with an error log — other bots still load.
The run field must be a bare filename (no / or \, no absolute
paths). This prevents path traversal outside the bot directory.
If a single bot's config is invalid, that bot is skipped — the rest still load. The supervisor never crashes on a single bad bot.
The dashboard runs on port 10000 by default (override with MB_PORT).
It provides:
| Route | Auth | Description |
|---|---|---|
GET / |
✅ | HTML dashboard (auto-refreshes every 30s) |
GET /login |
❌ | Login form |
POST /login |
❌ | Authenticate, set session cookie |
POST /logout |
❌ | Clear session |
GET /healthz |
❌ | Liveness probe (always 200) |
GET /readyz |
❌ | Readiness probe (200 if supervisor attached, else 503) |
- System overview panel: CPU %, load averages (1/5/15m), core count, memory used/total, swap, disk %, process count, container uptime.
- Per-bot cards: status dot (running/crashed/stopped/restarting/disabled), PID, uptime, restart count, last exit code, CPU%, memory MB, last error.
- Action buttons: Start / Restart / Stop (with confirmation dialog).
- Logs link: opens a per-bot log tail in a new tab.
- Live supervisor log: auto-refreshing supervisor log at the bottom.
- Color-coded bars: green (OK), yellow (warn), red (critical) for CPU and memory thresholds.
- Password storage: PBKDF2-HMAC-SHA256, 240,000 iterations, 24-byte salt.
Format:
pbkdf2_sha256$<iter>$<salt_b64>$<hash_b64>(Django-compatible). - Session: HMAC-signed cookie, 12-hour TTL,
HttpOnly+SameSite=Lax. MarkedSecurewhen the request is over HTTPS. - CSRF: every state-changing POST requires
X-CSRF-Tokenheader matching the session-issued token. Bad/missing token → 403. - Rate limiting: 5 failed login attempts per minute per IP → 429.
If no password is configured (neither env var nor _dashboard.password_hash),
MultiBots generates one on first boot, prints it to stdout/logs, and
persists the hash to .multibots_dashboard_pw next to config.json so
it survives restarts.
# View the auto-generated password from logs:
docker logs multibots 2>&1 | grep "Generated:"
# Or set a fixed password:
python -c "import dashboard; print(dashboard.hash_password('your-strong-pw'))"
# → pbkdf2_sha256$240000$abc...$def...
# Paste that into config.json under _dashboard.password_hashAll /api/* endpoints require authentication (session cookie). State-changing
endpoints also require a CSRF token (the X-CSRF-Token header).
Returns the full system + per-bot snapshot.
{
"ts": "2026-07-05T18:40:09Z",
"version": "2.0.0",
"system": {
"cpu_percent": 12.3,
"cpu_count": 4,
"load_1": 0.45, "load_5": 0.32, "load_15": 0.28,
"mem_total_mb": 2048.0, "mem_used_mb": 612.5, "mem_percent": 29.9,
"swap_total_mb": 1024.0, "swap_used_mb": 0.0, "swap_percent": 0.0,
"disk_percent": 47.2,
"boot_time": 1783276000.0,
"process_count": 87
},
"bots": [
{
"name": "my_bot",
"status": "running",
"pid": 1234,
"uptime_s": 3600.5,
"restart_count": 0,
"last_exit_code": null,
"last_error": "",
"cpu_percent": 1.2,
"memory_mb": 45.3,
"history": [{"t": 1783276000.0, "cpu": 1.1, "mem": 45.0}, ...],
"config": {
"name": "my_bot",
"source": "https://github.com/user/repo.git",
"run": "main.py",
"enabled": true,
"max_restarts": 5,
"has_env": true,
"env_keys": ["TOKEN"],
"env_masked": {"TOKEN": "1234…cdef"},
...
}
}
]
}Secret masking: env values are always masked in API responses. A value of length
Lis shown as<first4>…<last4>ifL > 8, else*repeatedLtimes. Raw values never leave the process.
Returns an array of all bot states (same shape as bots[] above).
Returns a single bot's state. 404 if unknown.
Returns up to 2000 lines of recent log output for the bot as text/plain.
Lines are tagged with the bot logger name: [multibots.bot.<name>].
Returns recent supervisor log lines (all bots + supervisor events).
Returns the per-bot CPU/RAM history as a JSON array of {t, cpu, mem} samples
(5s interval, up to 180 points = 15 min window).
All actions are POST and require X-CSRF-Token header.
| Endpoint | Effect |
|---|---|
POST /api/bots/<name>/start |
Start the bot if stopped. Idempotent (no-op if already running). |
POST /api/bots/<name>/stop |
SIGTERM → wait timeout_graceful → SIGKILL if still alive. |
POST /api/bots/<name>/restart |
Stop then start. New PID. |
POST /api/bots/<name>/enable |
Set enabled=true and start the bot. |
POST /api/bots/<name>/disable |
Set enabled=false and stop the bot. Watchdog will not restart it. |
{"ok": true, "action": "restart", "bot": "my_bot"}On error:
{"ok": false, "error": "unknown bot"}# 1. Login (saves cookies to ./cookies.txt)
curl -c cookies.txt -X POST http://localhost:10000/login \
-d "password=your-password"
# 2. Get CSRF token (from the dashboard HTML)
CSRF=$(curl -b cookies.txt http://localhost:10000/ | \
grep -oP 'const csrf = "\K[^"]+')
# 3. Restart a bot
curl -b cookies.txt -X POST http://localhost:10000/api/bots/my_bot/restart \
-H "X-CSRF-Token: $CSRF"Every setting in DEFAULTS (see main.py) can be overridden via an
environment variable with the MB_ prefix. Env vars take precedence over
config.json defaults but not over explicit bot-level config.
| Variable | Default | Description |
|---|---|---|
MB_PORT |
10000 |
Dashboard HTTP port |
MB_HOST |
0.0.0.0 |
Dashboard bind address |
MB_BOTS_DIR |
/app |
Root directory containing bot subdirectories |
MB_CONFIG_PATH |
config.json |
Path to the bots config file |
MB_LOG_LEVEL |
INFO |
Root log level (DEBUG/INFO/WARNING/ERROR) |
MB_LOG_DIR |
logs |
Directory for rotating log files (empty = console only) |
MB_LOG_MAX_BYTES |
5242880 |
Max bytes per log file before rotation |
MB_LOG_BACKUP_COUNT |
5 |
Number of rotated log files to keep |
MB_START_DELAY |
2.0 |
Seconds between bot startups (avoid fork-bomb) |
MB_MAX_RESTARTS |
5 |
Default max restarts before giving up |
MB_RESTART_DELAY_BASE |
2.0 |
Base delay for exponential backoff |
MB_RESTART_DELAY_MAX |
120.0 |
Cap on backoff delay |
MB_WATCHDOG_INTERVAL |
10.0 |
Seconds between watchdog polls |
MB_PING_INTERVAL |
120.0 |
Keep-alive ping interval (set high to disable) |
MB_PING_URL |
http://127.0.0.1:10000/ |
URL to ping (use external URL for cross-host keep-alive) |
MB_WEBHOOK_URL |
"" |
Slack/Discord/generic webhook URL for crash alerts |
MB_WEBHOOK_TIMEOUT |
5.0 |
Webhook POST timeout |
MB_SHUTDOWN_TIMEOUT |
15.0 |
Grace period for SIGTERM before SIGKILL |
MB_METRICS_HISTORY |
180 |
Number of per-bot metric samples to keep (180 × 5s = 15min) |
MB_DASHBOARD_ENABLED |
true |
Set to false to disable the dashboard |
MB_DASHBOARD_PASSWORD_HASH |
"" |
Pre-set dashboard password hash |
MB_DASHBOARD_SESSION_SECRET |
"" |
Session signing secret (auto-generated if empty) |
docker build -t multibots .
docker run -d --name multibots -p 10000:10000 multibotsdocker buildx create --use --name mb-builder
docker buildx build --platform linux/amd64,linux/arm64 \
-t your-registry/multibots:latest --push .version: "3.8"
services:
multibots:
build: .
ports:
- "10000:10000"
volumes:
- ./config.json:/app/config.json:ro
- ./logs:/app/logs
environment:
- MB_WEBHOOK_URL=https://hooks.slack.com/services/...
- MB_DASHBOARD_PASSWORD_HASH=pbkdf2_sha256$240000$...
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:10000/healthz"]
interval: 30s
timeout: 5s
retries: 3
start_period: 15s- Push this repo to GitHub.
- New → Web Service → connect the repo.
- Build Command:
docker build -t multibots .(or leave blank — Render auto-detects Dockerfile). - Start Command:
docker run -p $PORT -e MB_PORT=$PORT multibots(or use Render's native Docker deploy). - Set
MB_DASHBOARD_PASSWORD_HASHin Render's Environment tab. - Exposed port: 10000 (Render auto-detects from
EXPOSE).
heroku container:push web -a your-app
heroku container:release web -a your-app
heroku config:set MB_DASHBOARD_PASSWORD_HASH=pbkdf2_sha256\$240000\$... -a your-appfly launch --image multibots
fly secrets set MB_DASHBOARD_PASSWORD_HASH=pbkdf2_sha256\$240000\$...
fly deploy# Generate the hash:
docker run --rm multibots python -c \
"import dashboard; print(dashboard.hash_password('your-strong-password'))"
# Paste the output into config.json:
{
"_dashboard": {
"password_hash": "pbkdf2_sha256$240000$abc...$def..."
}
}Or via env var (preferred for CI/CD):
export MB_DASHBOARD_PASSWORD_HASH='pbkdf2_sha256$240000$abc...$def...'Without a stable session secret, all users get logged out on every restart:
export MB_DASHBOARD_SESSION_SECRET="$(openssl rand -hex 32)"# Slack incoming webhook:
export MB_WEBHOOK_URL='https://hooks.slack.com/services/T.../B.../...'
# Discord webhook:
export MB_WEBHOOK_URL='https://discord.com/api/webhooks/.../...'Crash alerts include the bot name, exit reason, and timestamp. Webhook delivery is best-effort — a failed webhook never blocks the supervisor.
The dashboard sets Secure cookies automatically when the request is over
HTTPS. For termination at a reverse proxy (nginx/Caddy/Cloudflare), ensure
X-Forwarded-Proto: https is set so Flask sees request.is_secure == True.
In docker-compose.yml:
deploy:
resources:
limits:
memory: 512M
cpus: '0.5'MultiBots itself uses <50 MB RAM. The 512 MB limit leaves room for ~5–10 typical Telegram bots.
# Rotate logs with 10MB × 5 files:
export MB_LOG_MAX_BYTES=10485760
export MB_LOG_BACKUP_COUNT=5For Docker log drivers, the supervisor's stdout is unbuffered — docker logs
works without configuration.
MultiBots assumes:
- The container runtime (Docker/host) is trusted.
- The bot source code is trusted (you wrote or reviewed it).
- The dashboard port (10000) is not exposed to the public internet without TLS + a strong password.
| Secret | Where it lives | Risk |
|---|---|---|
| Bot env vars (TOKENs) | config.json env block + bot process env |
Medium — never logged, never exposed via API (masked). Filesystem access required to read. |
| GitHub token (private repos) | Embedded in source URL |
Medium — run.sh suppresses URL echo. Filesystem access required to read config.json. |
| Dashboard password | PBKDF2 hash in config or env var | Low — only the hash is stored; brute force at 240k iterations is impractical. |
| Session secret | Env var or auto-generated | Medium — if compromised, attacker can forge session cookies until restart. |
- ❌ It does not encrypt
config.jsonat rest. Use filesystem encryption (LUKS, gocryptfs) if your threat model requires it. - ❌ It does not sandbox bot subprocesses. A malicious bot can read other bots' env vars if it has filesystem access. Run untrusted bots in separate containers.
- ❌ It does not provide multi-user auth. The dashboard has a single shared password. For multi-user, put it behind OAuth Proxy / Authelia / Cloudflare Access.
Every state-changing API endpoint (POST /api/bots/...) requires an
X-CSRF-Token header that matches the session-issued token. The token is
embedded in the dashboard HTML as const csrf = "..." and rendered into
each action button's JavaScript. Browsers' SameSite=Lax cookie policy
provides defense-in-depth against cross-site requests.
Login attempts are throttled to 5 per minute per IP. The limiter is
in-memory (per-process); for multi-worker gunicorn deployments, each worker
tracks its own counter — effective limit is 5 × workers per minute.
The dashboard samples every bot every 5 seconds and stores up to 180 samples (15 minutes of history). For each sample:
- CPU % (via
psutil.Process.cpu_percent) - Memory MB (via
psutil.Process.memory_info().rss)
System-wide metrics (CPU, load, memory, swap, disk, process count) are
captured on each /api/status request.
| Endpoint | Use | Behavior |
|---|---|---|
/healthz |
Kubernetes liveness, Render health check | Always returns 200 if the process is alive |
/readyz |
Kubernetes readiness | 200 if supervisor is attached and config loaded; 503 otherwise |
The Dockerfile wires /healthz into HEALTHCHECK with a 30s interval,
5s timeout, 15s start period, and 3 retries.
When a bot crashes, MultiBots POSTs to MB_WEBHOOK_URL:
{
"text": "⚠️ MultiBots: bot *my_bot* crashed — exited code=1",
"content": "⚠️ MultiBots: bot **my_bot** crashed — exited code=1",
"username": "MultiBots",
"bot_name": "my_bot",
"reason": "exited code=1",
"ts": "2026-07-05T18:40:09Z"
}The payload includes both text (Slack format) and content (Discord
format) keys for max compatibility. Failed webhook delivery is logged but
never blocks the supervisor.
Point your external monitor (UptimeRobot, Pingdom, etc.) at:
http://your-host:10000/healthz
For richer monitoring, scrape /api/status with Prometheus (a custom
exporter is on the roadmap — see #Roadmap).
- Check the dashboard: the bot card shows the last error in red.
- Check the supervisor log:
docker logs multibots 2>&1 | grep "my_bot". - Verify the run file exists: The supervisor logs
Bot 'X' run file missing: /app/X/main.pyif the file isn't there. This usually meansrun.shfailed to clone the repo — check the Docker build output. - Verify the run file is valid Python:
docker exec -it multibots python3 /app/my_bot/main.pyto run it manually.
- The supervisor auto-restarts crashed bots up to
max_restarts(default 5) with exponential backoff (2s, 4s, 8s, ..., capped at 120s). - After hitting the cap, the bot stays "crashed" and won't restart automatically. Use the dashboard's Start button to retry.
- Check
/api/bots/<name>/logsfor the bot's stdout/stderr.
- The dashboard runs on port 10000 by default. Verify with
curl http://localhost:10000/healthzfrom inside the container. - If using gunicorn (default Dockerfile CMD), check
docker logsfor gunicorn startup errors.
If you used auto-generation, the password is in the first boot logs:
docker logs multibots 2>&1 | grep "Generated:"If that's gone, set a new one:
# Generate a new hash:
python -c "import dashboard; print(dashboard.hash_password('new-pw'))"
# Set via env var and restart:
export MB_DASHBOARD_PASSWORD_HASH='pbkdf2_sha256$240000$...'
docker restart multibotsAfter 5 failed attempts in 1 minute, you'll get HTTP 429. Wait 60 seconds or restart the container to clear the in-memory limiter.
If you see bot PIDs still running after docker stop, the bot ignored
SIGTERM. Increase timeout_graceful (default 10s) for that bot in
config.json so the supervisor sends SIGKILL later. For truly stuck
processes, docker kill multibots will clean up everything.
- Verify the token in the URL isn't expired.
- Check that the URL format is exactly:
https://USERNAME:TOKEN@github.com/USERNAME/REPO.git - The build suppresses the URL echo to avoid leaking tokens in logs.
Test the clone manually:
git clone 'https://USERNAME:TOKEN@github.com/USERNAME/REPO.git' /tmp/test
That's a warning, not an error. If your bot has no Python dependencies
beyond the standard library, it's fine. If it does have deps, add a
requirements.txt to your bot's repo root.
MultiBots ships with 117 automated tests covering every feature.
tests/
├── conftest.py # shared fixtures (fake bots, settings, wait_for)
├── test_config.py # config schema validation (35 tests)
├── test_supervisor.py # start/stop/restart/watchdog/shutdown (19 tests)
├── test_metrics.py # CPU/RAM sampling, history cap, snapshots (5 tests)
├── test_webhook.py # Slack/Discord webhook delivery (6 tests)
├── test_dashboard_auth.py # PBKDF2, sessions, rate limit, CSRF (17 tests)
├── test_dashboard_api.py # all /api/* routes (29 tests)
└── test_integration.py # end-to-end deployment scenarios (6 tests)
# From the repo root:
pip install -r requirements.txt pytest
pytest tests/ -v
# With coverage:
pytest tests/ --cov=main --cov=dashboard --cov-report=term-missing
# Just the integration tests:
pytest tests/test_integration.py -v
# Just the auth tests:
pytest tests/test_dashboard_auth.py -vThe scripts/smoke_live.py script boots a real dashboard subprocess on
port 10099 and exercises every endpoint with real HTTP requests. Use this
to verify the system works end-to-end outside of pytest:
python scripts/smoke_live.py
# → ✅ ALL LIVE SMOKE TESTS PASSED (17 checks)python scripts/smoke_backward_compat.py
# → ✅ ALL BACKWARD COMPAT TESTS PASSED| Category | What's tested |
|---|---|
| Config | All field types, validation rules, error paths, backward compat with v1 schema, global inheritance, env coercion, name validation |
| Supervisor | Start/stop/restart/enable/disable, watchdog auto-restart, max-restart cap, graceful shutdown (SIGTERM), SIGKILL fallback for stuck bots, concurrent operations (lock safety) |
| Metrics | Per-bot CPU/RAM sampling, history cap enforcement, dead bot → zero metrics, system snapshot never raises |
| Webhook | Disabled webhooks, successful delivery, HTTP errors, network errors, timeout passthrough |
| Auth | PBKDF2 hash format, correct/wrong/corrupted passwords, salt uniqueness, session issue/verify/expiry/tamper, rate limit (5/min, IP-scoped, window expiry), auto-generated password persistence |
| API | All read endpoints (status, bots, single bot, logs, metrics history), all action endpoints (start/stop/restart/enable/disable), auth gating on every protected route, CSRF enforcement, 404/400/403 error paths |
| Integration | Full deployment from real config.json, dashboard wired to supervisor, login → dashboard → API → action → logout flow, env masking (no secret leak), backward compat with v1 README example, hot-reload scenario, crash → webhook delivery |
The v1 schema ({bot: {source, env, run}}) is fully supported. You do not
need to change anything to upgrade.
- The dashboard now requires a password (auto-generated on first boot,
printed to logs). Find it with:
docker logs multibots 2>&1 | grep Generated: - Bots now auto-restart on crash (up to 5 times by default).
- The keep-alive pinger is fixed (was pinging
0.0.0.0which doesn't work on all platforms; now pings127.0.0.1). - Bots receive SIGTERM on container stop instead of being orphaned.
- The Docker image runs as non-root (uid 1000).
- Docker HEALTHCHECK is wired to
/healthz.
{
"_global": {
"max_restarts": 10
},
"my_legacy_bot": {
"source": "https://github.com/x/y.git",
"env": {"TOKEN": "..."},
"run": "main.py"
},
"my_modern_bot": {
"source": "https://github.com/x/z.git",
"env": {"TOKEN": "..."},
"run": "main.py",
"enabled": true,
"max_restarts": 20,
"restart_delay_base": 5.0,
"args": ["--worker-count", "4"]
}
}- Add
_globalto set defaults for all bots. - Add
_dashboard.password_hashto set a fixed dashboard password. - Add per-bot
enabled: falseto disable a bot without removing it. - Add per-bot
max_restarts: 0to disable auto-restart for that bot. - Add per-bot
argsto pass command-line arguments.
Nothing. v2 is a strict superset of v1.
git clone https://github.com/maruf009sultan/MultiBots.git
cd MultiBots
pip install -r requirements.txt pytest
pytest tests/ -v- Single-file modules (
main.py,dashboard.py) — keep sections clearly delimited with header comments. - Every public class/function has a docstring.
- Defensive error handling: supervisor must never crash on a single bot failure.
- Tests are mandatory for new features.
- Tests pass:
pytest tests/ -v - Live smoke test passes:
python scripts/smoke_live.py - Backward compat test passes:
python scripts/smoke_backward_compat.py - No new dependencies without justification
- README updated if behavior changed
- No secrets in code or commit history
- Prometheus metrics exporter (
/metricsendpoint) - WebSocket live log streaming (instead of polling)
- Per-bot log file rotation (instead of in-memory capture)
- Multi-user auth (OAuth/OIDC)
- Bot deployment without rebuild (git pull on restart)
- Config hot-reload without restart
- Per-bot CPU/RAM limits via cgroups
MIT — see LICENSE file.
# Build & run:
docker build -t multibots . && docker run -p 10000:10000 multibots
# View auto-generated password:
docker logs multibots 2>&1 | grep "Generated:"
# Tail supervisor log:
docker logs -f multibots
# Tail a specific bot's log:
docker exec multibots cat /app/logs/multibots.log | grep "bot.my_bot"
# Generate a dashboard password hash:
docker run --rm multibots python -c "import dashboard; print(dashboard.hash_password('pw'))"
# Run tests:
pytest tests/ -v
# Live smoke test:
python scripts/smoke_live.py| What | Default |
|---|---|
| Dashboard HTTP port | 10000 |
| Bot working directory | /app/<bot_name> |
| Config file | /app/config.json |
| Log directory | /app/logs (or logs/ if running locally) |
| Auto-gen password file | .multibots_dashboard_pw (next to config.json) |
- Bugs: GitHub Issues
- README source of truth: this file
- Code is self-documenting: read
main.pytop-to-bottom — every section has a header comment explaining its role.