Why I Ditched Docker Compose and Went Bare-Metal for My ERP Staging

Why I Ditched Docker Compose and Went Bare-Metal for My ERP Staging

I love Docker Compose. For local dev, it’s unmatched — spin up PostgreSQL, Redis, and your app with one command and you’re done. Clean. Reproducible. Beautiful.

But for my ERP staging environment? I ripped it out completely and went bare-metal. Here’s why, and why you might want to do the same.

The Problem

My ERP runs inventory, finance, and HR modules. Each has its own dependencies. Some need PostgreSQL 15 with specific extensions (pg_partman for table partitioning, pg_cron for scheduled maintenance). Others use SQLite for local caching. One module communicates with a physical warehouse scanner over a serial port — something Docker handles about as elegantly as a toddler handles chopsticks.

Docker Compose worked fine when I had two services and a single database. Then I hit six services. Then I needed to test serial port passthrough, USB device mapping for barcode scanners, and NFS mounts for shared storage across branches. Suddenly my docker-compose.yml was a forty-line monster of volume mounts, device mappings, network configs, and environment variables. It broke on every minor Docker Desktop update, and debugging “why did my container lose its USB mapping” was never, ever worth the time.

The Switch

Here’s what my staging setup looks like now:

  • PostgreSQL 15 — installed directly via apt, configured with production-adjacent settings (shared_buffers, effective_cache_size tuned to the actual hardware, not Docker’s default 128MB)
  • Redis — same, direct install with persistence enabled
  • App services — run via systemd service units with proper restart policies, resource limits, and logging
  • Serial and USB peripherals — just work, because they’re plugged directly into the host. No –device flags, no cgroup permission battles
  • Monitoring — Prometheus node exporter scraping the real machine metrics, not a container’s view of the kernel

Every service logs to journald. I check everything with journalctl -u {service} — no docker logs, no docker exec, no “which container was that again?” Just standard Linux tooling every sysadmin already knows.

The Real Benefit

The biggest win wasn’t performance or simplicity. It was fidelity to production.

My production ERP runs on bare-metal Ubuntu Server. When I was developing and staging inside Docker, I’d occasionally ship something that worked perfectly in a container but broke on the real system. Volume mount permission issues (Docker’s rootless mode mapping UIDs differently). Network driver quirks (macvlan vs bridge vs host mode behaving differently for UDP broadcast). Environment variable handling quirks (Docker’s init vs systemd’s environment file parser).

With bare-metal staging, what you test is what you deploy. Every commit gets deployed to staging via a simple Ansible playbook — the same one used for production, just pointed at a different host. It runs the full test suite: unit tests, integration tests, and a smoke test that physically connects to the warehouse scanner and prints a test label. If it passes on staging, it passes on production. No surprises.

What I Still Use Docker For

I’m not anti-container. Docker excels at ephemeral workloads. I still use it for:

  • Spinning up a disposable PostgreSQL instance for a quick data migration test
  • Running integration tests against fresh databases with known state
  • Testing library upgrades in complete isolation before touching the real system
  • CI/CD pipelines where every build starts from a clean slate

But for long-running staging environments that need to mimic production behavior? The abstraction overhead isn’t worth it when your stack is five services or fewer, all running on the same OS, talking to real hardware.

The Verdict

If your app is stateless, horizontally scalable, and speaks HTTP — Docker Compose is perfect. Keep using it.

If your app touches hardware, uses serial ports, mounts NFS shares, integrates with USB peripherals, or needs low-level system access — consider bare-metal staging. You’ll spend less time debugging Docker abstractions and more time building features that actually work in production.

I migrated my ERP staging environment in one afternoon. The Ansible playbook is 80 lines. I haven’t touched docker-compose.yml since, and my deployment pipeline hasn’t had a single “works on my machine” incident in months.


Discover more from Susiloharjo

Subscribe to get the latest posts sent to your email.

Discover more from Susiloharjo

Subscribe now to keep reading and get access to the full archive.

Continue reading