# Load Balancer TLS, DNS, routing, and the certificate lifecycle Elements manages for you. Every Elements deploy machine runs its own load balancer. It is part of Elements, starts alongside the app, and gives you two things automatically: SSL via Let's Encrypt for every configured domain, and request balancing across every machine in the environment. There is no extra software to install, no separate appliance to put in front of the cluster, and no separate config file to maintain. The load balancer reads the same `config.jsoc` your app reads, and it auto-reloads on new builds as needed, so changes to domains, certificates, or services propagate without restarting anything. Because every machine runs the same load balancer and knows about every other machine in the environment, you can point your DNS at one of your machines, several of them, or all of them. Whichever box receives a request will distribute it across the cluster. What it does: - HTTP listener on port 80 (configurable). Serves ACME challenges and 301-redirects to HTTPS when TLS is on. - HTTPS listener on port 443 (configurable). Terminates TLS, then proxies to the app or service backend. - Auto SSL via ACME / Let's Encrypt. Certificates issued and renewed automatically for every configured domain. - Static SSL: bring-your-own certificates served by SNI match (overrides ACME for those hosts). - Host routing: dispatches by `Host` header to the app or a named service. - Round-robin: balances requests across all machines in the environment. - Failover: retries other machines when a backend fails. Serves the maintenance page only when every backend is unreachable. - Redirects: 301 from configured source hosts to destination hosts before any service routing. - Maintenance: serves a static maintenance page when `deploy.maint = true`. The load balancer hot-swaps its handler atomically when the project config changes. Adding a domain, flipping `ssl`, or rotating a static cert takes effect without dropping connections. ### Auto SSL (acme) When `ssl: true` (the default when `domain` is set) and the machine has a `publicIp`, the LB enables ACME via Let's Encrypt. On the first HTTPS handshake for a configured host, the LB negotiates a certificate using the HTTP-01 challenge served on port 80. Subsequent handshakes use the cached certificate. Renewal happens automatically before expiry. Set up: 1. Configure the domain in `deploy.domain` (and any service `domains`). 2. Point DNS at the machine's public IP. 3. Open ports 80 and 443 to the public internet. The first HTTPS request takes a few seconds while the certificate is issued. After that, it's cached on the machine and renewed automatically. ACME is enabled when `deploy.ssl` is true (default if `domain` is set), at least one domain is configured, and the machine has a `publicIp`. When any condition is missing, TLS is disabled and the LB serves cleartext on port 80. Useful for internal-only deployments or local testing. ### Static TLS Certs To override ACME for specific hosts, drop a cert/key pair on disk and reference them in `deploy.tlsCerts`: ```jsoc deploy: { tlsCerts: { "*.example.com": { cert: "config/tls/wildcard.crt", key: "config/tls/wildcard.key" }, "example.com": { cert: "config/tls/wildcard.crt", key: "config/tls/wildcard.key" }, "api.example.com": { cert: "config/tls/api.crt", key: "config/tls/api.key" }, }, } ``` When the LB sees a matching SNI hostname, it serves the static cert instead of talking to ACME. Match priority: 1. Exact match (`api.example.com`). 2. Single-label wildcard (`*.example.com`). ACME continues to operate for any host without a static entry. The two modes coexist per-host. Cert paths resolve relative to the project root. Keep keys out of version control (mode 600; add to `.gitignore`). ### DNS Point DNS at the public IP of one or more machines. For a small app, point at `production0` and stop: ``` A example.com ``` For redundancy or scale, point at multiple machines: ``` A example.com A example.com A example.com ``` DNS round-robin distributes incoming requests across machines. The LB on each machine then round-robins to all the other machines, so a request that hits the busiest machine still gets balanced. Multiple A records also give you basic failover: if one machine is unreachable at the network level, the resolver falls back to another. For finer-grained failover, use a managed DNS provider with health checks (Cloudflare, Route 53). Elements doesn't need it, but you can layer it on. ### Routing The LB looks at the `Host` header on each request: 1. Strip the port. 2. If the host matches a `redirects` entry, return 301 to the destination host. 3. If the host matches a service's `domains`, proxy to that service's port (round-robined across machines). 4. Otherwise, proxy to the app backend (default port 4000, round-robined across machines). ```jsoc deploy: { services: [ { name: "api", port: 8080, domains: ["api.example.com", "v1.api.example.com"], bin: "services/api/bin/api", }, ], } ``` ### Round-Robin and the public/private Distinction Public requests (source IP outside the private network) are round-robined across every machine in the environment that has a `privateIp`. Each LB picks a different machine on each request via an atomic counter. Requests from a private IP (other LBs forwarding to this one) are served locally instead of being round-robined again. This prevents infinite loops where two LBs forward to each other indefinitely. ### Failover When a backend connection fails (refused, timeout), the LB tries the next machine. It cycles through every backend before giving up. Only when every backend is unreachable does the LB serve the maintenance page. A single machine going down doesn't take traffic out. Add machines for headroom; the cluster heals itself per request. ### Maintenance Mode ```jsoc deploy: { maint: true, } ``` When `maint` is true, the LB serves a static maintenance page on every request instead of proxying. Use during heavy data migrations or planned outages. The maintenance page is also served as the fallback when no backend is reachable. ### CDN A CDN isn't required. Asset URLs are content-hashed and the LB serves them with `Cache-Control: immutable`, so the browser caches them indefinitely after the first fetch. The origin handles asset traffic well at small to medium scale. When you do want a CDN (CloudFront, Cloudflare, Fastly, Bunny, etc.) the integration is one config setting: 1. Create a distribution at the CDN. Point it at the LB's public IP or hostname as the origin. The CDN gives you a hostname back, e.g. `d1234abcd.cloudfront.net`. 2. Set `build.assetUrl` in `config.jsoc`: ```jsoc { build: { assetUrl: env("ASSET_URL", ""), }, } ``` ``` # production.env ASSET_URL=https://d1234abcd.cloudfront.net ``` 3. Deploy. That's the entire integration. The build rewrites every emitted asset reference in your pages to use that base URL. Browsers fetch from the CDN; the CDN fetches from origin once on cache miss, then serves from edge. In development, leave `ASSET_URL` empty. Elements emits relative URLs that hit the LB directly. There is nothing to switch on between development and production beyond the environment variable. Configure the CDN to forward the `Host` header so the LB's host routing still works for any non-asset request that hits the same distribution.