Skip to content

Moving from Ingress NGINX to Traefik

Caution

This page is being continuously updated as more edge cases of the NGINX Ingress Controller to Traefik migration are being discovered.

Due to the March 2026 retirement of Ingress NGINX, Welkin is adopting Traefik instead as the standard Ingress Controller. We are making the transition as smooth as possible for both Application Developers and Platform Administrators.

Welkin Clusters will run both Ingress NGINX and Traefik during a transitional period to make sure environments have the opportunity to safely migrate. As an application developer, you can start testing on Traefik today by stepwise adapting your services with just a few changes at a time.

Prerequisites

Confirm both controllers are operational.

kubectl get pods -n ingress-nginx
kubectl get pods -n traefik

You should be able to resolve the *.traefik.$DOMAIN to the Traefik LoadBalancer IP using for example dig.

export TRAEFIK_EXTERNAL_LB=$(dig "*.traefik.$DOMAIN" +short)

The Migration Strategy

To guarantee service continuity, we utilize a side-by-side validation strategy, where Traefik is configured to shadow your existing NGINX Ingress resources. By running both controllers simultaneously, you are able to verify your services in isolation before redirecting any production traffic.

Clone and Patch

For all Ingress resources, follow these steps to migrate from NGINX to Traefik:

  1. Create a copy of the existing Ingress resource.
  2. Update the ingressClassName to traefik.
  3. Replace NGINX-specific annotations or configuration snippets with the corresponding Traefik Middlewares.

Note

Traefik natively supports all standard Kubernetes Ingress fields.

If your Ingress uses only standard fields like paths and hosts without custom annotations, simply changing the ingressClassName is often sufficient.

Please read "Converting Annotations to Traefik Middlewares" for information about annotation support and instructions on how to create appropriate Traefik Middlewares.

Validation

You can verify that your services are running correctly on Traefik by sending a request directly to the Traefik LoadBalancer IP. This allows you to test the migration in isolation while your production DNS continues to point safely to NGINX.

Option 1: Client-Side Resolution

You can test routing without changing DNS by running:

curl --resolve example.base-domain.com:443:$TRAEFIK_EXTERNAL_LB https://example.base-domain.com

Alternatively, by updating your local /etc/hosts file to point your domain to the $TRAEFIK_EXTERNAL_LB IP.

This allows you to test the application in a browser on standard ports.

Option 2: Separate DNS

You can create separate Ingress objects and DNS pointing to the $TRAEFIK_EXTERNAL_LB IP.

The *.traefik.$DOMAIN DNS record pointing to the Traefik LoadBalancer can also be used directly.

The Permanent Switch

When validation is successful update your public DNS records to point to the Traefik LoadBalancer IP. Once propagation is complete, delete the old NGINX Ingress.

Converting Annotations to Traefik Middlewares

Traefik provides support for translating many common Ingress-NGINX annotations when using the nginx Ingress class name, though some may behave slightly differently.

However, not all annotations are supported. Some require additional steps and need to be converted to Traefik Middlewares. For more details on which annotations are supported or unsupported, see Traefik & Ingresses with NGINX annotations.

Caution

Due to a limitation in how Traefik's translation between Ingress NGINX and its own configuration format works: if you continue to use Ingress-NGINX annotations, you cannot use Traefik Middlewares on the same resource. We therefore recommend a full conversion to Traefik Middlewares to avoid configuration issues.

In the following sections below are step-by-step guides to help you convert various types of annotations into their Traefik Middleware equivalents.

Applying Middleware

To apply any Middleware to a Kubernetes Ingress resource, you add an annotation in the format traefik.ingress.kubernetes.io/router.middlewares: <namespace>-<middleware-name>@kubernetescrd.

In the annotation, you specifify the namespace and the name of the Middleware.

# Create a Middleware resource
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: request-headers
  namespace: prod
spec:
  headers:
    customRequestHeaders:
      X-Custom-Request-Added-Header: "test-request"
---
# Specify the namespace and the name of the Middleware in the annotation
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: demo-app
  namespace: prod
  annotations:
    traefik.ingress.kubernetes.io/router.middlewares: prod-request-headers@kubernetescrd
spec:
  ingressClassName: traefik

