Architecture Defense: Vultr VPS + Docker Compose + Git Pull Updates for OpenEMR
I deployed OpenEMR (fork at https://github.com/MichaelHabermas/openemr, based on the official openemr/openemr repo) exactly as the project intends: official Docker images + docker-compose.yml with 2–3 services (OpenEMR PHP/Apache container + MariaDB; Redis explicitly skipped).
Live instance: https://openemr.titleredacted.cc/ (SSL working, login page loads cleanly).
This was stood up in the 48-hour review window: clone → local Docker Compose test → Vultr Ubuntu VPC deploy → domain/env tweaks → live. It is the simplest, lowest-risk, production-viable path for this scope.
-
Vultr Ubuntu VPC (self-hosted VPS) – not Railway / Render / Fly.io / Heroku-style PaaS
- Full control, zero vendor lock-in, predictable flat monthly cost. A small Vultr instance is cheaper than Railway’s compute + DB + bandwidth once you have real users, file uploads, and reports running.
- Direct firewall/port management (we opened only what was needed for web + Docker). Cloudflare DNS + proxy works perfectly; no PaaS networking quirks.
- Medical EMR data stays on infrastructure we fully own and can snapshot/backup at the hypervisor level. HIPAA-adjacent self-hosting is common for OpenEMR precisely because you control the data plane.
- Railway would force us to split the ready-made
docker-compose.ymlinto separate services, manage linked volumes/DB persistence their way, and pay for every CPU/DB minute. Not worth it when the official Docker workflow already works perfectly on a VPS.
-
Docker Compose (2–3 services) – the official, recommended path
Official OpenEMR devops repo and DOCKER_README.md ship production-readycompose.ymlfiles with exactly this pattern: oneopenemrservice (PHP + Apache) + MariaDB. No Redis in production templates. We mirrored it.- Redis was deliberately omitted: it’s a dev-only caching optional in some “development-easy-redis” examples. For our current scope (small practice, no heavy concurrent load), it adds memory, another service, and complexity with zero measurable gain. We can profile later and add it in one line if needed.
- Volumes are grafted so the host git checkout is mounted into the container. This is the real-world “easy mode” that OpenEMR developers themselves use:
docker compose down && git pull && docker compose up -d. Code changes land instantly, no rebuilds, no Docker Hub pushes. Dead simple for a small team.
-
Domain + SSL handled inside the Docker container
We set the domain env vars (standard in the official image) and let the container mint the Let’s Encrypt cert. No external reverse proxy, no extra Traefik/Nginx layer, no manual certbot dance. One-time env tweak, done. Cloudflare DNS points straight at the VPS public IP. Minimal moving parts, maximum reliability. -
Deployment & Update Workflow = deliberately “good enough for now”
# Initial git clone ... cp .env.example .env # set domain, DB creds, etc. docker compose up -d # Every future update docker compose down git pull docker compose up -d
This is exactly how most self-hosted OpenEMR production instances run. No CI/CD pipeline, no GitHub Actions, no Railway rebuild queue. We validated the entire flow locally in the 48-hour window, then mirrored it on Vultr. It works, it’s auditable, and it scales with the team’s actual velocity.
| Aspect | Our Vultr + Compose + Git Pull | Railway (or equivalent PaaS) | Winner for This Project |
|---|---|---|---|
| Setup time | 48-hour full validation | Faster for toy apps, slower for multi-service + custom SSL | Vultr |
| Multi-service DB+App | Native Compose, one file | Split services + manual linking | Vultr |
| Code updates | git pull + restart (seconds) |
Git push → rebuild | Vultr |
| Cost | Fixed low VPS monthly | Usage-based (compute + DB + egress) | Vultr (real usage) |
| Control / Debugging | SSH, logs, volumes, firewall | Platform abstractions | Vultr |
| Data sovereignty | Full control on our VPC | On their infra | Vultr |
| Redis / extras | Add only when needed | Easy but still extra cost | Vultr |
| Medical data fit | Self-hosted standard | Possible but more compliance layers | Vultr |
Railway is fantastic when you want zero ops. Here the ops are already zero because OpenEMR’s Docker story is mature and the workload is classic LAMP-in-containers. Adding a PaaS layer would be complexity theater.
This is the pragmatic, battle-tested architecture that the OpenEMR maintainers themselves ship and that hundreds of clinics run in production: VPS + official Docker Compose + simple git-driven updates.
We did the 48-hour deep dive, ran it locally, deployed it live, fixed the domain/SSL vars, opened the ports, and proved it works. It is the correct “good enough for now” solution.
After further security/audit passes we can revisit (add Redis, move to Kubernetes via openemr-devops, or layer Coolify on the same VPS if we want a GUI), but right now this deployment is simple, maintainable, cost-effective, and fully aligned with how OpenEMR is designed to be run.
No need to over-engineer. This is the defensible, real-world choice.