HAProxy for K3s¶
HAProxy is the external load balancer in front of the K3s cluster. It exposes one stable LAN address and distributes API and ingress traffic across all three K3s nodes. It runs natively in a small LXC on Zion.
LXC¶
| Property | Value |
|---|---|
| System | Debian 13, unprivileged LXC |
| CPU | 1 vCPU |
| Memory | 256 MB |
| Swap | 256 MB |
| Root disk | 4 GB |
| Runtime | Native haproxy.service |
HAProxy is stateless. Its important state is the configuration file:
Traffic and load balancing¶
flowchart LR
clients["LAN clients<br/>kubectl · browsers · K3s services"]
haproxy["HAProxy LXC<br/>80 · 443 · 6443"]
master["master<br/>node1"]
worker1["worker1<br/>node2"]
worker2["worker2<br/>node3"]
clients -->|"HTTP :80<br/>HTTPS :443<br/>K3s API :6443"| haproxy
haproxy -->|"round-robin<br/>80 · 443 · 6443"| master
haproxy -->|"round-robin<br/>80 · 443 · 6443"| worker1
haproxy -->|"round-robin<br/>80 · 443 · 6443"| worker2
All three K3s machines are control-plane and embedded-etcd nodes despite the historical names master, worker1 and worker2.
Each backend uses balance roundrobin. New API and HTTPS connections, and HTTP requests, are distributed between available nodes. Backend health checks keep an unavailable node out of rotation.
The API and HTTPS frontends use TCP mode. TLS passes through HAProxy and is terminated by K3s or Traefik. Port 80 uses HTTP mode. HAProxy therefore stores no kubeconfig, cluster credentials or application certificates.
Why it left Docker¶
The old HAProxy ran in Docker on Oracle Legacy and used an additional address assigned to that host. This created two dependencies for a critical cluster endpoint: the Docker engine and the network configuration of a general-purpose server.
The replacement LXC owns IP address directly. It can start, stop, update and restore without Logos or another Docker host.
The previous deployment is retained as a reference:
Migration and cutover¶
The migration was deliberately simple because HAProxy has no application data:
- Record the legacy listeners, backend addresses and timeouts.
- Create the Debian LXC with its final CPU, memory and disk limits.
- Install native HAProxy.
- Copy the existing
haproxy.cfgto/etc/haproxy/haproxy.cfg. - Validate it with
haproxy -c. - Stop the legacy HAProxy container and remove the additional address from Oracle Legacy.
- Assign the same IP address to the new LXC.
- Start HAProxy and verify listeners
80,443and6443. - Test the Kubernetes API and one ingress route.
- Keep the old container stopped until the LXC passes normal operation and backup.
Why K3s clients did not change¶
The network identity and ports stayed the same. Internal DNS for the cluster continued to resolve same IP address, so:
- kubeconfig API endpoints remained valid;
- K3s
--serverreferences remained valid; - ingress consumers kept the standard HTTP and HTTPS frontend ports.
The cutover changed the machine behind the endpoint, not the endpoint itself.
Configuration rules¶
- API, HTTP ingress and HTTPS ingress use separate frontend and backend sections.
- API and HTTPS use TCP mode; HTTP uses HTTP mode.
balance roundrobindistributes traffic across the three nodes.- Every K3s node is listed in the relevant backend.
- Backend checks remove unavailable nodes from rotation.
- Connect, client and server timeouts are explicit.
- Configuration is validated before reload or restart.
- HAProxy process health and K3s backend health are checked separately.
Kubernetes application routing remains in Flux and Kubernetes. HAProxy only forwards traffic to the cluster.
Validation¶
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
systemctl is-active haproxy
systemctl is-enabled haproxy
ss -lnt
journalctl -u haproxy --no-pager -n 50
Functional checks:
- Confirm all three frontend ports are listening.
- Test every backend from the HAProxy LXC.
- Check Kubernetes API readiness through the frontend.
- Open one internal ingress endpoint through HAProxy.
- Confirm that no unexpected systemd units are failed.
Quick diagnosis¶
| Observation | Check |
|---|---|
| Configuration validation fails | haproxy.cfg syntax and section mode |
| Frontend port is closed | haproxy.service and local listener |
| One backend fails | Node state and VLAN reachability |
| API works but an application fails | Ingress, Service, DNS or application |
| Every backend fails | Cluster power, VLAN, switch or HAProxy route |