Setup guide

Install ServerMind on your server.

From a fresh VPS to chatting with your stack in about two minutes — including what you need, how to choose your AI, and how to reach it without a domain.

01 What you need

ServerMind is deliberately light. If you have a Linux box and can paste a command into it, you're set.

A Linux VPS
Ubuntu, Debian, Fedora, Alpine — any modern distro with systemd.
Shell access
SSH in as a normal user. Root works for the API backends, but not for Claude Code.
An AI key
A free Gemini key, a Claude subscription, or any OpenAI-compatible key. Pick during setup.
A little RAM
Runs comfortably on a small VPS. The installer adds Bun + PM2 automatically.

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
TipIf you'll use the Claude Code backend, install as a normal user — Claude Code refuses to run as root. The Gemini / OpenAI-compatible backends are fine either way.

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-code

Other OpenAI-compatible APIs

  • Groqhttps://api.groq.com/openai/v1 (free, very fast)
  • OpenRouterhttps://openrouter.ai/api/v1
  • Ollamahttp://127.0.0.1:11434/v1 (local, no key, nothing leaves the box)
PrivacyWith a cloud model, your prompts and the command output it needs go to that provider under your account. Want nothing to leave the server at all? Use local Ollama.

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_HOST to 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
Why this is fineOver an SSH tunnel or tailnet, plain HTTP is safe — your password and TOTP code never cross the open internet.

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.

Don'tDon't set 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

  • macOSServerMind.dmg (universal: Apple Silicon + Intel). First launch: right-click the app → Open (unsigned beta).
  • WindowsServerMind-Setup.exe (Windows 10/11). First launch: More info → Run anyway.
Install the controller firstThe app connects to a running ServerMind controller — install it on your server (steps 02–03 above) before you add it here.

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. root or ubuntu.
  • AuthenticationPrivate 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 port5500.

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 ConnectOpen 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.)

Why it's safeOne outbound SSH tunnel — nothing new is exposed to the internet, the controller stays bound to localhost, and the desktop app never bypasses your password + 2FA.

06 After install

  • Start on boot — run pm2 startup and execute the command it prints, then pm2 save.
  • Logspm2 logs servermind.
  • Reconfigurecd ~/servermind && bun run setup (or bun run setup-auth for just the password/2FA).
  • Apply changespm2 reload servermind.
  • Update — re-run the install command; it updates an existing install in place. (Your .env is 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, port 465, 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.

No mail server neededServerMind relays through your provider — it does not run its own mail server. That keeps deliverability high (VPS IPs are usually blacklisted) and the attack surface small.

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 | bash

2 · 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> --mesh

The 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.

Fleet-wide chatAsking one question across all servers ("which servers are unhealthy?") needs an OpenAI-compatible AI backend on the controller (Gemini, Groq, xAI, OpenRouter, Ollama…). The Claude Code backend runs tools locally only, so it can't drive remote boxes.
Reaching the controllerThe controller needs a reachable address; agents do not. With --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 SELECT at 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 checkGET a 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

  1. Open the dashboard, click Tools in the sidebar, then + Add tool.
  2. 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".
  3. Pick a type and fill its fields (below).
  4. Click Test to run it once and see the live output — fix anything before saving.
  5. 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
memory

Read-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_orders query
  • "Break down orders by status." → the AI writes that SELECT through 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.
The real DB boundary is your database userA DB console lets the AI read anything its database user can read. That's the point — and the safe way to use it is to give the tool a read-only user scoped to just the data you want exposed, not an admin login. The read-only gate is the app-layer safety net on top of that.
PrivacyA DB console's results are sent to your configured AI provider so it can answer in plain English. Use a local Ollama backend if you want zero data egress.

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.

Heads upDeleting the install directory removes your .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 app

11 Security model

Security is enforced server-side, below the model — not by trusting the AI to behave.

  • No shell. Every command runs as argv against a read-only allowlist — no rm, no writes, no network. Command injection is structurally impossible. File reads are confined to safe roots; secrets like /proc/*/environ are 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.
Full threat modelAn honest breakdown of what's structurally prevented and the real residual risks (running as root, encryption-at-rest limits, dependencies) lives in SECURITY.md.
Maintainer noteIf you use the Claude Code backend, pin the 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 mnopabcdefghijklmnop). 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.

Ready in two minutes.

Self-hosted, open-source, and free with Gemini. No domain required.

$ curl -fsSL https://servermind.dev/install.sh | bash
← Back to overview