Skip to content

Instantly share code, notes, and snippets.

@decagondev
Created April 28, 2026 17:41
Show Gist options
  • Select an option

  • Save decagondev/361a28438ab7f5d5814badf11aba0ca3 to your computer and use it in GitHub Desktop.

Select an option

Save decagondev/361a28438ab7f5d5814badf11aba0ca3 to your computer and use it in GitHub Desktop.

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.

Core Decisions & Why They Win in the Real World

  1. 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.yml into 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.
  2. Docker Compose (2–3 services) – the official, recommended path
    Official OpenEMR devops repo and DOCKER_README.md ship production-ready compose.yml files with exactly this pattern: one openemr service (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.
  3. 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.

  4. 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.

Direct Comparison to Railway (and similar PaaS)

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.

Bottom Line

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment