Configuring Public and Private Services with Two Traefik Instances
When running a Kubernetes cluster that hosts both internet-facing and internal services, the cleanest approach is to split incoming traffic at the ingress controller level. You can do this by deploying two separate Traefik instances: one that handles public ingress, and one that handles private ingress.
This guide walks through that setup. You will deploy two Helm releases of Traefik with different IngressClasses, then route traffic to public and internal services accordingly.
How It Works Copy link
Two independent Traefik instances run in the cluster:
traefik-publicprocesses only Ingress resources withingressClassName: publictraefik-privateprocesses only Ingress resources withingressClassName: private
Each instance creates its own LoadBalancer service with different parameters:
- The public Traefik receives an external IP and accepts traffic from the internet
- The private Traefik creates an internal load balancer with no public IP, accessible only within the private network
This lets you use a single cluster for both external and internal applications without mixing their entry points.
Prerequisites Copy link
Before you begin, make sure you have:
- helm installed
- A running Kubernetes cluster
- Access to the cluster's private network for testing the private ingress (for example, via a separate cloud server in the same network)
Configure Traefik Copy link
Both Traefik instances are deployed from the same Helm chart, but with separate values files.
Public Traefik configuration Copy link
Create a file named traefik-public-values.yaml with the following content:
fullnameOverride: traefik-public
ingressClass:
enabled: true
isDefaultClass: false
name: public
service:
enabled: true
type: LoadBalancer
providers:
kubernetesCRD:
enabled: true
allowCrossNamespace: false
ingressClass: public
kubernetesIngress:
enabled: true
ingressClass: public
publishedService:
enabled: trueThis configures the public Traefik instance to handle ingress resources of the public class and create a standard LoadBalancer with an external IP.
Key settings to note:
ingressClass.name: publicsets the class name that public Ingress resources will referenceproviders.kubernetesCRD.ingressClassandproviders.kubernetesIngress.ingressClassrestrict Traefik to resources of its own classpublishedService.enabled: trueenables correct publishing of the ingress service address
Private Traefik configuration Copy link
Create a file named traefik-private-values.yaml with the following content:
fullnameOverride: traefik-private
ingressClass:
enabled: true
isDefaultClass: false
name: private
service:
enabled: true
type: LoadBalancer
annotations:
k8s.hostman.com/attached-loadbalancer-no-external-ip: "true"
providers:
kubernetesCRD:
enabled: true
allowCrossNamespace: false
ingressClass: private
kubernetesIngress:
enabled: true
ingressClass: private
publishedService:
enabled: trueThis configures the private Traefik instance to handle ingress resources of the private class and create an internal load balancer with no public IP.
The key parameter here is k8s.hostman.com/attached-loadbalancer-no-external-ip: "true", which tells the platform to provision an internal load balancer without a public IP address.
Install Both Traefik Instances Copy link
Add the Traefik Helm repository:
helm repo add traefik https://helm.traefik.io/traefik
helm repo updateDeploy the public instance:
helm install traefik-public traefik/traefik \
-n traefik-public --create-namespace \
-f traefik-public-values.yamlDeploy the private instance:
helm install traefik-public traefik/traefik \
-n traefik-public --create-namespace \
-f traefik-public-values.yamlOnce both are installed, check that the LoadBalancer services have been created:
kubectl get svc -n traefik-public
kubectl get svc -n traefik-privateExpected behavior differs between the two:
traefik-publicshould receive an external IP addresstraefik-privatewill not receive an external IP, since it uses an internal load balancer
You can also check the status of both load balancers from the Hostman dashboard. Wait until both have finished provisioning before proceeding.
At this stage, also confirm that both ingress classes are registered in the cluster:
kubectl get ingressclassThe output should list both public and private.
Routing Traffic to Public and Private Services Copy link
Once both Traefik instances are running, routing is as simple as setting the appropriate ingressClassName in each Ingress manifest.
For a public service:
spec:
ingressClassName: publicFor a private service:
spec:
ingressClassName: privateAll traffic is then routed through the corresponding ingress controller automatically.
Practical Example Copy link
The following manifests demonstrate the full setup. The example includes:
- Two public services:
service1andservice2 - One private service:
service3 - Two Ingress resources with different
IngressClasses
ConfigMap with test HTML pages
Create config-map.yaml:
apiVersion: v1
kind: ConfigMap
metadata:
name: service-config
namespace: ingress-example
data:
service1.html: |
<html>
<head><title>Service 1</title></head>
<body><h1>Welcome to Service 1!</h1></body>
</html>
service2.html: |
<html>
<head><title>Service 2</title></head>
<body><h1>Welcome to Service 2!</h1></body>
</html>
service3.html: |
<html>
<head><title>Service 3</title></head>
<body><h1>Welcome to Service 3!</h1></body>
</html>Public service1
Create service1-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: service1
namespace: ingress-example
spec:
replicas: 2
selector:
matchLabels:
app: service1
template:
metadata:
labels:
app: service1
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
volumeMounts:
- name: config-volume
mountPath: /usr/share/nginx/html
volumes:
- name: config-volume
configMap:
name: service-config
items:
- key: service1.html
path: service1.html
---
apiVersion: v1
kind: Service
metadata:
name: service1
namespace: ingress-example
spec:
selector:
app: service1
ports:
- protocol: TCP
port: 80
targetPort: 80Public service2
Create service2-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: service2
namespace: ingress-example
spec:
replicas: 2
selector:
matchLabels:
app: service2
template:
metadata:
labels:
app: service2
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
volumeMounts:
- name: config-volume
mountPath: /usr/share/nginx/html
volumes:
- name: config-volume
configMap:
name: service-config
items:
- key: service2.html
path: service2.html
---
apiVersion: v1
kind: Service
metadata:
name: service2
namespace: ingress-example
spec:
selector:
app: service2
ports:
- protocol: TCP
port: 80
targetPort: 80Private service3
Create service3-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: service3
namespace: ingress-example
spec:
replicas: 2
selector:
matchLabels:
app: service3
template:
metadata:
labels:
app: service3
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
volumeMounts:
- name: config-volume
mountPath: /usr/share/nginx/html
volumes:
- name: config-volume
configMap:
name: service-config
items:
- key: service3.html
path: index.html
---
apiVersion: v1
kind: Service
metadata:
name: service3
namespace: ingress-example
spec:
selector:
app: service3
ports:
- protocol: TCP
port: 80
targetPort: 80Public ingress
Create ingress-public.yaml:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: example-public-ingress
namespace: ingress-example
spec:
ingressClassName: public
rules:
- host: ingress1.example.com
http:
paths:
- path: /service1
pathType: Prefix
backend:
service:
name: service1
port:
number: 80
- path: /service2
pathType: Prefix
backend:
service:
name: service2
port:
number: 80Note that spec.ingressClassName is set to public, which means this Ingress will be handled by the public ingress controller.
Private ingress
Create ingress-private.yaml:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: example-private-ingress
namespace: ingress-example
spec:
ingressClassName: private
rules:
- host: ingress2.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: service3
port:
number: 80Here spec.ingressClassName is set to private, so this Ingress will be handled by the private ingress controller.
Apply the manifests
Create the ingress-example namespace:
kubectl create namespace ingress-exampleApply all manifests:
kubectl apply -f config-map.yaml
kubectl apply -f service1-deployment.yaml
kubectl apply -f service2-deployment.yaml
kubectl apply -f service3-deployment.yaml
kubectl apply -f ingress-public.yaml
kubectl apply -f ingress-private.yamlVerify that deployments, services, and ingress resources were created:
kubectl get deploy,svc,ingress -n ingress-exampleYou should see three services, three deployments, and two ingress resources. If anything is missing, the corresponding manifest either was not applied or contains an error.
With this setup:
- Requests to
ingress1.example.com/service1...are routed toservice1 - Requests to
ingress1.example.com/service2...are routed toservice2 - Requests to
ingress2.example.com/are routed toservice3, accessible only from within the private network via the internal Traefik load balancer
Verify the Setup Copy link
Before sending test requests, check that each ingress has the correct class assigned:
kubectl describe ingress example-public-ingress -n ingress-example
kubectl describe ingress example-private-ingress -n ingress-exampleConfirm that example-public-ingress shows Ingress Class: public and example-private-ingress shows Ingress Class: private. If the class does not match, Traefik will not process that resource.
Once the public load balancer has an external IP, test the routes:
curl http://ingress1.example.com/service1.html \
--resolve ingress1.example.com:80:PUBLIC_LB_EXTERNAL_IP
curl http://ingress1.example.com/service2.html \
--resolve ingress1.example.com:80:PUBLIC_LB_EXTERNAL_IPWhere PUBLIC_LB_EXTERNAL_IP is the external IP assigned to the public load balancer.
If you receive a 404, double-check that you are using the hostname ingress1.example.com and the paths /service1 or /service2.
To test the private service, run the following from a machine inside the same private network as the cluster:
curl http://ingress2.example.com/ \
--resolve ingress2.example.com:80:PRIVATE_LB_IPWhere PRIVATE_LB_IP is the private IP of the internal load balancer.
You should receive an HTML page served by service3.
If the private route does not respond, confirm that the request is coming from within the private network. External internet traffic cannot reach this ingress, even if the cluster resources are configured correctly.