MuninMunin
Sign inStart free
Home/Journal/Self-host the whole thing on one machine.
Engineering · 9 min read

Self-host the whole thing on one machine.

Three containers, one command, no seat count. What `docker compose up` actually starts, the four secrets to set before anyone else can reach it, what stays in stub mode until you wire it — and how the footprint compares to self-hosting Chatwoot or EspoCRM.

A small plain matte grey box the size of a hardback book sitting alone on a pale oak kitchen island in a bright modern home, one braided cable running off the counter edge, a person softly out of focus at the far counter — the whole platform, running on one machine in your own house.
The whole thing, on a machine you own.

The install is not the hard part, and hasn't been for years. Clone the repo, copy the example env file, run one command, and three containers come up on one host. The hard part is the fortnight after: the placeholder secrets you never replaced, the mailer that is still a stub, the Postgres role that decides whether row-level security is doing anything at all. This is the pass that covers those, in the order the machine forces.

Can you self-host a CRM and helpdesk on one machine?

Yes. Munin self-hosts as three containers — Postgres with pgvector, a backend on port 3001, a dashboard on port 3000 — brought up by a single docker compose up against the MIT-licensed repository. There is no seat count, no contact cap, and no feature gate. The self-hosted build is the same code that runs Munin Cloud, composed with single-tenant auth instead of multi-tenant.

That last point is the one worth sitting with, because it is unusual in this category. There is no enterprise/ or ee/ directory in the repo holding back the parts you actually want. MIT covers all of it — Knowledge Base, Conversations, CRM, CMS, Outreach, Analytics, the curator queue, the audit log, webhooks, and all 204 MCP tools.

bashInstall
git clone https://github.com/getmunin/munin.git
cd munin
cp .env.example .env
docker compose up

What does docker compose up actually start?

Three services and two named volumes. Postgres 16 with the vector extension, the backend, and the web dashboard. No Redis, no separate queue process, no search cluster — the curator job queue and the hybrid BM25 + pgvector search both live in the same Postgres the rest of the data does.

The backend container runs node dist/migrate.js before node dist/main.js, so migrations apply on every boot. Postgres reports healthy before the backend is allowed to start.

  • PGpgvector/pgvector:pg16 on the munin-pg volume. The only stateful service in the stack, and the only one you need a backup plan for.
  • APIThe backend on :3001 — MCP over Streamable HTTP, OAuth 2.1 with dynamic client registration, and the 218-endpoint REST control plane. Assets land on the munin-data volume.
  • WEBThe dashboard on :3000 — sign-in, API keys, review queues, connection cards. Not where the work happens. The agent is where the work happens.

Which secrets must I set before anyone else can reach it?

Four: MUNIN_AUTH_SECRET, MUNIN_KEY_PEPPER, MUNIN_ENCRYPTION_KEY, and MUNIN_STORAGE_LOCAL_SECRET. Left at their replace-me-* placeholders, docker compose up generates strong values on first boot and persists them in the munin-data volume. That is fine on a laptop. For anything shared, set them yourself so they survive a volume you decide to throw away.

Two more govern who gets in at all. MUNIN_ALLOWED_EMAIL_DOMAINS is empty by default, which means invite-only: the first person to sign up becomes admin of the singleton org, and everyone after them needs an invitation token. MUNIN_AUTH_TRUSTED_ORIGINS defaults to localhost:3000 and must be set to your real dashboard URL in production, or sign-in fails CSRF checks.

bash.env
# One line each, at install time.
openssl rand -base64 48   # MUNIN_AUTH_SECRET
openssl rand -base64 48   # MUNIN_KEY_PEPPER
openssl rand -base64 48   # MUNIN_ENCRYPTION_KEY
openssl rand -base64 48   # MUNIN_STORAGE_LOCAL_SECRET

# Who can reach it
MUNIN_ALLOWED_EMAIL_DOMAINS=acme.io
MUNIN_AUTH_TRUSTED_ORIGINS=https://munin.acme.io

# What it calls itself, everywhere it has to name itself
NEXT_PUBLIC_MCP_URL=https://munin.acme.io/mcp
NEXT_PUBLIC_AUTH_URL=https://munin.acme.io
MUNIN_API_URL=https://munin.acme.io

Set the encryption key before you store a single secret

