MuninMunin
Sign inStart free
Home/Journal/Self-host the whole thing on one machine.
Engineering · 20 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, SuiteCRM, Chatwoot, Zammad, FreeScout, ERPNext, Odoo, Dolibarr. Then the Munin runbook: three containers, one command, the four secrets to set, the reverse proxy in front, how to run it with no port open at all, what stays on the machine, and how many clients one install holds.

Three identical empty warm-grey ceramic pots in a row on a white-painted counter in a bright modern utility room, with one matte cobalt blue watering can standing beside them — three containers, and the upkeep that comes with owning them.
Three containers. The watering is the part nobody demos.

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. SuiteCRM is the most customisable for enterprise sales processes. 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. SuiteCRM sits at the other end: an Apache and PHP application with a cron job and one MySQL or MariaDB behind it, which is why it still turns up on shared hosting two decades in. All three are excellent software; two of them 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. SuiteCRM, Zammad, ERPNext and Dolibarr hold nothing back at all. 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
SuiteCRMCRM, marketing, service, casesApache/PHP app and cronMySQL 5.7/8.0 or MariaDBAGPL-3.0Nothing
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; the SuiteCRM row was added on 16 September 2026. Packaging moves; check the repository before you commit, including ours.

Check who publishes the image, not just who publishes the code

On 28 August 2025 Bitnami moved its existing container catalogue to docker.io/bitnamilegacy, which its own notice says receives no further updates or security patches. For several projects here that repository was the most-pulled image on Docker Hub. Before you pin a tag, check whether the image comes from the project, from a vendor, or from a volunteer — it is a different maintenance promise from the one the licence made.

How do I tell whether a self-hosted project is still maintained?

Read the release history, not the star count. Stars measure how many people liked the idea once; releases measure whether anyone is still fixing it. Check three things on the repository: the date of the most recent tagged release, whether the default branch has commits from this month, and whether the newest tag is marked pre-release rather than stable.

That third check is the one that catches people, because a project can look current and have a two-year-old stable line. The releases below were read from each project's own repository on 22 September 2026, and most of this field is in good health — which is worth saying plainly, because "abandoned open source" is a lazy objection that mostly isn't true any more.

  • Shipping this monthERPNext v16.35.0 on 15 September, Twenty v2.41.0 on 17 September, Chatwoot v4.18.0 on 18 September, FreeScout 1.8.241 on 19 September, Munin v5.34.0 on 21 September. Odoo tags rather than publishes releases, and its default branch had commits the same week.
  • Shipping this quarterEspoCRM 10.0.8 on 8 September and Dolibarr 24.0.1 on 7 September. Zammad publishes its releases outside GitHub's release feed; its default branch had commits on 21 September.
  • Slower, and worth a look before you commitSuiteCRM's 7.x line last tagged 7.15.2 on 31 July 2026. Note that 8.x is developed in a separate repository, so a quiet 7.x tree is not the whole picture — check both before concluding anything.
  • The one to actually checkMonica's latest stable release is v4.1.2, from 4 May 2024. The newest tag is a v5 pre-release from April 2025, while the project's own site markets the rewrite as v3. Alive and mid-rewrite rather than abandoned, but three version numbers is a real thing to know about software you are about to put your address book into.

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.

How do I put HTTPS in front of a self-hosted CRM?

With a reverse proxy, and the shape is identical for every project on this page: terminate TLS at the proxy, forward to the application's port on localhost, and let the proxy renew the certificate. Caddy does it in three lines with automatic certificates. nginx does it in about thirty, plus certbot. Neither is the hard part. Forgetting it is.

Munin publishes two ports, so the proxy gets two site blocks — the dashboard on :3000 and the backend on :3001. Whichever hostnames you choose, the three environment values from the block above have to name them exactly: MUNIN_AUTH_TRUSTED_ORIGINS is the dashboard's HTTPS origin or sign-in fails its CSRF check, and NEXT_PUBLIC_MCP_URL is the URL every MCP client will be handed, so it must be the address those clients can actually reach rather than the one you typed on the host.

caddyfile/etc/caddy/Caddyfile
munin.acme.io {
	reverse_proxy localhost:3000
}

api.munin.acme.io {
	reverse_proxy localhost:3001
}

# Then, in .env, the hostnames must agree:
#   MUNIN_AUTH_TRUSTED_ORIGINS=https://munin.acme.io
#   MUNIN_WEB_URL=https://munin.acme.io
#   MUNIN_API_URL=https://api.munin.acme.io
#   NEXT_PUBLIC_MCP_URL=https://api.munin.acme.io/mcp

Can I self-host a CRM without opening a port to the internet?

Yes, and for a customer database it is often the better answer. Two shapes work. An outbound-only tunnel — Cloudflare Tunnel, ngrok — runs a daemon on your host that dials out and publishes a hostname, so no inbound port is open and no public IP is needed. A private overlay network — Tailscale, WireGuard — puts the instance on a network only your own devices can reach, and publishes nothing at all.

