Unreleased documentation. Choose your installed release in the version menu. Features described here may be absent from that release.
Configure HAProxy pods and access¶
The chart deploys two HAProxy replicas by default. Configure Service access,
replicas and autoscaling, and resource budgets
through Helm values. Use haproxy.podSpec for scheduling, volumes, and other pod
settings. Add settings to your complete Helm values file.
You can also manage the pods separately.
Resource limits¶
Use the resource sizing guide to budget the installation. Set HAProxy resources with haproxy.resources and agent resources with haproxy.agent.resources.
Expose traffic and management ports¶
Separate Services expose controller health and metrics, the admission webhook, HAProxy traffic and stats, and the HAPTIC agent.
Controller Service¶
The controller's ClusterIP Service exposes health and metrics ports. For a
release named haptic, the Service is also named haptic. Other release names
use the chart's fullname, usually <release>-haptic:
| Name | Container port | Values key | Purpose |
|---|---|---|---|
healthz |
8080 | controller.ports.healthz |
Single source for the process listener, liveness/readiness probes, Service, and /debug/* introspection endpoints |
metrics |
9090 | controller.ports.metrics |
Single source for the process listener, Service, and Prometheus monitors; 0 disables metrics |
The separate <fullname>-webhook Service exposes port 443 (controller.webhook.service.port) and forwards to port 9443 (controller.ports.webhook).
Configure the health and metrics Service under controller.service:
HAProxy Service¶
The <fullname>-haproxy Service sends traffic to HAProxy pods. For a release
named haptic, it's haptic-haproxy. It uses NodePort by default; configure
its ports through haproxy.service.* and pod ports through haproxy.ports.*:
| Name | Service port | Container port | nodePort default |
|---|---|---|---|
http |
80 | 80 | 30080 |
https |
443 | 443 | 30443 |
stats |
8404 | 8404 | 30404 |
The agent gets its own internal-only ClusterIP Service (<fullname>-haproxy-dataplane, for example <release>-haptic-haproxy-dataplane) on port 5555. Its type comes from haproxy.agent.service.type.
Local access, including kind clusters, works with kubectl port-forward
regardless of Service type. For a release named haptic in namespace haptic:
In another terminal, send requests to http://localhost:8080 with the hostname
configured on your Ingress. HTTP and HTTPS Gateways use
their own Services; port-forward to
the Gateway's Service to reach its listeners. NodePort access from the host also
requires a reachable node address or matching kind port mappings.
LoadBalancer access requires a load-balancer implementation in your cluster. Set the Service type and any annotations required by that implementation:
External / self-managed HAProxy — turn off the chart's HAProxy deployment and manage pods yourself (see HAProxy Pod Requirements):
PROXY protocol¶
If an upstream load balancer replaces the client address, HAProxy sees that balancer as the source. Access logs, source-IP rate limits, and IP-based policies then use the address of the balancer.
The load balancer fixes this by adding a PROXY protocol header that carries the original address. Enable the matching listeners:
controller:
config:
templatingSettings:
extraContext:
proxyProtocol:
enabled: true
httpPort: 8081
httpsPort: 8444
That adds two binds, adds them to the HAProxy Service and the NetworkPolicy, and
leaves haproxy.ports.http / haproxy.ports.https exactly as they were. Point
the balancer at the new ports:
Requests arriving on the PROXY ports carry the real client through the access
log's client_ip, src-keyed rate limiting, the WAF, and IP access control
lists. Terminated HTTPS on httpsPort uses the same certificates, ciphers, and
protocol negotiation as the plain HTTPS bind; with TLS-Passthrough configured,
httpsPort attaches to the SNI-routing frontend instead so passthrough hosts
keep working.
Send the header, or the connection is dropped
HAProxy has no "PROXY header optional" mode. A connection reaching
httpPort or httpsPort without the header is rejected, so only the
upstream balancer may target these ports. Everything else — direct access,
in-cluster clients, NodePort traffic, probes — keeps using the regular
haproxy.ports.http / haproxy.ports.https, which is why these are
additional ports rather than a flag on the existing ones.
This is separate from the
haproxy-haptic.org/proxy-protocol
annotation, which makes HAProxy send a PROXY header to a backend.
Full HAProxy Service reference¶
haproxy:
enabled: true
ports:
http: 80 # HAProxy container HTTP bind
https: 443 # HAProxy container HTTPS bind
stats: 8404 # Stats/health page
dataplane: 5555 # HAPTIC agent
service:
type: NodePort # ClusterIP, NodePort, or LoadBalancer
annotations: {}
loadBalancerIP: ""
loadBalancerSourceRanges: []
externalTrafficPolicy: "" # Cluster | Local
http:
port: 80
nodePort: 30080 # Only honored for NodePort/LoadBalancer
https:
port: 443
nodePort: 30443
stats:
port: 8404
nodePort: 30404
Replicas and autoscaling¶
The chart runs 2 HAProxy replicas by default. Set haproxy.replicaCount to change the fixed count:
For traffic-driven autoscaling, enable KEDA under haproxy.keda. When haproxy.keda.enabled is true, the chart creates a ScaledObject and stops writing a fixed replicas onto the Deployment (KEDA owns it), scaling between minReplicaCount and maxReplicaCount from the triggers you define:
haproxy:
keda:
enabled: true
minReplicaCount: 2
maxReplicaCount: 10
triggers:
- type: cpu
metricType: Utilization
metadata:
value: "70"
KEDA must be installed in the cluster, and haproxy.keda.triggers must list at least one trigger — it's empty by default. Any KEDA scaler works; the block above uses CPU utilization.
Initial bootstrap config¶
New HAProxy pods start with a minimal configuration from haproxy.initialConfig,
stored in the <fullname>-haproxy-config ConfigMap. For the default release name,
that's haptic-haproxy-config. The controller replaces it with your rendered
configuration.
The default keeps /healthz returning 200 on the stats port and /ready returning 503 ("waiting for controller config"), so the pod stays NotReady until the controller applies its first real config.
Once the agent receives and successfully loads the rendered configuration,
/ready returns 200 and Kubernetes adds the pod to the Service. If that first
apply fails, the agent restores the bootstrap files and the pod stays unready.
On a later container restart, HAProxy reuses the configuration on disk if it
passes haproxy -c. It falls back to the bootstrap configuration only if the
file is missing or invalid.
If you replace haproxy.initialConfig, preserve the worker socket, /healthz,
and a /ready response of 503. Returning 200 would send application traffic
to a pod before its routes exist. The value supports Helm template expressions;
changing it rolls the HAProxy pods on the next upgrade.
Access logging¶
Read, customize, and forward request logs with the access logging guide. It also covers request IDs, the Vector sidecar, and monitoring dropped records.
Pod readiness and restarts¶
Each pod runs HAProxy in master-worker mode plus the HAPTIC agent, which owns the pod's file tree and its runtime sockets. The chart supervises the SPOA hub and Vector processes inside their sidecar containers: a child exit or repeated failed health check leaves HAProxy running while the supervisor restarts only that child, with a backoff capped at 30 seconds.
HAProxy's /ready endpoint controls pod readiness. It returns 503 while the
bootstrap configuration is active and 200 once a rendered configuration runs.
The agent, SPOA hub, and Vector run as native sidecars: Kubernetes starts them
before HAProxy and stops them after HAProxy exits, preserving dependencies during
connection draining.
The agent's /readyz endpoint reports whether it can accept configuration updates;
a rejected update doesn't make it unready. Its liveness probe uses the local Unix
socket. A stopped container or a failing probe on a custom sidecar can still make
the pod unready.
The watchdog uses /usr/bin/bash and timeout, which the default images provide.
With a custom sidecar image missing either command, the supervisor logs a warning
and still restarts child processes that exit.
HAProxy Pod requirements¶
When haproxy.enabled: false, you're responsible for deploying HAProxy pods yourself. The controller discovers them via the pod selector at controller.config.podSelector, which defaults to:
controller:
config:
podSelector:
matchLabels:
app.kubernetes.io/component: loadbalancer
app.kubernetes.io/name: haptic # set dynamically by the chart
app.kubernetes.io/instance: haptic # your Helm release name
If your existing HAProxy pods don't have those exact labels, either relabel them or override controller.config.podSelector.matchLabels to match.
Each discovered pod must:
- Carry labels matching
podSelector.matchLabels - Run HAProxy in master-worker mode with a master socket the agent can reload through, and a worker
stats socketit can run runtime commands on - Run the agent in the same pod, from the HAPTIC image, sharing the config volume with HAProxy
- Expose the agent on
haproxy.ports.dataplane(default 5555), mounting its TLS identity and trusting the controller identity; see agent certificates - Run the same HAProxy major.minor series as
haproxyVersionso the controller validates configuration with the matching binary - Keep the agent available during termination with native-sidecar ordering and the drain hook used by the chart
- Provide every dependency selected by your templates, including SPOA plugins and log sockets when enabled
Start from the deployment for your installed version¶
A separate workload controller must preserve the agent's TLS mounts, shared volumes, runtime sockets, bootstrap configuration, probes, and termination ordering. Use the chart-generated deployment for your version as the reference; a minimal HAProxy-plus-agent manifest omits dependencies enabled by default.
For an existing haptic release in namespace haptic, export its deployment
for inspection:
kubectl get deployment haptic-haproxy -n haptic -o yaml > haptic-haproxy-reference.yaml
helm get manifest haptic -n haptic > haptic-release-reference.yaml
The second file includes the associated bootstrap ConfigMaps and Services.
Identity Secrets are managed separately, as described in
agent certificate management.
Set haproxy.enabled: false only once your external workload definition supplies
these dependencies and matches the configured pod selector. Helm removes its
Deployment when you disable it; plan that ownership change as a rollout.