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

Self-host the whole thing on one machine.

Which self-hosted CRM and help desk to run, ranked by how many moving parts you have to own — Munin, Twenty, EspoCRM, Chatwoot, Zammad, FreeScout, ERPNext, Odoo, Dolibarr. Then the Munin runbook: three containers, one command, the four secrets to set, and what stays a stub until you wire it.

Overhead on brushed pale birch ply: three identical plain matte-white blocks standing together with nothing joining them, beside nine smaller blocks tangled to each other by short olive cords — three things to keep alive, or a dozen.
The count that decides your Saturdays.

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 help desk 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 the whole MCP tool catalogue.

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

What is the best self-hosted CRM and help desk in 2026?

There is no single winner, and the honest way to split the field is by how many moving parts you are willing to own. FreeScout and EspoCRM are the lightest — both run on ordinary PHP hosting. Twenty is the best-looking self-hosted CRM. Chatwoot has the widest channel coverage. Zammad is the deepest ticketing system. ERPNext is the most complete free ERP. Munin puts CRM and help desk on one record from one command.

That framing matters more than a feature grid, because on your own metal the feature you never use costs nothing and the service you have to keep alive costs every week. Zammad wants PostgreSQL, Redis, a Rails app, and — strongly recommended, if you want its search to be good — Elasticsearch, which brings heap sizing and index management with it. ERPNext runs Gunicorn, a worker pool, Node, MariaDB, and three separate Redis instances for cache, queue, and socketio. Both are excellent software and both are asking you to operate a small platform team's worth of infrastructure.

The second axis is the licence, and it does not sort the way the badges suggest. Chatwoot is MIT at the root with an enterprise/ directory under a separate commercial licence — SLA management, audit logs, and custom dashboards live in it, and production use of that folder needs a subscription. Twenty is AGPL-3.0 with individual files marked @license Enterprise. FreeScout is AGPL-3.0 with official modules — Knowledge Base, Workflows, Customer Portal — sold per licence key. None of that is dishonest; all of it is normal. It just means the thing you can self-host is not always the thing you demoed.

ProjectCoversServices to runDatastoresLicenceHeld back
MuninCRM, help desk, KB, CMS, outreach, analytics3 containersPostgres 16 + pgvectorMITNothing
TwentyCRMServer, worker, database, RedisPostgres + RedisAGPL-3.0 coreFiles marked @license Enterprise: SSO, row-level permissions, audit logs
EspoCRMCRM, cases, portal, KBApp, job daemon, optional WebSocketMariaDB, MySQL or Postgres 15+AGPL-3.0Advanced Pack licensed separately, per installation
ChatwootHelp desk, widest channel coverageRails web, Sidekiq worker, database, RedisPostgres + RedisMIT coreenterprise/ directory under a separate commercial licence
ZammadTicketing, deepest workflow engineRails app, database, Redis, ElasticsearchPostgres + Redis + ElasticsearchAGPL-3.0Nothing
FreeScoutShared-mailbox help deskPHP app and cronMySQL, MariaDB or PostgresAGPL-3.0Official modules sold per licence key
ERPNextFull ERPGunicorn, worker pool, Node, database, three Redis instancesMariaDB + Redis ×3GPL-3.0Nothing
OdooERP suiteApp and databasePostgresLGPL-3.0 (Community)Enterprise edition, in a private repository
DolibarrERP and CRM, ~100 modulesPHP appMySQL, MariaDB or PostgresGPL-3.0-or-laterNothing

Re-checked against each project's own deployment and licence documentation on 3 September 2026. Packaging moves; check the repository before you commit, including ours.

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 full-text + 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 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.

How much RAM does a self-hosted CRM need?

Between 1 GB and 8 GB, depending entirely on how many services the project makes you run. FreeScout runs on the cheapest VPS a provider sells. EspoCRM is comfortable on 1 GB. Munin wants about 2 GB for Postgres plus two Node processes. Twenty asks for at least 2 GB. Zammad recommends at least 4 GB for the containers, more if Elasticsearch shares the box. ERPNext starts at 4 GB for a small production instance and 8 GB by 25 to 50 users.

The failure mode is always the same and it is worth naming: undersizing memory does not produce an error, it produces swap. A page that took 200 ms takes four seconds, background jobs stall quietly, and you spend a Saturday convinced the application is broken. Buy the next VPS size up. It is the cheapest decision on this page.

Disk is the easier half — the database, uploaded assets, and room to hold a dump while you restore it. Twenty gigabytes is a sane starting point for any of these, and Postgres will tell you long before it matters.

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 127.0.0.1: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.

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"}'

How do I back up a self-hosted CRM?

One database and one asset volume, on a schedule, restored somewhere else at least once. For Munin that is a pg_dump of the munin-pg volume and a copy of munin-data; every other project on this page is the same two objects with different names. There is no managed backup service in any self-hosted build, including ours. That job is yours.

The part people skip is the restore. A backup you have never restored is a file, not a backup, and the failure is always discovered on the worst possible morning. Restore into a scratch container once, confirm the row counts, and put a reminder in the calendar for six months' time.

bashBackup and verify
# Dump the database
docker compose exec -T postgres \
  pg_dump -U munin -Fc munin > munin-$(date +%F).dump

# Copy the asset volume
docker run --rm -v munin-data:/data -v "$PWD":/backup alpine \
  tar czf /backup/munin-data-$(date +%F).tar.gz -C /data .

