Infrastructure Notes
How I Finally Got ERPNext and Coolify to Coexist on the Same VPS
Without burning it down.
There is a particular kind of confidence that only exists a few minutes before deploying something complicated to a production server.
“They’re both Docker applications. Docker isolates everything. They’ll coexist just fine.”
It was a perfectly reasonable assumption. It was also the assumption that cost me several evenings.
Day 1 — Confidence Is Not an Architecture
By the time ERPNext entered the picture, the VPS was already doing exactly what I wanted.
Coolify had become the control plane for almost everything I hosted. Applications deployed with a Git push, Traefik quietly handled reverse proxying, HTTPS certificates renewed themselves, and Docker networks stayed neatly isolated.
Then I decided to install ERPNext.
If you’ve never looked beneath the surface of ERPNext, it is easy to think of it as another web application.
It isn’t.
ERPNext is better described as an ecosystem built around the Frappe Framework. What appears to be a single login page is actually a collection of tightly coupled services, each expecting the others to exist exactly where they are supposed to be.
Behind the scenes, ERPNext depends on:
- MariaDB storing transactional data
- Multiple Redis instances serving different responsibilities
- Queue workers processing asynchronous jobs
- Scheduled workers handling recurring tasks
- Socket.IO managing realtime events
- Gunicorn serving Python applications
- An Nginx layer that assumes it is sitting at the front door
My mistake was not installing ERPNext. My mistake was assuming it wanted to be treated like every other container I had ever deployed.
Day 2 — When Two Reverse Proxies Think They Are In Charge
Docker itself never complained very much. That was what made the debugging process so deceptive.
docker compose up -d
Containers started. Networks were created. Volumes mounted successfully.
✔ Network frappe_default Created
✔ Container redis-cache Started
✔ Container redis-queue Started
✔ Container mariadb Started
✔ Container frappe-backend Started
✔ Container frontend Started
Everything appeared healthy. Until someone actually visited the website.
Coolify had already claimed responsibility for ports 80 and 443 because that is exactly what Traefik is supposed to do. ERPNext’s bundled frontend expected those same ports.
Neither application was technically wrong. They simply disagreed on who owned the edge of the network.
The symptoms were inconsistent. Docker sometimes refused to bind ports. Sometimes SSL failed. Sometimes browsers disagreed with each other. Chrome showed ERR_SSL_PROTOCOL_ERROR, while Firefox appeared perfectly happy.
The First Lesson: Infrastructure Needs Clear Ownership
The turning point was not discovering a hidden configuration option. It was realising I had accidentally designed a system where two components both believed they were responsible for the same job.
Traefik wanted to:
- Terminate HTTPS
- Route incoming traffic
- Manage the public edge
ERPNext wanted to:
- Expose itself directly
- Use its bundled Nginx
- Control its own front door
Both systems were behaving exactly as designed. The design itself was flawed.
Modern infrastructure works best when every responsibility has exactly one owner: one DNS authority, one reverse proxy, one certificate manager, and one entry point.
Once I understood that, the solution became surprisingly simple. ERPNext should not expose ports 80 or 443 at all. Those ports already had an owner.
Day 3 — Stop Fighting the Architecture
Instead of trying to expose ERPNext directly, I stopped thinking about public networking altogether.
MariaDB did not need to be reachable from the internet. Neither did Redis, the workers, or Socket.IO. Those services only needed to communicate with each other.
Internet
│
▼
Traefik / Coolify
│
────────────────────
Docker Network
│
├── frappe-web
├── websocket
├── scheduler
├── redis-cache
├── redis-queue
├── redis-socketio
└── mariadb
Notice what is missing. No service is attempting to publish public ports.
ports:
- "80:80"
- "443:443"
Instead, the services only expose themselves internally.
services:
frontend:
expose:
- "8080"
websocket:
expose:
- "9000"
The containers became private citizens inside the Docker network. Traefik became the only resident allowed to answer the front door.
Day 4 — Reverse Proxies Cannot Read Minds
At this stage the application loaded. That felt like progress. It was not finished.
ERPNext still behaved as though every request arrived over plain HTTP. Secure cookies were not always secure. Redirects occasionally looped. WebSocket upgrades behaved unpredictably.
ERPNext simply had no idea someone else had already handled HTTPS.
X-Forwarded-Proto: https
X-Forwarded-Host: erp.example.com
X-Forwarded-Port: 443
Once those headers were trusted, the application’s behaviour changed almost instantly. Secure cookies remained secure, sessions stopped disappearing, realtime events stayed connected, and browser warnings vanished.
Ironically, HTTPS had been working for hours. ERPNext simply did not know it.
Day 5 — Silence Is the Best Metric
People often celebrate the moment an application loads for the first time. That was not my favourite moment.
Mine came several hours later when I realised I had not looked at the logs in ages. Not because I had forgotten, but because there was nothing interesting happening anymore.
docker ps
docker logs frappe-backend
No restart loops. No stack traces. No SSL renewals failing overnight. No mysterious 502 errors.
The infrastructure had become boring. And boring infrastructure is exactly what production systems should be.
What This Deployment Actually Taught Me
This project taught me far more about architecture than it did about Docker.
The biggest lesson was not a command, a configuration file, or even ERPNext itself. It was understanding that complex systems behave predictably once every component has clearly defined responsibilities.
Coolify manages:
- Deployments
- Reverse proxying
- TLS termination
- Domain routing
- Certificate renewal
ERPNext manages:
- Business logic
- Inventory
- Accounting
- Manufacturing
- CRM, HR, and workflows
Neither platform needs to replace the other. They simply need to respect where one ends and the other begins.
Final Reflection
Looking back, none of the failures were random.
Every SSL warning, redirect loop, port conflict, and browser inconsistency came from the same incorrect assumption: that every Docker application wants to be deployed in exactly the same way.
ERPNext does not. It expects to manage an internal ecosystem of tightly connected services. Coolify expects to manage everything that faces the outside world.
Once I stopped trying to make either platform compromise its design, the deployment stopped feeling fragile.
Very little configuration changed after that. What changed was my understanding of the architecture.
Sometimes the hardest infrastructure problems are not solved by adding another configuration file. Sometimes they are solved by deciding, once and for all, who is actually in charge.
If you're passionate about creating thoughtful, user centred digital experiences, building custom products, or collaborating on projects where design and development go hand in hand, I'd love to connect. Whether it's to exchange ideas, explore opportunities, or collaborate on something meaningful, feel free to reach out.