MUNIN_ENCRYPTION_KEY is wired into pgcrypto through a per-request transaction GUC and encrypts every stored credential — LLM provider keys, IMAP and SMTP passwords. Secrets written before it is set cannot be recovered. MUNIN_KEY_PEPPER is the same shape of decision for API-key hashes: rotate it and every key you have issued stops working. Both are one openssl rand -base64 48 at install time, and then you never think about them again.

Why are there two database URLs?

Because Postgres superusers bypass row-level security, regardless of FORCE. MUNIN_MIGRATE_URL is the privileged connection that runs CREATE EXTENSION, applies DDL, and creates the restricted munin_app role. DATABASE_URL is the one the application actually uses, and it points at munin_app.

If you collapse those two into one superuser URL because it is quicker, the RLS policies are still there and still compile, and they stop applying. Tenancy in Munin is carried by the database rather than by application code checking an orgId on the way past, so that one shortcut is the difference between isolation you can point at and isolation you are hoping for.

What is still a stub after the first boot?

Three things, deliberately. Transactional email defaults to an in-memory stub, embeddings default to a deterministic local stub, and asset storage defaults to the local disk. Nothing in a fresh install reaches a third-party API, which means you can evaluate the whole platform on a laptop without creating a single vendor account first.

Each one is a couple of lines when you want the real thing.

  • Transactional emailMUNIN_MAIL_PROVIDER=stub until you set it to resend and supply RESEND_API_KEY. Verification, reset, and invite links are built against MUNIN_WEB_URL, so set that to your real dashboard host at the same time.
  • EmbeddingsA deterministic stub until OPENAI_API_KEY is set. OPENAI_BASE_URL points at anything OpenAI-compatible — LM Studio, vLLM, or llama.cpp on the same box if you want the vectors to stay in the building, or a hosted EU provider if you don't.
  • Vector dimensionMUNIN_EMBEDDING_DIMENSIONS defaults to 1536 and matches the shipped migrations. There is no live ALTER path for it in the open-source migrations, so choose the embedding model before you load documents rather than after.
  • Asset storagelocal writes to /var/munin/assets on the munin-data volume and serves through the backend. Switch MUNIN_STORAGE_PROVIDER to s3 for any S3-compatible service — Scaleway, R2, MinIO, AWS — via SigV4 presigned URLs.
3
containers: Postgres, backend, dashboard
204
MCP tools on the same `/mcp` endpoint as Cloud
MIT
no enterprise directory, no open-core tier

How do I point Claude, Cursor, or ChatGPT at a self-hosted install?

At one URL: NEXT_PUBLIC_MCP_URL. That value is what Munin advertises through RFC 9728 protected-resource metadata, what the dashboard's connect snippets display, and what every MCP client is handed. Set it once to the address clients can actually reach, and the OAuth flow does the rest.

The local-development shape is http://localhost:3001/mcp. The production shape is your own hostname behind TLS. Nothing else in the client config changes — this is the same wiring as running your CRM from Claude Code against Cloud, with the hostname swapped.

bashConnect
# Claude Code
claude mcp add munin-local http://localhost:3001/mcp

# Browse the tool surface interactively
npx @modelcontextprotocol/inspector
# URL = http://localhost:3001/mcp   Auth = Bearer mn_admin_...

# Sanity-check the transport by hand
curl -N -X POST http://localhost:3001/mcp \
  -H "Authorization: Bearer mn_admin_..." \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

What does self-hosting cost that Munin Cloud doesn't?

Operational work that has nothing to do with customers. Backups of the munin-pg volume, TLS termination in front of port 3001, upgrades on your own schedule, and whoever answers when Postgres fills the disk at three in the morning. Munin does not ship a managed backup service; that job is yours, and it is a real job.

What you get for it is the version of ownership that survives a pricing page. No caps on contacts, calls, or storage, because there is no meter in the code to enforce them. Data residency wherever you put the machine, decided by you rather than by a sub-processor list. And an audit log, a review queue, and 204 MCP tools that behave identically to the hosted build, because they are the same build. Every module also ships symmetric export and import tools, so moving between your metal and Cloud is a scripted operation in either direction rather than a negotiation — which is the entire point of the licence.

The licence is identical either way. The pager is the difference.

How does the footprint compare to self-hosting Chatwoot or EspoCRM?

All three run comfortably on one host. The difference is how many moving parts and how much of the customer they each hold.