# Restore into a scratch database and count the rows
docker compose exec -T postgres createdb -U munin munin_restore_test
docker compose exec -T postgres \
  pg_restore -U munin -d munin_restore_test < munin-$(date +%F).dump

What breaks when you upgrade?

Usually nothing, and the exception is always Postgres. Munin applies its own migrations on every boot, so pulling a new image and restarting is the whole upgrade path for the application. A Postgres major version change is different: the data directory format changes, the container will refuse to start against the old volume, and you need a dump-and-reload. That is true of every project here that ships a database container, which is most of them.

Two habits make this uneventful. Pin the image tags rather than tracking latest, so a restart at two in the morning is not also a version bump. And take the dump above immediately before any upgrade, not on the usual schedule — the five minutes it costs is the difference between a rollback and an incident.

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 more than 200 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 Chatwoot, Zammad, or EspoCRM?

All of them run on one host. The difference is how many services you keep alive and how much of the customer each one holds.

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 already lives on WhatsApp, install Chatwoot — Munin's four channels are email, chat widget, SMS, and voice, and that is the buyer Chatwoot should win. Its deployment adds a Rails web process, a Sidekiq worker, Postgres, and Redis, and its enterprise/ directory sits under a separate commercial licence.

Zammad is the deepest ticketing engine here and holds nothing back under AGPL-3.0. It also wants Elasticsearch next to Postgres and Redis for search worth having, which is a fourth service with its own memory profile and its own opinions.

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 AGPL-3.0 with a commercial licence sold separately, and its Advanced Pack is a paid extension licensed per installation.

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 help-desk field, licences read properly in both.

Can you self-host a CRM for free?

The licence, yes. The machine, no. Every project in the table above costs nothing to install, and Munin, Zammad, ERPNext, and Dolibarr hold nothing back behind a paid tier at all. What you pay instead is a VPS — realistically €5 to €20 a month for one of these — plus the hours nobody puts in the spreadsheet.

An analysis by Open Source Alternatives costed a self-hosted CRM at roughly $4,500 to $10,800 over three years, most of it engineering time at $100 an hour and two to five hours a month of maintenance, against $86,000 to $132,000 for Salesforce Enterprise over the same window. Treat both as indicative rather than quotes. The gap is still the reason this whole category exists, and the licence-by-licence comparison is where the free half gets read properly.

The honest version of the trade: self-hosting converts a subscription into a responsibility. If somebody on your team is happy owning a Postgres, that is a good trade and the software is genuinely free. If nobody is, a hosted plan is cheaper than the outage.

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 is the best self-hosted CRM? For a sales pipeline, Twenty. For breadth with nothing gated, Dolibarr or ERPNext. For the lightest possible box, EspoCRM. For CRM and help desk on one customer record under a permissive licence, Munin — three containers, MIT throughout, one docker compose up. Your real selection criterion is which one you will still be patching in six months.

What is the best self-hosted help desk? Chatwoot if you need WhatsApp, Instagram, and Telegram inboxes. Zammad if you need the deepest ticketing and workflow engine and can operate Elasticsearch. FreeScout if you want a shared mailbox on cheap PHP hosting. Munin if the support thread and the CRM record should be the same customer rather than two systems syncing.

What are the system requirements to self-host Munin? A Linux host with Docker and Docker Compose, around 2 GB of RAM, and enough disk for Postgres and your CMS assets — 20 GB is a sane start. The stack is three containers, with no Redis, queue broker, or search cluster to provision alongside them.

How much RAM does a self-hosted CRM need? One to eight gigabytes, set by how many services the project runs rather than by how many contacts you have. EspoCRM and FreeScout are happy on 1 GB. Munin wants about 2 GB. Twenty asks for at least 2 GB. Zammad recommends at least 4 GB, more if Elasticsearch shares the host. ERPNext starts at 4 GB and wants 8 GB by 25 to 50 users.

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. The whole catalogue — 200+ MCP tools, the REST API, and around 50 bundled skills — is 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 — sometimes in a directory, sometimes in a licence header on individual files, sometimes in a repository you cannot see. The full licence-by-licence comparison is here.

How do I back up a self-hosted install? A pg_dump of the database and a copy of the asset volume, on a schedule. Then restore it into a scratch database once and check the row counts, because an unrestored backup is a file rather than a backup.

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 MCP, 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

  • Pick a self-hosted CRM by how many services you are willing to keep alive. Zammad wants Postgres, Redis and Elasticsearch. ERPNext runs three separate Redis instances. Munin is three containers and one Postgres.
  • Read where the commercial code sits, not the badge. Chatwoot is MIT with an enterprise/ directory under a separate licence, Twenty marks individual files @license Enterprise, FreeScout sells official modules per licence key. Zammad, ERPNext, Dolibarr and Munin hold nothing back.
  • Budget 1 to 8 GB of RAM depending on the project — EspoCRM and FreeScout on 1 GB, Munin around 2 GB, Twenty at least 2 GB, Zammad at least 4 GB plus Elasticsearch, ERPNext 4 GB rising to 8 GB by 50 users. Undersizing shows up as swap, not as an error.
  • 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.
  • Back up the database and the asset volume, and restore one into a scratch database before you need to. Application upgrades apply their own migrations; a Postgres major version is the one that needs a dump and reload.
  • MIT with no enterprise directory: all six modules, 200+ MCP tools, a REST API, around 50 bundled skills, and export tools that move you 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.

Kjell Rune Monsø, founder.