This page shows how to run a reverse proxy such as Traefik or nginx directly on your nodes with no Hetzner load balancer in front. For the usual setup with a load balancer, see [Install Traefik](/docs/hetzner/apalla/network/expose/install-traefik). By default, every Syself node runs a default-deny host firewall. How you deploy the proxy decides whether you have to open ports in it yourself. With `hostPort` you do not. With `hostNetwork` you do, and the policy has to be written correctly or part of your traffic is dropped without an error. ## What changes without a load balancer Without a load balancer you own the address, the health checking, and the failover. Clients reach the nodes directly, and every node listed in DNS has to serve the request on its own. - Nothing checks health for you, so a failed node keeps receiving traffic until you change DNS. Run the proxy on more than one node and give it a PodDisruptionBudget, as in [Run a production-ready workload](/docs/hetzner/apalla/workloads/production/run-a-production-ready-workload). - A bare-metal node keeps its address when it is reprovisioned. A cloud node gets a new one, so update its `A` record. - The proxy does see the client's real address, because nothing sits in front to rewrite it, so there is no PROXY protocol to set up. Your backends read the client from `X-Forwarded-For` as they would behind any proxy. See [Preserve the client source IP](/docs/hetzner/apalla/network/load-balancing/preserve-client-source-ip). ## Run the proxy with hostPort Leave `hostNetwork` off, which is the default, and publish 80 and 443 with `hostPort`. The node answers on both ports, and Cilium hands each request to the proxy's pod before the [host firewall](/docs/hetzner/apalla/security/ports-and-listeners) checks it, so no policy is required. ```yaml title="traefik-values.yaml" service: enabled: false ports: web: hostPort: 80 websecure: hostPort: 443 nodeSelector: node.example.com/ingress: "true" ``` Set that label on the worker pool rather than with `kubectl label`, so nodes still carry it after they are replaced. See [Label nodes and assign roles](/docs/hetzner/apalla/servers-and-nodes/labels/label-nodes-and-assign-roles). ## If you need hostNetwork instead Some proxies run with `hostNetwork: true` to reach a network only the node can see. The request then ends at the node, where the host firewall applies and denies every port it is not configured to allow. Allow the ports yourself: ```yaml title="allow-ingress-proxy.yaml" apiVersion: cilium.io/v2 kind: CiliumClusterwideNetworkPolicy metadata: name: allow-ingress-proxy spec: nodeSelector: matchLabels: node.example.com/ingress: "true" ingress: - fromEntities: [world] toPorts: - ports: - {port: "80", protocol: TCP} - {port: "443", protocol: TCP} ``` The ports in the rule are the ones the proxy binds. `world` covers a client reaching the node directly, which is the whole path on this page. A Service in front of the proxy changes that, so add `remote-node` as well. The node that receives the request may forward it to a backend on another node. It replaces the client's address with its own, so the second node sees the request coming from a node rather than from the internet. Leave `remote-node` out and that traffic is dropped silently. A Service also opens a NodePort on every node, but the rule does not use that number. Cilium rewrites the NodePort to the proxy's port before the host firewall sees the packet, so the rule still lists the proxy's port. The policy is yours to keep. The Syself platform restores its own policies (`base-rule-set`, `control-plane-rule-set`, `ssh-rule-set`), so never edit those or reuse their names. See [Segment with network policies](/docs/hetzner/apalla/security/segment-with-network-policies). > [!NOTE] > A `hostNetwork` pod binds its ports in the node's namespace, where anything below 1024 is privileged. Most proxy images run as non-root, so they cannot bind 80 or 443 there. A chart that drops all capabilities strips `NET_BIND_SERVICE` even from root, so grant it back. For the Traefik chart, override the ports and set both security contexts. `podSecurityContext` runs the pod as root, and `securityContext` adds the capability back to the container: ```yaml title="traefik-values.yaml" hostNetwork: true ports: web: port: 80 websecure: port: 443 podSecurityContext: runAsUser: 0 runAsGroup: 0 runAsNonRoot: false securityContext: capabilities: drop: [ALL] add: [NET_BIND_SERVICE] ``` Traefik's own defaults are 8000 and 8443, which are above 1024 and need none of these settings. Override them only when the node itself has to answer on 80 and 443. ## Verify it From outside the cluster, request each node IP you published: ```console $ curl -sS -o /dev/null -w '%{http_code}\n' http:/// 200 ``` Any answer from the proxy means the path works, including a `404` before you add a route. A timeout means the host firewall dropped the packet. A refused connection means nothing was listening on that port. [Troubleshoot load balancers and Services](/docs/hetzner/apalla/network/debug/troubleshoot-load-balancers-and-services) shows how to read the drops. ## Related - [Choose how to expose an application](/docs/hetzner/apalla/network/expose/choose-how-to-expose), the options with a load balancer - [Install Traefik](/docs/hetzner/apalla/network/expose/install-traefik), the same proxy behind a load balancer - [Networking and Cilium](/docs/hetzner/apalla/concepts/internals/networking), the datapath and the two network layers