I’m setting up ERPNext (with the Frappe HR app) for a small company. About 7 people will actually use it. We host on-prem — we have a capable server and the in-house know-how — so Frappe Cloud is not the route we want to take.
What I’ve tried so far:
Installed directly on Ubuntu 24.04 (bare-metal bench install). Everything works fine, including TLS certificates. Smooth so far.
I’ve also read that the Docker setup is considered more “production ready” and nicer for consistent installs, updates and backups.
Where I’m unsure:
The only thing that worries me about the bare-metal setup is the update path. On the other hand, the Docker production setup is ~9 containers (backend, frontend, websocket, scheduler, two queues, MariaDB, two Redis), and I’m not sure that orchestration overhead is justified at our scale.
My questions:
For a small instance like this (~7 users, no custom apps right now), would you recommend staying on the bare-metal bench install, or moving to Docker? What actually tips the decision in practice?
What does a reliable update workflow look like in each case? My understanding is bare-metal = backup + bench update, and Docker = backup + swap image tag + bench --site all migrate. Is that correct, and what are the common pitfalls (e.g. major version jumps, the encryption_key / site_config.json on restore)?
Any real-world experience running Frappe HR specifically in either setup that would influence the choice?
I’d rather pick the approach I can operate safely long-term than chase “production ready” for its own sake. Appreciate any input from people running similar small deployments.
TLDR: A single bare-metal host is almost certainly fine for your use case.
The “What Production Setup Do I Need” question is never a simple answer. Some people talk about “concurrent users.” Others talk about “transactions per hour”. The truth is it’s always more complicated than that.
Kinds of ERP Activities
ERPNext Users: They could be doing something low-intensity, like creating a new Customer record. Or doing something high-intensity, like running complex Reports that query a wide date range.
External API Calls: Are 3rd party services sending requests to your REST endpoints? What exactly are they doing?
Background Tasks: What kinds of tasks are running periodically in the background? Are they CPU intensive, I/O intensive?
Database Connections: Do you have 3rd party ETL or Reporting connected to your MariaDB database? What kinds of queries/statements are running?
As you can see, even with just 7 users, you could run ERP performance into a brick wall (under the right conditions)
This makes it very difficult to give free, accurate, good advice. We’d have to know everything about that small company’s business: its users, what they will be doing, the exact hardware specs, and much more. Before we could provide any kind of meaningful recommendation. You can get that advice, but it’s called consulting, and you’ll need to pay for it.
With all that said, I’m reasonably confident you’ll be fine as-is. I’ve run much more intensive setups than you’re describing, just on a single VPS. If you do encounter problems, the first thing to do is move the MariaDB database to a separate host (physical, VPS, managed database, whatever). Low latency between gunicorn and MariaDB is very important. That will give you the biggest improvement for the least effort.
Your workflow seems reasonable. But don’t forget to copy your backups to a second location. The built-in bench backup doesn’t mean anything if the local backups are lost, missed, or corrupted.
I don’t have any experience with Frappe HR to share.
Thanks for the heads-up! In my case I went the Docker route and self-host everything through my own self-hosted registry — I build the image (Frappe + ERPNext + HR + Wiki via apps.json) on a separate build VM, push it to my local registry, and Coolify just pulls the finished image. The install was definitely tricky and took some trial and error, but so far everything’s working. I’m aware of the immutable-image limitation, so I handle app changes by rebuilding the image and running bench migrate rather than get-app/new-app inside the container. Will see how it holds up over time, but the setup is running now.