Chatwoot is the most complete self-hosted inbox in the field, and it is not close on channel breadth: WhatsApp, Instagram, Facebook, and Telegram all have first-class inboxes. If your support lives on WhatsApp, install Chatwoot and stop reading here — Munin's four channels are email, chat widget, SMS, and voice, and no amount of architecture makes up for a channel your customers are already using. Chatwoot's deployment adds a Rails web process, a Sidekiq worker, Postgres, and Redis.

EspoCRM is the lightest of the three and the most conventional — a mature CRM that will run on a 1 GB box, with a job daemon, an optional WebSocket server, and MariaDB alongside it. It is AGPLv3 with a commercial licence sold separately, which matters if you plan to modify it and expose it as a service.

Munin is three containers because there is one datastore and one customer underneath all six modules. A support thread, a deal, a knowledge article, a published post, and an outreach draft touching the same person are rows that already know about each other — no sync job, no reconciliation, no second system of record quietly disagreeing with the first. That is what makes the tool surface worth pointing an agent at: the agent reads a conversation and updates a contact and cites a document in one session, because there is nothing between those three things. If you want the fuller ranking on either side, we have written both — the open-source CRM field and the open-source helpdesk field, licences read properly in both.

Who should self-host, and who should not?

Self-host if you have a data-residency obligation that names a jurisdiction or a machine, if you already run Postgres for something else and the on-call rota exists, or if you intend to modify the code — MIT means you can fork it, ship it, and never tell us.

Don't self-host if you are one person. Backups, upgrades, and the three-in-the-morning page are a second job, and a founder already has one. Cloud Free is €0 a month with all six modules and all four channels unlocked, capped at 5,000 MCP calls, 250 contacts, and 100 MB — enough to find out in an afternoon whether the shape suits you, on exactly the code in this article. Move to your own machine later if you want to; the export tools are in the repo, and they work in both directions.

Frequently asked questions

What are the system requirements to self-host Munin? A Linux host with Docker and Docker Compose, and enough disk for Postgres and your CMS assets. The stack is three containers — Postgres 16 with pgvector, the backend, and the dashboard — with no Redis, queue broker, or search cluster to provision alongside them.

Is self-hosted Munin the same as Munin Cloud? Yes, same code. The open-source build composes the shared modules with single-tenant auth; Cloud composes the identical modules with multi-tenant auth. All 204 MCP tools, 218 REST endpoints, and 45 bundled skills are present in both.

Is Munin really MIT, or is it open core? MIT, with no enterprise directory in the repository and no feature held back for a paid tier. That is a deliberate difference from most of this category, where the front-page badge says open source and the useful half lives under a separate licence.

Do I need an OpenAI key to run it? No. Embeddings fall back to a deterministic local stub, so search works out of the box. Set OPENAI_BASE_URL to a local server — LM Studio, vLLM, llama.cpp — if you want real vectors without anything leaving the host.

How do I connect Claude or Cursor to a self-hosted instance? Set NEXT_PUBLIC_MCP_URL to the address your clients can reach, then add that URL to the client. Munin advertises it through RFC 9728, and the first call triggers the OAuth consent screen in your browser.

Can I move to Munin Cloud later, or back again? Yes, in either direction. Every module ships symmetric *_export and *_import tools over both MCP and REST, and the bundled playbooks/data-migration skill sequences them in foreign-key order so dependent records resolve their parents on the target.

The short version

  • Munin self-hosts as three containers — Postgres with pgvector, backend on :3001, dashboard on :3000 — from one docker compose up.
  • Set MUNIN_AUTH_SECRET, MUNIN_KEY_PEPPER, MUNIN_ENCRYPTION_KEY, and MUNIN_STORAGE_LOCAL_SECRET yourself for anything shared. The encryption key must be set before you store a credential.
  • Keep MUNIN_MIGRATE_URL and DATABASE_URL separate. The app connects as the restricted munin_app role so row-level security actually applies.
  • Mail, embeddings, and asset storage ship as stubs so a fresh install reaches no third-party API. Each is a couple of lines to wire up.
  • MIT with no enterprise directory: all six modules, 204 MCP tools, and the export tools that let you move between your machine and Cloud in either direction.

The repository is on GitHub under MIT, and if you would rather not run Postgres yourself, Munin Cloud Free is the same code without the pager.

If it doesn't run on one machine you own, it isn't self-hosted. It's just hosted.