Rate-limiting and allowlisting

For rate-limiting and allowlisting -- which need a conversion from NGINX annotation to Traefik Middleware -- please find an example in our documentation on Network Model.

Here the allowlist annotation nginx.ingress.kubernetes.io/whitelist-source-range: 98.128.193.2/32 is currently not a supported annotation. To keep the allow-list feature a Middleware resource is created:

# Blocklisted IP's will get HTTP 403.
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: demo-allowlist
  namespace: <namespace>
spec:
  ipAllowList:
    sourceRange:
      - 98.128.193.2/32

And the Traefik-specific Middleware annotation is added to the Ingress resource:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: demo-app
  annotations:
    traefik.ingress.kubernetes.io/router.middlewares: <namespace>-demo-allowlist@kubernetescrd
spec:
  ingressClassName: traefik
...

Header manipulation

Traefik's Headers Middleware can handle several types of header manipulation. Depending on your use-case, it can replace or complement the following annotations (non-exhaustive list):

  • nginx.ingress.kubernetes.io/custom-headers
  • nginx.ingress.kubernetes.io/enable-cors
  • nginx.ingress.kubernetes.io/cors-allow-origin
  • nginx.ingress.kubernetes.io/cors-allow-headers

The Headers Middleware can also do things that in Ingress-NGINX is normally handled with configuration-snippet, such as adding custom response headers, or removing headers.

For example, here's how a custom request header can be added by the controller:

---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: headers-request-added
spec:
  headers:
    customRequestHeaders:
      X-Custom-Request-Added-Header: "test-request"

Then, annotate it on the Ingress resource:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: demo-app
  annotations:
    traefik.ingress.kubernetes.io/router.middlewares: <namespace>-headers-request-added@kubernetescrd
spec:
  ingressClassName: traefik
...

If we instead want to configure it to manage CORS, it can be configured in the following fashion:

apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: cors-config
  namespace:
spec:
  headers:
    accessControlAllowMethods:
      - GET
      - OPTIONS
      - POST
      - PUT
      - DELETE
    accessControlAllowOriginList:
      - "https://app.example.com"
      - "https://admin.example.com"
    accessControlAllowHeaders:
      - "*"
    accessControlAllowCredentials: true
    accessControlMaxAge: 100
    addVaryHeader: true

Then apply it to your Ingress:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: demo-app
  annotations:
    traefik.ingress.kubernetes.io/router.middlewares: -cors-config@kubernetescrd
spec:
  ingressClassName: traefik
...

Response Compression

Traefik's Compress Middleware is used to compress responses (using Zstandard, Brotli or Gzip) before sending them to the client. This replaces the common Ingress-NGINX pattern of using a configuration-snippet to inject manual compression directives:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: nginx-compression-example
  annotations:
    nginx.ingress.kubernetes.io/configuration-snippet: |
      gzip on;
      gzip_min_length 2048;

In Traefik, this can be handled by adding compress to a Middleware resource. Adding compress to a Middleware by default adds support for Gzip, Brotli and Zstandard compression with Gzip having highest priority.

The following example demonstrates a basic Middleware configuration with a minimum byte threshold:

apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: traefik-compression-example
spec:
  compress:
    minResponseBodyBytes: 2048 # Equivalent to gzip_min_length

and then adding the annotation to the Ingress resource:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: demo-app
  annotations:
    traefik.ingress.kubernetes.io/router.middlewares: <namespace>-traefik-compression-example@kubernetescrd
spec:
  ingressClassName: traefik
...

Path manipulation

Traefik uses specific Path Middlewares to handle URL modifications that are typically managed by rewrite annotations in NGINX. These Middlewares offer more granular control over how paths are modified before being sent to the backend service.

Common NGINX annotations that map to these Middlewares include:

  • nginx.ingress.kubernetes.io/rewrite-target
  • nginx.ingress.kubernetes.io/app-root
  • nginx.ingress.kubernetes.io/configuration-snippet (when used for path rewrites)

Add Prefix

If you need to prepend a path to the request (similar to NGINX path modification), use the AddPrefix Middleware:

apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: addprefix-foo
spec:
  addPrefix:
    prefix: /foo

Replace Path

