The HA building blocks

We’ve already covered the classic HA building blocks on their own:

  • HAProxy: a load balancer that spreads traffic across backends and drops the dead ones
  • VRRP: a floating IP that fails over between two nodes, so the load balancer itself isn’t a single point of failure.

I highly suggest to read them if you haven’t already, so that you can later implement the architecture you choose in this page, based on your actual website architecture.

Still, Load balancers in front of one web server aren’t enough the moment that web server dies: it needs to be redundant as well, otherwise its considered a single point of failure.

This page gives you a generic view on how to make your website resilient from top to bottom.

It comes down to two levers:

  • Detection + self-healing: notice a failure within seconds and try to recover automatically (restart, failover, serve from cache).
  • Remove single points of failure (SPOFs): no single host or component whose death takes the whole site down (CDN, replicas, DB failover, resilient DNS).

How far you push depends on whether your site is static (easy) or dynamic (hard), and on how critical it really is.


Detection + Self-Healing

The cheapest, highest-ROI layer: applies whether the site is static or dynamic.

flowchart LR
    subgraph D["🔭 detect"]
        Mon["uptime monitor"]
        HC["healthcheck"]
    end
    Mon -->|"down"| Alert["📣 alert you<br/>(Telegram / email)"]
    HC -->|"unhealthy"| Heal["♻️ auto-restart<br/>(systemd / container)"]
  • External uptime monitoring + alerting (UptimeRobot, Healthchecks.io…): know within a minute, and route the alert somewhere you’ll actually see (email, Telegram…)
  • Service auto-restart: systemd Restart=on-failure, containers restart: unless-stopped.
  • Health checks: nginx up, app /healthz, Docker HEALTHCHECK, so the platform can detect a sick instance and restart it
  • TLS auto-renew (certbot.timer) so the certificate never silently expires
  • Resource guards: disk-space alerts + log rotation (a full disk takes sites down quietly)
  • Backups + tested restore: content, config and data (a restore you’ve never tried isn’t a backup)

Remove SPOFs

Static sites

A static site has no server-side state… so you can easily cache and replicate it anywhere.

This is the cheapest path to real HA.

flowchart LR
    User((🌍 users)) -->|HTTPS| CDN["☁️ CDN<br/>edge cache · Always Online · TLS · DDoS"]
    CDN -->|"cache miss"| Origin["🖥️ origin<br/>(VPS / static host)"]
    CDN -. "origin down → serve cached copy" .-> User
  • CDN in front (e.g. Cloudflare, free): edge caching “Always Online” (serves a cached copy if the origin is down), anycast/resilient DNS, TLS at the edge, DDoS protection, hidden origin IP. The single biggest win for a static site.
  • Static hosting (Cloudflare Pages, GitHub Pages…): the site’s uptime stops depending on any one host.
  • Multi-origin (if you stay self-hosted): two or more servers serving the same content, with DNS failover or load balancing with VRRP.
flowchart TB
    User((🌍 users))
    VIP(["🔼 floating IP — Keepalived/VRRP"])
    subgraph LB["🔀 Load balancers — no SPOF"]
        LB1["HAProxy A"]
        LB2["HAProxy B"]
    end
    subgraph ORIG["🖥️ Static origins — identical content"]
        O1["web server 1<br/>(nginx + static files)"]
        O2["web server 2<br/>(nginx + static files)"]
    end
    User --> VIP
    VIP --> LB1
    VIP --> LB2
    LB1 --> ORIG
    LB2 --> ORIG

Dynamic sites

A dynamic site is an application that has state (database, sessions, uploads…), so HA means making each layer redundant, not just the front end.

flowchart TB
    User((🌍 users))
    VIP(["🔼 floating IP — Keepalived/VRRP"])
    subgraph LB["🔀 Load balancers — no SPOF"]
        LB1["HAProxy A"]
        LB2["HAProxy B"]
    end
    subgraph K8S["☸️ Kubernetes — schedules + self-heals N stateless app pods"]
        A1["app pod 1"]
        A2["app pod 2"]
        A3["app pod N"]
    end
    subgraph STATE["🗄️ Shared state (keeps the apps stateless)"]
        Redis["Redis · sessions"]
        OBJ["object storage · uploads"]
    end
    subgraph DB["🛢️ Database — HA"]
        P[("primary")]
        Rr[("replica")]
    end
    User --> VIP
    VIP --> LB1
    VIP --> LB2
    LB1 --> K8S
    LB2 --> K8S
    K8S --> STATE
    K8S --> P
    P -->|"failover"| Rr
  • Orchestration: this is where Kubernetes (or Docker Swarm) earns its keep by scheduling replicas across nodes and self-healing (overkill for a static site, of course)
  • Load balancer must not itself be a SPOF: two LBs with VRRP (that covers failures: for the throughput side of the same problem, see how to scale Load Balancers).
  • Database HA: primary + replica with automatic failover plus backups with a tested restore.
  • HA of the application itself: it’s the program that generates the site dynamically per request (reads the DB and renders the HTML). Just to make an example, in my domain I host a Grafana installation, that uses Go and Node to generate the dynamic UI.