The trade between them is worth thinking through before you pick, because it is the same trade as the rest of this article. A tunnel is easier and adds a company to your data path, which is exactly the thing you self-hosted to avoid; if the reason you are reading this is a jurisdiction, read that vendor's terms before you route your customers through it. An overlay adds nobody, and costs you reach: a tailnet address is only resolvable from inside the tailnet, so an MCP client running on your own laptop connects fine while any client that dials out from a vendor's servers cannot see it at all.

Check each tool's own documentation for the current commands rather than a copy of them here — both projects have changed their CLI more than once, and a wrong flag in a networking tool fails in the least obvious way.

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.

Can one self-hosted install serve several clients or companies?

One install serves one organisation. The open-source build is single-tenant and invite-only: the first person to sign up becomes admin of that org, and everyone after them needs an invitation token or an allowed email domain. For several client organisations you run one stack per client, or you use Munin Cloud, which is the multi-tenant build of the same code.

A stack per client is a lighter sentence than it sounds, because a stack here is three containers and a Postgres rather than a platform. Ten clients is ten compose files behind one reverse proxy, each with its own volume, its own pg_dump, its own upgrade window, and its own MCP URL to hand that client's agent. That is also the isolation story you want to be able to say out loud when a client asks where their data is: not a filter, a machine. The boundary inside the schema is doing its work regardless — the row-level security policies are in the migrations and the application connects as the restricted munin_app role — and a separate deployment adds a separate blast radius on top of it.

Worth knowing that this is where the whole category sits, rather than where we sit alone. EspoCRM's own forum is direct that multiple companies are not supported and separate installations are the answer. Odoo has two partial answers rather than one clean one: multi-company inside a single database, where the isolation is only as good as the record rules, or one Odoo process serving several databases through dbfilter, where one tenant's slow query is still everyone's slow query. Across this field, "a tenant" usually resolves to "a deployment" — which makes the per-deployment footprint in the table above the number that decides what ten clients cost you. The agency version of this question works through what the alternative costs once you sign the tenth.

  • MUMunin — one org per install, invite-only. Several clients means several stacks: three containers each, one reverse proxy in front, one backup each. Munin Cloud is the multi-tenant build if you would rather not own that.
  • ESEspoCRM — separate installations per company. Several instances on one host under separate subdomains is the pattern its community documents; cookies are host-only, so the sessions stay apart.
  • ODOdoo — multi-company inside one database, isolated by record rules, or one process across several databases via dbfilter. Two different trades, both sharing a failure domain.
  • RLSWhichever you pick, ask where the boundary is actually enforced — in the database, or in application code that has to remember. That answer outlives any feature list.

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.

What is the best self-hosted private CRM?

A self-hosted CRM is only private if its defaults are. Munin, Zammad, SuiteCRM, ERPNext and Dolibarr all reach no third-party API on a fresh install, and EspoCRM and FreeScout are the same on ordinary PHP hosting. So the useful question is not whether privacy is possible in each of them. It is how many settings you have to change to lose it, and whether you can name them.

That is a shorter list than a policy page and a more honest one, because it is checkable. For Munin it is four settings, and the next section names all four. For any other project on this page, the same exercise works: grep the configuration for every key that takes an API endpoint or an API key, and the ones you find are your sub-processor list. If the answer comes back longer than you expected, that is the finding.

What actually leaves the machine?

Nothing, until you decide it should. A fresh install reaches no third-party API at all: mail is an in-memory stub, embeddings are a deterministic local stub, and assets are written to a local volume. Search works, the dashboard works, the MCP endpoint works, and no vendor account exists anywhere in the setup. That is the literal answer to "private CRM" — not a policy page, a default.

Four switches change it, and they are worth writing down as a list, because that list is your data-flow diagram and it is the one an auditor asks for. Setting MUNIN_MAIL_PROVIDER=resend sends recipient addresses and message bodies to Resend. Setting OPENAI_API_KEY sends document and message text to an embedding provider — or point OPENAI_BASE_URL at LM Studio, vLLM, or llama.cpp on the same box and the vectors never leave the building. Switching MUNIN_STORAGE_PROVIDER to s3 sends assets to the bucket you name, which can be MinIO on the same host. And the LLM provider you configure for the built-in agent runner is the fourth.

Everything else stays where you put it. Credentials for those providers — API keys, IMAP and SMTP passwords — are encrypted at rest through pgcrypto with MUNIN_ENCRYPTION_KEY, so the connection strings in your database are not readable by someone who ends up with a dump of it. If your reason for self-hosting is a jurisdiction rather than a bill, the residency argument has its own piece, and the short version is that on your own metal the sub-processor list is whatever those four switches say it is.

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

Does a self-hosted install need an external AI service to answer anything?

It needs an LLM provider you choose, and nothing else. Munin ships its own in-process agent runner — per organisation, inside the backend, no extra container — which answers live conversations on every channel and works a durable background queue: knowledge-base curation, CRM hygiene, contact extraction from closed threads, outreach drafts. External MCP clients are optional rather than required.

This is the part that surprises people who have deployed the rest of this field, where "and now run the worker" is the fourth bullet of every install guide. There is no worker container here and no Redis to broker the queue; the job queue is durable in the same Postgres the data lives in, with retry and dead-letter handling, which is most of why the container count is three. Point OPENAI_BASE_URL at a local server and the entire loop — answer, draft, curate — runs without a packet leaving the host.

The review posture does not loosen because the runner is local. Outreach is propose-only wherever it runs: every message waits in a queue for a human, and recipients are drawn only from contacts with recorded consent. We wrote up why that queue is the interesting part rather than the drafting.

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 240 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 deep customisation of an enterprise sales process, SuiteCRM. 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, SuiteCRM 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.

Can one self-hosted install serve several clients? One install serves one organisation. The open-source build is single-tenant and invite-only, so several clients means several stacks — three containers each, behind one reverse proxy — or Munin Cloud, which is the multi-tenant build of the same code. That is close to the category norm: EspoCRM documents separate installations per company, and Odoo's answers are multi-company inside one database or dbfilter across several.

What leaves the machine on a self-hosted install? Nothing by default. Mail, embeddings, and asset storage all ship as local stubs, so a fresh install reaches no third-party API. Four settings change that — a mail provider, an embedding key, S3 storage, and the LLM provider for the agent runner — and each has a local option: llama.cpp or vLLM for embeddings, MinIO for assets, a local model for the runner.

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.

Does it need a separate worker or queue container? No. The agent runner is in-process, per organisation, and its job queue is durable in the same Postgres as everything else, with retry and dead-letter handling. That is the main reason the stack is three containers rather than five.

Is self-hosted Munin the same as Munin Cloud? 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 — 240 MCP tools, 278 REST endpoints, and 62 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.

Which container image should I pin? Check who publishes it. On 28 August 2025 Bitnami moved its existing catalogue to docker.io/bitnamilegacy, which receives no further updates or security patches, and for several projects in this field that was the most-pulled image on Docker Hub. Prefer an image the project itself builds; where there isn't one, know whose build you are trusting.

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.

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.

What is the best self-hosted private CRM? One whose defaults are private before you configure anything. Munin, Zammad, SuiteCRM, ERPNext and Dolibarr reach no third-party API on a fresh install; EspoCRM and FreeScout are the same on plain PHP hosting. Judge them by how many settings it takes to lose that, and whether you can name them. For Munin it is four: a mail provider, an embedding key, S3 storage, and the LLM provider for the agent runner.

How do I put HTTPS in front of a self-hosted CRM? With a reverse proxy that terminates TLS and forwards to the app on localhost. A Caddy site block is three lines and manages the certificate for you; nginx plus certbot does the same job at more length. For Munin, proxy :3000 for the dashboard and :3001 for the backend, then make MUNIN_AUTH_TRUSTED_ORIGINS and NEXT_PUBLIC_MCP_URL name those public hostnames — sign-in fails its CSRF check otherwise.

Can I run a self-hosted CRM without opening a port? Yes, two ways. An outbound-only tunnel such as Cloudflare Tunnel publishes a hostname from a daemon on your host, so nothing inbound is open — at the cost of adding that vendor to your data path. A private overlay such as Tailscale or WireGuard publishes nothing at all, at the cost of reach: a client on your own laptop connects, and a client dialling out from a vendor's servers cannot resolve the address.

How do I know a self-hosted project is still maintained? Read releases, not stars. Check the date of the newest tagged release, whether the default branch has commits this month, and whether that newest tag is a pre-release. Most of this field is healthy — ERPNext, Twenty, Chatwoot, FreeScout and Munin all shipped releases in the week to 21 September 2026. Monica is the one to check: its stable line is v4.1.2 from May 2024, with a v5 pre-release above it.

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. SuiteCRM is an Apache/PHP app and a cron. 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. SuiteCRM, Zammad, ERPNext, Dolibarr and Munin hold nothing back.
  • Check who publishes the container image as well as who publishes the code. Bitnami moved its existing catalogue to docker.io/bitnamilegacy on 28 August 2025, and that repository receives no further updates or security patches.
  • Budget 1 to 8 GB of RAM depending on the project — EspoCRM, SuiteCRM 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.
  • Nothing leaves the machine by default: mail, embeddings and asset storage all ship as local stubs. Four settings change that, and each has a local option — llama.cpp or vLLM for embeddings, MinIO for assets, a local model for the agent runner.
  • The agent runner is in-process and per organisation, with a durable job queue in the same Postgres. No worker container, no Redis, no separate scheduler — which is why the stack is three containers.
  • One install serves one organisation, invite-only. Several clients means one three-container stack each behind a reverse proxy, or Munin Cloud, which is the multi-tenant build of the same code. EspoCRM documents separate installations per company too.
  • 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.
  • Put a reverse proxy in front before anyone else can reach it, and make the hostnames in MUNIN_AUTH_TRUSTED_ORIGINS and NEXT_PUBLIC_MCP_URL match it. If you would rather open no port at all, an outbound tunnel or a private overlay network both work — the tunnel adds a vendor to the data path, the overlay costs you any client that connects from someone else's servers. 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, 240 MCP tools, 278 REST endpoints, 62 bundled skills, and export tools that move you between your machine and Cloud in either direction — counted against the live sources on 23 September 2026.

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.