01 What you need
ServerMind is deliberately light. If you have a Linux box and can paste a command into it, you're set.
You do not need a domain name, a TLS certificate, or a reverse proxy to get started — see Accessing it.
02 Install
One command. It installs Bun and PM2, clones the repo, runs the setup wizard, and starts ServerMind under PM2.
$ curl -fsSL https://servermind.dev/install.sh | bash
Prefer to do it by hand?
$ git clone https://github.com/AjjlalAhmed/servermind.git && cd servermind
$ bun install
$ bun run setup # guided config → writes .env
$ bun run start # or: pm2 start ecosystem.config.cjs
The setup wizard
Whichever way you install, bun run setup walks you through everything and writes it to .env. It's re-runnable anytime — it remembers your current values:
- Admin password — hashed with argon2id.
- Two-factor (TOTP) — scan a QR with Google Authenticator / Authy / 1Password and confirm a code.
- AI backend — Claude Code or any OpenAI-compatible API, with a live connection test.
- Access & networking — private (SSH/Tailscale) or a public HTTPS domain.
- PM2, monitored services, MySQL & Redis — optional, each with a live probe.
03 Choose your AI
ServerMind brings no AI of its own — you point it at one. Pick a backend in the wizard, or set it in .env.
Google Gemini free
Around 1,500 requests a day with no credit card. Get a key at aistudio.google.com.
AI_BACKEND=openai
AI_BASE_URL=https://generativelanguage.googleapis.com/v1beta/openai
AI_MODEL=gemini-2.0-flash
AI_API_KEY=<your key>Claude subscription best quality
Uses the locally-installed claude CLI with your subscription — no API key, no per-token billing. Install Claude Code and run claude login on the box first.
AI_BACKEND=claude-codeOther OpenAI-compatible APIs
- Groq —
https://api.groq.com/openai/v1(free, very fast) - OpenRouter —
https://openrouter.ai/api/v1 - Ollama —
http://127.0.0.1:11434/v1(local, no key, nothing leaves the box)
04 Accessing ServerMind
It binds to 127.0.0.1:5500 and serves its own UI. A domain and reverse proxy are optional — choose how you'll reach it in the wizard.
No domain (recommended to start)
Reach it privately from your laptop — nothing is ever exposed to the internet:
# SSH tunnel — then open http://localhost:5500
$ ssh -L 5500:127.0.0.1:5500 user@your-server
- Tailscale — set
BIND_HOSTto the server's tailnet IP and reach it from any device on your tailnet. - Cloudflare quick tunnel — a throwaway HTTPS URL with no domain owned:
cloudflared tunnel --url http://127.0.0.1:5500
Public domain over HTTPS
Front it with a reverse proxy. The wizard detects Caddy or Nginx and prints a ready-to-paste config; set SECURE_COOKIES=1 (it does this for you) so the session cookie is marked Secure, and set TRUST_PROXY=1 so brute-force lockout and rate limiting use the real client IP from your proxy's X-Forwarded-For instead of the proxy's own address.
# Caddy — automatic HTTPS
servermind.example.com {
reverse_proxy 127.0.0.1:5500 {
flush_interval -1 # stream the chat (SSE) without buffering
}
}Nginx works too — use proxy_pass with proxy_buffering off and get a certificate with certbot.
BIND_HOST=0.0.0.0 and browse to http://server-ip:5500 directly — that sends your credentials in cleartext. Use a tunnel, a tailnet, or HTTPS.05 Desktop app
The recommended way to reach your dashboard. ServerMind Desktop opens one outbound SSH tunnel to your controller and shows its dashboard in a native window — no domain, reverse proxy, certificate, or manual ssh -L. It's a viewer: nothing on your servers depends on it.
Download
- macOS — ServerMind.dmg (universal: Apple Silicon + Intel). First launch: right-click the app → Open (unsigned beta).
- Windows — ServerMind-Setup.exe (Windows 10/11). First launch: More info → Run anyway.
Connect over SSH (a remote server)
Click + Controller, choose SSH tunnel, and fill in how you reach the box over SSH:
- Host — your server's IP or hostname; SSH user — e.g.
rootorubuntu. - Authentication — Private key file (e.g.
~/.ssh/id_ed25519), SSH agent (a key already loaded), or Password. This is your SSH login — not your ServerMind password. - ServerMind port —
5500.
The app forwards a random local port to the controller's 127.0.0.1:5500, pins the SSH host key on first connect, and stores any secret in your OS keychain.
Connect directly (local / Docker / LAN)
If the controller is already reachable from your machine (local Docker, a LAN box, or a tailnet), pick Direct — no SSH, set Host to localhost (or the box's address) and port 5500. No SSH credentials needed.
Log in
Click Connect → Open dashboard. The controller's own login appears — enter your password and 6-digit TOTP code. (The Add-controller form only gets you to the box; this is the app's own login.)
06 After install
- Start on boot — run
pm2 startupand execute the command it prints, thenpm2 save. - Logs —
pm2 logs servermind. - Reconfigure —
cd ~/servermind && bun run setup(orbun run setup-authfor just the password/2FA). - Apply changes —
pm2 reload servermind. - Update — re-run the install command; it updates an existing install in place. (Your
.envis never overwritten.)
First login: open the UI, enter your password and the current 6-digit TOTP code. To make a change to your server, flip “Arm mutations” in the UI first — it clears itself after a single change (and auto-disarms after a few minutes either way), so each action you want is its own deliberate step.
07 Email reports & alerts
ServerMind can email you a daily health report and alert you the moment something needs attention — no extra service to run.
What you get
- Daily report at an hour you choose — CPU, memory, disk, uptime, every monitored service (✓/✗), PM2 processes, and an "all good / N issues" summary.
- Alerts when disk or memory crosses your threshold, a monitored service goes down, or a TLS certificate is near expiry — each with a cooldown so it never spams you.
Enable it
Two ways: the Settings panel in the dashboard (gear icon → Notifications & alerts — edits apply live, no restart), or the setup wizard's step 9:
$ cd ~/servermind && bun run setup
Pick how it sends mail:
- SMTP — Gmail or your own relay. For Gmail use host
smtp.gmail.com, port465, and a Google App Password (16 chars, not your login password — paste it without spaces). - Resend — an API key (free tier, simplest, best inbox delivery).
Set the daily-report hour and the disk/memory thresholds, then hit "Send a test email now?" to confirm it works (check spam the first time). TLS-cert alerts use your SERVERMIND_DOMAIN automatically.
08 Multiple servers (fleet)
A single server needs none of this — it works on its own, zero config. When you want to watch and manage several servers from one place, turn one instance into a controller and have the others connect to it as agents.
Agents dial out to the controller over one WebSocket, so you don't open any new inbound ports on them. Every server keeps enforcing its own read-only allowlist and arm switch — the controller can ask a server to do something, but it can't bypass that server's guardrails.
1 · Install the controller
The normal install is the controller — it manages its own box (a "fleet of one") and is ready to accept other servers, with the agent hub on and a join token generated for you. The Fleet tab appears in the dashboard.
$ curl -fsSL https://servermind.dev/install.sh | bash2 · Add each other server
In the controller dashboard, open Fleet → Add server — it prints the exact one-line command (with the join token filled in) to run on the new box. It's the same installer, in agent mode:
$ curl -fsSL https://servermind.dev/install.sh | bash -s -- \
--controller wss://<controller>/fleet/agent --token <token>The agent has no UI, auth, or AI — it just dials out to the controller, so no inbound ports are opened on it. Use wss:// when the controller is behind HTTPS (recommended — the join token travels in the connection). Plain ws:// to a remote host is refused by default; over a trusted private network add --insecure. The agent reconnects on its own if the controller restarts.
3 · See your fleet
Open the controller's dashboard and click Fleet — every connected server appears with live CPU, memory, disk, and its Redis / MySQL / PM2 health. "Manage" a server to chat with just that box; "Revoke" drops one. Reports and alerts from all of them arrive deduped in one inbox.
4 · Optional: a self-hosted WireGuard mesh
Add --mesh to both install commands to run the controller↔agent link over your own WireGuard network — no third-party coordination server (unlike Tailscale), every key generated on your machines.
$ # controller
$ curl -fsSL https://servermind.dev/install.sh | bash -s -- --mesh
$ # each agent (from Fleet → Add server)
$ curl -fsSL https://servermind.dev/install.sh | bash -s -- \
--controller wss://<controller>/fleet/agent --token <token> --meshThe controller installs WireGuard, opens UDP 51820, and writes a tightly-scoped sudoers rule so the unprivileged app can reload only its own WireGuard interface — no shell, no other command. Each agent generates its own keypair locally (its private key never leaves the box), enrolls by sending only its public key, brings up its tunnel, and then talks to the controller over the encrypted mesh.
--mesh it binds so agents can connect and opens UDP 51820 on the host firewall — but a cloud provider's security group / network ACL is separate, so allow UDP 51820 there too. The shared join token is used only for the first bootstrap hop; the per-agent WireGuard keys are individually revocable.09 Custom tools
The built-in tools cover the common cases. Custom tools let you teach the assistant about your stack — your database, your health endpoint, your one diagnostic command — without writing code or weakening the safety model. Add, edit, test and remove them from the dashboard's Tools tab (operator only, behind your login).
The core idea: you define and freeze the tool; the AI can only trigger it. The model never supplies a command, path, or URL — it just decides when to call what you already approved. The one exception is the DB console, where the AI writes a read-only query that's validated before it runs.
The five kinds
- Pinned command — one exact, frozen command (e.g.
redis-cli INFO memory), run with no shell so metacharacters mean nothing. Read-only by default; tick "changes things" to make it a mutation gated by the arm switch. - Read-only DB query — a frozen SQL query you write, for MySQL/MariaDB or PostgreSQL.
- DB console — the AI writes a read-only
SELECTat the moment you ask, against a database you configured. Ask "how many active orders?" and it composes the query, runs it, and answers. - HTTP health check —
GETa frozen URL; optionally assert a status code or a JSON field. - Read a file — tail a frozen path, confined to the same safe roots as the log reader (
/var/log, PM2 logs…).
Add one, step by step
- Open the dashboard, click Tools in the sidebar, then + Add tool.
- Give it a name (lowercase letters, numbers, underscores — e.g.
active_orders) and a one-line description. The description is what the AI reads to decide when to reach for the tool, so be specific — "count of currently-active orders", not just "orders". - Pick a type and fill its fields (below).
- Click Test to run it once and see the live output — fix anything before saving.
- Save tool. It's added to the assistant immediately, on both AI backends — no restart.
Fields for each type
Pinned command — enter the command one argument per line (it's run with no shell, so don't quote or chain). Tick "this changes things" only if it mutates state; that puts it behind the arm switch. Optional timeout (ms).
redis-cli
INFO
memoryRead-only DB query — choose the engine (MySQL/MariaDB or PostgreSQL), fill host, port, database, user, password, and the frozen query. Example: a MySQL tool on 127.0.0.1:3306, database appdb, user sm_ro, query:
SELECT COUNT(*) AS active_orders FROM orders WHERE status = 'active'DB console — the same connection fields, but no query: the AI writes a read-only SELECT each time you ask. Point it at your orders database and then ask "break down orders by status" or "who are our biggest customers?".
HTTP health check — a frozen url (GET), plus optional checks: an expected status code, and/or a JSON field path and its expected value (e.g. path status equals ok).
Read a file — a frozen absolute path under a safe root (/var/log/…, ~/.pm2/logs/…) and an optional line count.
Recommended: a read-only database user
For any DB tool, don't reuse an admin login — create a dedicated user that can only SELECT, scoped to just the database you want the assistant to see. That's the real boundary; the in-app read-only gate is a second layer on top.
-- MySQL / MariaDB
CREATE USER 'sm_ro'@'%' IDENTIFIED BY 'a-strong-password';
GRANT SELECT ON appdb.* TO 'sm_ro'@'%'; -- only the DB you want exposed
FLUSH PRIVILEGES;-- PostgreSQL
CREATE ROLE sm_ro LOGIN PASSWORD 'a-strong-password';
GRANT CONNECT ON DATABASE appdb TO sm_ro;
GRANT USAGE ON SCHEMA public TO sm_ro;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO sm_ro;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO sm_ro;Test, edit, remove
Every tool has a Test button that runs it once and shows the output (a DB console with no query you typed runs a SELECT 1 connectivity check). Each tool card has Edit and Delete. A pinned command marked "changes things" won't Test or run unless Mutations (top-right) are armed — the same gate as a service restart.
Use it — just ask
Once saved, the tool is in the assistant's toolbox. Go to Chat and ask in plain English; the AI matches your question to the tool's description and runs it:
- "How many active orders do we have?" → your
active_ordersquery - "Break down orders by status." → the AI writes that
SELECTthrough your DB console - "Is the API healthy?" → your HTTP health check
How the safety holds
Custom tools never loosen the guardrails — they're enforced the same way the built-ins are:
- Frozen by you. The command/query/URL/path lives in the tool's definition; the AI can't change it. A poisoned prompt can at most call a tool you already vetted.
- Read-only databases, two layers. Every DB query (frozen and console) passes a read-only gate — only
SELECT/SHOW/EXPLAIN-class statements, a single statement, and no file/exec vectors (INTO OUTFILE,LOAD_FILE,COPY … PROGRAM,pg_read_file…). For PostgreSQL the session also runs read-only, so the engine itself rejects any write — even a data-modifying CTE the text check can't catch. - Mutations stay gated. A pinned command marked "changes things" goes through the same server-side arm switch as a service restart — off by default.
- Secrets encrypted. A DB tool's password is encrypted at rest (AES-256-GCM) and masked in the dashboard.
On a fleet
Custom tools are agent-owned: each server defines its own tools and advertises only their names to the controller. When you Manage a server in the Fleet tab, the AI is offered that box's tools and the call runs on that box, re-validated locally. The controller can trigger a server's tools but can never define or push one — so it still can't make a box do anything its own config forbids.
10 Uninstall
One command removes ServerMind cleanly. It won't touch Bun, PM2, or git — you probably use those elsewhere.
$ curl -fsSL https://servermind.dev/uninstall.sh | bash
From a cloned repo you can run bun run uninstall instead. It stops and deletes the servermind PM2 process (and runs pm2 save so it stays gone after a reboot), then — after you confirm — removes the install directory, including its .env and logs.
.env — the password hash, 2FA secret, and AI key. Back it up first if you plan to reinstall with the same settings.Manual equivalent
$ pm2 delete servermind && pm2 save
$ rm -rf ~/servermind # contains .env + logs
$ pm2 unstartup # only if ServerMind was your boot-start app11 Security model
Security is enforced server-side, below the model — not by trusting the AI to behave.
- No shell. Every command runs as
argvagainst a read-only allowlist — norm, no writes, no network. Command injection is structurally impossible. File reads are confined to safe roots; secrets like/proc/*/environare blocked. - No database. Settings live in a JSON file, sessions in memory — there is no SQL/ORM, so SQL injection has nothing to attack.
- Mutations gated. Restart/stop/start are refused unless you arm them via a server-side switch the model can't set itself. The gate is re-checked on every call and an arm is consumed by a single change, so prompt injection in tool output can't silently flip the switch or chain extra changes.
- Auth. Password + TOTP 2FA, argon2id hashing, HttpOnly / SameSite=Strict cookies, brute-force lockout, rate limiting, strict CSP.
- Settings & secrets. The dashboard edits a safe subset (email, alerts, monitoring, AI) — never auth, the service allowlist, or PM2 access. Writes are schema-validated and atomic; secret fields are encrypted at rest (AES-256-GCM) and masked in the API, never returned to the browser.
- Yours. Runs entirely on your box; secrets stay in local files (chmod 600); nothing is sent to the project's authors.
claude CLI version on the host rather than auto-upgrading — the built-in tool deny-list is matched to known versions.12 Troubleshooting
I can't open the dashboard
It's bound to localhost by design. From your laptop, open an SSH tunnel (above) and visit http://localhost:5500 — don't expect to hit the server's public IP directly.
“auth not configured”
The password/2FA wasn't set. Run bun run setup-auth on the server, then pm2 reload servermind.
Claude Code: nothing happens / “refuses to run as root”
Install Claude Code and run claude login as the same user that runs ServerMind, and don't run that user as root. Confirm the binary path with which claude and set CLAUDE_BIN if needed.
The AI connection test failed
Double-check AI_BASE_URL, AI_MODEL, and AI_API_KEY in .env. If your key contains a $, the env loader can mangle it — regenerate one without $.
The dashboard shows no PM2 processes
PM2 is per-user. If your apps run under a different user (e.g. root), set PM2_COMMAND="sudo -n /root/.bun/bin/pm2" and grant a passwordless sudo rule — the wizard prints the exact line.
Login works locally but not behind Cloudflare/HTTPS
Set SECURE_COOKIES=1 so the session cookie is marked Secure, and make sure your proxy passes X-Forwarded-Proto. Also set TRUST_PROXY=1 so per-client rate limiting and lockout read the real visitor IP from the proxy rather than treating every request as one client. For streaming chat, disable response buffering (flush_interval -1 in Caddy, proxy_buffering off in Nginx).
Test email fails with "SMTP 535"
535 means Gmail rejected the login. Use a Google App Password (16 chars), not your normal password, and paste it without spaces (abcd efgh ijkl mnop → abcdefghijklmnop). Confirm 2-Step Verification is on, host is smtp.gmail.com, and port is 465.
An agent won't show up in the Fleet
Check the agent's logs (pm2 logs servermind-agent). Most often the FLEET_JOIN_TOKEN doesn't match the controller's, or SERVERMIND_CONTROLLER is wrong — it must be the full WebSocket URL ending in /fleet/agent and reachable from the agent (try curl-ing the controller host). Use wss:// if the controller is behind HTTPS; a plain ws:// URL to a remote host exits immediately unless you set FLEET_ALLOW_INSECURE=1. A closed login is 1008 unauthorized (token mismatch).
Still stuck? Open an issue on GitHub.