Configuring Public and Private Services with Two Traefik Instances

Updated on 22 May 2026

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

Two independent Traefik instances run in the cluster:

  • traefik-public processes only Ingress resources with ingressClassName: public
  • traefik-private processes only Ingress resources with ingressClassName: 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

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

Both Traefik instances are deployed from the same Helm chart, but with separate values files.

Public Traefik configuration

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: true

This 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: public sets the class name that public Ingress resources will reference
  • providers.kubernetesCRD.ingressClass and providers.kubernetesIngress.ingressClass restrict Traefik to resources of its own class
  • publishedService.enabled: true enables correct publishing of the ingress service address

Private Traefik configuration

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: true

This 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

Add the Traefik Helm repository:

helm repo add traefik https://helm.traefik.io/traefik
helm repo update

Deploy the public instance:

helm install traefik-public traefik/traefik \
  -n traefik-public --create-namespace \
  -f traefik-public-values.yaml

Deploy the private instance:

helm install traefik-public traefik/traefik \
  -n traefik-public --create-namespace \
  -f traefik-public-values.yaml

Once both are installed, check that the LoadBalancer services have been created:

kubectl get svc -n traefik-public
kubectl get svc -n traefik-private

Load balancer provisioning can take up to 10 minutes. The EXTERNAL-IP column may show pending while the resource is being created.

Expected behavior differs between the two:

  • traefik-public should receive an external IP address
  • traefik-private will 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 control panel. Wait until both have finished provisioning before proceeding.

At this stage, also confirm that both ingress classes are registered in the cluster:

kubectl get ingressclass

The output should list both public and private.

Routing Traffic to Public and Private Services

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: public

For a private service:

spec:
  ingressClassName: private

All traffic is then routed through the corresponding ingress controller automatically.

Practical Example

The following manifests demonstrate the full setup. The example includes:

  • Two public services: service1 and service2
  • 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: 80

Public 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: 80

Private 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: 80

Public 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: 80

Note 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: 80

Here 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-example

Apply 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.yaml

Verify that deployments, services, and ingress resources were created:

kubectl get deploy,svc,ingress -n ingress-example

You 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 to service1
  • Requests to ingress1.example.com/service2... are routed to service2
  • Requests to ingress2.example.com/ are routed to service3, accessible only from within the private network via the internal Traefik load balancer

Verify the Setup

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-example

Confirm 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.

The example uses ingress1.example.com and ingress2.example.com as placeholder hostnames. You can use them with curl --resolve without configuring real DNS. If you want to open the public ingress in a browser, replace ingress1.example.com with a real domain and configure a DNS record pointing to the public load balancer IP.

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_IP

Where 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_IP

Where 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.

Was this page helpful?
Updated on 22 May 2026

Do you have questions,
comments, or concerns?

Our professionals are available to assist you at any moment,
whether you need help or are just unsure of where to start.
Email us
Hostman's Support