To completely replace the path of a request, use the ReplacePath Middleware:

apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: replacepath-foo
spec:
  replacePath:
    path: /foo

For complex rewrites equivalent to nginx.ingress.kubernetes.io/rewrite-target, use regex-based replacement:

apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: replacepathregex-foo
spec:
  replacePathRegex:
    regex: "^/foo/(.*)"
    replacement: "/bar/$1"

Strip Prefix

To remove a specific prefix from the path before it reaches the backend—a common requirement when the application is not "path-aware"—use StripPrefix:

apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: stripprefix-foo
spec:
  stripPrefix:
    prefixes:
      - /foo

Alternatively, for dynamic path stripping based on patterns, use StripPrefixRegex:

apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: stripprefixregex-foo
spec:
  stripPrefixRegex:
    regex:
      - "/foo/[a-z0-9]+/[0-9]+/"

Error handling and redirects

The Errors, RedirectScheme and RedirectRegex Middlewares are used to handle error responses and redirects.

It can replace or complement the following annotations (non-exhaustive list):

  • nginx.ingress.kubernetes.io/custom-http-errors:
  • nginx.ingress.kubernetes.io/default-backend:
  • nginx.ingress.kubernetes.io/ssl-redirect:
  • nginx.ingress.kubernetes.io/force-ssl-redirect:
  • nginx.ingress.kubernetes.io/permanent-redirect:
  • nginx.ingress.kubernetes.io/permanent-redirect-code:
  • nginx.ingress.kubernetes.io/temporal-redirect:
  • nginx.ingress.kubernetes.io/temporal-redirect-code:

In some cases, these Middlewares can also replace functionality implemented via configuration-snippet annotations.

Errors

If you need to present a custom page when an error occurs, use the Errors Middleware:

apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: errors
spec:
  errors:
    status:
      - "502"
    query: /
    service:
      name: nginx
      port: 80

Redirect Regex

If you need to create a permanent or temporary redirect, use the RedirectRegex Middleware:

apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: redirect-regex
spec:
  redirectRegex:
    regex: ^https?://domain\.com/(.*)
    replacement: https://example.com/${1}
    permanent: true

Redirect Scheme

If you need to redirect scheme (HTTP -> HTTPS), then use RedirectScheme Middleware:

apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: redirect-scheme
spec:
  redirectScheme:
    scheme: https
    permanent: true

Sticky sessions (Session Affinity)

Sticky sessions, or session affinity, is supported by Traefik. More details about how it works can be found here.

Sticky sessions can replace the following Ingress-NGINX annotations:

  • nginx.ingress.kubernetes.io/session-cookie-max-age:
  • nginx.ingress.kubernetes.io/session-cookie-secure:

To ensure that the sticky session cookie has a certain Max-Age and is only sent over TLS, the following annotations need to be set on the Service object.

Note

For sticky sessions, the annotations need to be set on the backend Service object, not the Ingress!

apiVersion: v1
kind: Service
metadata:
  name: example-service
  annotations:
    traefik.ingress.kubernetes.io/service.sticky.cookie.secure: "true"
    traefik.ingress.kubernetes.io/service.sticky.cookie.maxage: "600"
spec:
...

TLS Options

TLSOption objects can be used to customize TLS settings for ingresses.

Caution

Normally, there's no need to modify the TLS options for Traefik, since the defaults provided by Welkin is secure by default. You can read more about the defaults here. If you manually modify the settings, you may reduce the security of your HTTPS traffic!

If you need to modify the settings, similar to the Ingress-NGINX annotation nginx.ingress.kubernetes.io/ssl-ciphers:, you need to create a TLSOption.

As an example, if you want to enforce only TLSv1.3, you can use the following:

apiVersion: traefik.io/v1alpha1
kind: TLSOption
metadata:
  name: very-modern-tls
spec:
  minVersion: VersionTLS13

Then, annotate it on the Ingress resource:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: demo-app
  annotations:
    traefik.ingress.kubernetes.io/router.tls.options=<namespace>-very-modern-tls@kubernetescrd
spec:
  ingressClassName: traefik
...

External Authentication

Traefik's ForwardAuth Middleware replaces the nginx.ingress.kubernetes.io/auth-url annotation. This Middleware forwards authentication to an external service before allowing requests to reach your application.

Here's an example ForwardAuth Middleware configuration:

apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: auth-service
  namespace:
spec:
  forwardAuth:
    address: https://auth.example.com/verify
    trustForwardHeader: true
    authResponseHeaders:
      - X-Auth-User
      - X-Auth-Email

Then apply it to your Ingress:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: demo-app
  annotations:
    traefik.ingress.kubernetes.io/router.middlewares: -auth-service@kubernetescrd
spec:
  ingressClassName: traefik
...

Metrics

Ingress-NGINX Metric Traefik equivalent Notes
nginx_ingress_controller_requests traefik_service_requests_total Filter by code instead of status.
nginx_ingress_controller_request_duration_seconds traefik_service_request_duration_seconds Warning: Histogram buckets differ by default. (See longer explanation below)
nginx_ingress_controller_response_duration_seconds traefik_service_request_duration_seconds Partial: Traefik bundles "wait + processing" into a single metric. It does not separate "upstream time" from "total time" in metrics. (See longer explanation below)
nginx_ingress_controller_header_duration_seconds MISSING
nginx_ingress_controller_connect_duration_seconds MISSING
nginx_ingress_controller_response_size MISSING (Histogram) Traefik provides a Counter (traefik_service_responses_bytes_total) but no Histogram. You can track bandwidth, but not "size distribution."
nginx_ingress_controller_request_size MISSING (Histogram) Traefik provides a Counter (traefik_service_requests_bytes_total) but no Histogram.
nginx_ingress_controller_bytes_sent traefik_service_responses_bytes_total
nginx_ingress_controller_nginx_process_connections traefik_open_connections
nginx_ingress_controller_nginx_process_connections_total traefik_entrypoint_requests_total
nginx_ingress_controller_nginx_process_cpu_seconds_total process_cpu_seconds_total
nginx_ingress_controller_nginx_process_num_procs go_goroutines NGINX is multi-process (workers), Traefik is single-process (Goroutines).
nginx_ingress_controller_nginx_process_oldest_start_time_seconds process_start_time_seconds
nginx_ingress_controller_nginx_process_read_bytes_total traefik_entrypoint_requests_bytes_total
nginx_ingress_controller_nginx_process_requests_total traefik_entrypoint_requests_total
nginx_ingress_controller_nginx_process_resident_memory_bytes process_resident_memory_bytes
nginx_ingress_controller_nginx_process_virtual_memory_bytes process_virtual_memory_bytes
nginx_ingress_controller_nginx_process_write_bytes_total traefik_entrypoint_responses_bytes_total
nginx_ingress_controller_build_info MISSING Traefik does not output a specific metric for this. Could maybe use kube_pod_container_info instead.
nginx_ingress_controller_check_success MISSING Traefik lacks the this metric because it updates routing rules dynamically in memory, whereas NGINX must validate a static configuration file before every reload.
nginx_ingress_controller_config_hash MISSING Traefik does not expose a config hash. Use traefik_config_last_reload_success to verify sync timestamp.
nginx_ingress_controller_config_last_reload_successful MISSING
nginx_ingress_controller_config_last_reload_successful_timestamp_seconds traefik_config_last_reload_success Timestamp of last successful config update.
nginx_ingress_controller_ssl_certificate_info traefik_tls_certs_not_after Like NGINX, Traefik puts the cert details (CN, Issuer) in the labels. However, the value Differs, Traefik is the expiry timestamp and NGINX is 1.
nginx_ingress_controller_success MISSING traefik_config_reloads_total could possibly be used to prove the controller is active but they are not counting the same thing.
nginx_ingress_controller_orphan_ingress MISSING
nginx_ingress_controller_admission_config_size MISSING
nginx_ingress_controller_admission_render_duration MISSING
nginx_ingress_controller_admission_render_ingresses MISSING
nginx_ingress_controller_admission_roundtrip_duration MISSING
nginx_ingress_controller_admission_tested_duration MISSING
nginx_ingress_controller_admission_tested_ingresses MISSING

Troubleshooting

See Traefik's official migration docs for more troubleshooting advice.