Skip to main content

Nov 17, 2025

Migrate from ingress-nginx to the ngrok Operator

2,390 words

Topics:

Last week, the Kubernetes SIG Network and the Security Response Committee (SRC) announced the retirement of ingress-nginx. It will receive best-effort support until March 2026, at which point all maintenance will stop. The repository will be made read-only and that’s that. It is recommended you migrate away from ingress-nginx before March 2026.

We provide our own Kubernetes Operator at ngrok and have written in the past about what differentiates the ngrok Operator from others. In this post I’ll show you how to migrate from ingress-nginx to the ngrok operator.

Prerequisites

I’m going to assume you have a cluster with ingress-nginx running in it. For example, here it is in the cluster I’m using for this post.

$ kubectl -n ingress-nginx get podsNAME                                       READY   STATUS      RESTARTS   AGEingress-nginx-admission-create-dt6q4       0/1     Completed   0          37mingress-nginx-admission-patch-6j2q2        0/1     Completed   1          37mingress-nginx-controller-9cc49f96f-vcjq2   1/1     Running     0          37m

And to demonstrate that things are working correctly, I’ve created a small deployment using the testcontainers/helloworld image:

apiVersion: apps/v1kind: Deploymentmetadata:  name: helloworld  labels:    app: helloworldspec:  replicas: 1  selector:    matchLabels:      app: helloworld  template:    metadata:      labels:        app: helloworld    spec:      containers:        - name: helloworld          image: testcontainers/helloworld:1.3.0-linux          ports:            - containerPort: 8080          readinessProbe:            httpGet:              path: /ping              port: 8080            initialDelaySeconds: 5            periodSeconds: 5          livenessProbe:            httpGet:              path: /ping              port: 8080            initialDelaySeconds: 10            periodSeconds: 10---apiVersion: v1kind: Servicemetadata:  name: helloworld  labels:    app: helloworldspec:  selector:    app: helloworld  ports:    - name: http      port: 80      targetPort: 8080      protocol: TCP  type: ClusterIP---apiVersion: networking.k8s.io/v1kind: Ingressmetadata:  name: helloworldspec:  ingressClassName: nginx  rules:    - host: test.samwho.dev      http:        paths:          - path: /            pathType: Prefix            backend:              service:                name: helloworld                port:                  number: 80

If I request http://test.samwho.dev/ping using httpie, I see a PONG response:

$ http test.samwho.dev/pingHTTP/1.1 200 OKConnection: keep-aliveContent-Length: 4Content-Type: text/plain; charset=utf-8Date: Fri, 14 Nov 2025 14:28:51 GMTPONG

Sign up for ngrok

To use the ngrok Operator, sign up for an account at ngrok.com. Once you have an account, make note of your authtoken and you need to create an API key. You’ll find your authtoken in the Your Authtoken part of the dashboard. For your API key, go to the API Keys part of the dashboard and click + New API Key then follow the steps.

"Do I need to pay to use the ngrok operator?"

If you’re migrating an existing domain over to ngrok, yes, you will need to be on our pay-as-you-go plan. You can see the details on our pricing page. If you’re not migrating and want to use the free, randomly generated domain we assign to you, you won’t need to pay.

Install the ngrok Operator

The recommended way to install the ngrok Operator is through Helm. Substitute <YOUR_API_KEY> and <YOUR_AUTH_TOKEN> with your API key and authtoken from the previous step.

$ helm repo add ngrok https://charts.ngrok.com$ helm install ngrok-operator ngrok/ngrok-operator \    --namespace ngrok \    --create-namespace \    --set credentials.apiKey=<YOUR_API_KEY> \    --set credentials.authtoken=<YOUR_AUTH_TOKEN>

If all goes well you should see a message that ends like this:

=== 🎉 Installation Complete 🎉 =========================================================🎉 Thanks for installing the ngrok-operator!➡️ Check out the next-steps docs: https://ngrok.com/docs/gateway/k8s📣 Questions or feedback? Open an issue at https://github.com/ngrok/ngrok-operator/issues

And the pods should settle into the Running status:

$ kubectl -n ngrok get podsNAME                                     READY   STATUS    RESTARTS   AGEngrok-operator-agent-5cbb74c95-t4bhv     1/1     Running   0          29mngrok-operator-manager-b6c749988-8qq97   1/1     Running   0          4m38s

"Sam, why is your operator-manager pod newer than your operator-agent pod?"

Funny story. I typed out the helm install command by hand while writing this post (I don’t know what I was thinking) and I tried to --set credentials.authToken=$NGROK_AUTHTOKEN. I didn’t notice that it should actually be credentials.authtoken with a lowercase t. Took me a little while to figure out what I did wrong, and fixing it recreated the pod. Hopefully you don’t trip over the same thing!

Register a domain in ngrok

Before you create an ngrok Ingress resource, you need to register the domain you want to use in ngrok. Head over to the Domains section of the dashboard and register the domain you want to use by clicking the +New Domain button.

Note: You must already own the domain you want to use. ngrok is not a domain registrar, the registration process in ngrok is so that we know how to route traffic to you, it is not the same as buying a domain with a registrar.

I’m making the assumption here that you’re migrating in with a non-ngrok domain. In the Domains page of the ngrok dashboard, you can register both custom domains (e.g. test.samwho.dev) as well as ngrok domains (e.g. test.ngrok.io). This migration guide covers the process of migrating a non-ngrok domain into ngrok.

Without a certificate (involves downtime)

The easiest way to migrate is to register a domain and get ngrok to automatically provision a certificate for you. However, doing this will result in a short period of downtime. In my testing, this lasted about 5 minutes. This is because ngrok uses Let’s Encrypt to provision certificates, and it needs your DNS name to point at ngrok to pass the ACME HTTP01 challenge. While the certificate is being provisioned, your domain will be considered insecure and most clients/browsers will refuse to connect to it.

The next section will cover how to avoid this downtime, but the remainder of this section will focus on creating a domain with an ngrok-managed certificate.

Fill in the domain creation form with your desired domain name, and leave everything else default. Here’s what it looked like for me:

When you’ve created the domain, you’ll be prompted to set the domain’s DNS record to be a CNAME pointing at ngrok. I recommend holding off from doing this right now, because when you do it you’ll break the domain for users until you’ve created your new Ingress resource. You’ll make the DNS switch as the last step of this migration process.

With a certificate (no downtime)

To avoid any downtime, you need to create a domain in ngrok with a certificate. After migrating, you’ll be able to switch to a certificate managed by ngrok without incurring any downtime. This step is just required to avoid downtime during the migration.

To demonstrate, I’ve configured my cluster to use cert-manager and I’ve issued a certificate for test.samwho.dev with Let’s Encrypt. My helloworld Ingress resource now looks like this:

apiVersion: networking.k8s.io/v1kind: Ingressmetadata:  name: helloworld  annotations:    cert-manager.io/cluster-issuer: letsencrypt-prodspec:  ingressClassName: nginx  rules:    - host: test.samwho.dev      http:        paths:          - path: /            pathType: Prefix            backend:              service:                name: helloworld                port:                  number: 80  tls:    - hosts:        - test.samwho.dev      secretName: letsencrypt-prod

I can verify it’s setup correctly by looking at the TLS section of the Ingress resource:

$ kubectl get ingress helloworld -o jsonpath='{.spec.tls}'[{"hosts":["test.samwho.dev"],"secretName":"letsencrypt-prod"}]

ngrok ask for 2 pieces of information when setting up a domain with your own certificate: the certificate itself, and the certificate private key. I can grab both of these from the secret that cert-manager created for my Ingress, which is called letsencrypt-prod as seen above. You should treat these as sensitive information and handle them securely.

$ kubectl get secrets letsencrypt-prod -o jsonpath='{.data.tls\.crt}' | base64 -d$ kubectl get secrets letsencrypt-prod -o jsonpath='{.data.tls\.key}' | base64 -d

If you’re using a different certificate manager, the way that you get this information may vary. You will know if you have the right values because the certificate looks like this:

-----BEGIN CERTIFICATE-----<loads of gibberish>-----END CERTIFICATE-----

And the private key looks like this:

-----BEGIN PRIVATE KEY-----<loads of gibberish>-----END PRIVATE KEY-----

With these in hand, navigate to the TLS certificates section of the ngrok dashboard and click +Create TLS Certificate. You’ll see a dialog that looks like this:

Put your certificate and private key in the appropriate boxes. In the description field, I recommend putting the domain name that the certificate applies to. In my case test.samwho.dev. Click save when you’re done.

Now to register the domain. Head over to the Domains section of the ngrok dashboard and click +Create Domain. Click on the certificate dropdown and select “Upload Certificate”. You should see your certificate:

Click save, and you’ll be shown instructions for setting up the CNAME record for your domain. Don’t do this just yet. You need to set up the ngrok Ingress resource first, which we’ll do next.

Create an ngrok Ingress

Rather than updating the existing Ingress resource, you’ll create a new one next to it that uses the ngrok ingress class. Here’s the Ingress resource I used in my cluster:

apiVersion: networking.k8s.io/v1kind: Ingressmetadata:  name: helloworld-ngrokspec:  ingressClassName: ngrok  rules:    - host: test.samwho.dev      http:        paths:          - path: /            pathType: Prefix            backend:              service:                name: helloworld                port:                  number: 80

Save this as ngrok-ingress.yaml and apply it:

$ kubectl apply -f ngrok-ingress.yaml

When the ngrok Operator picks up this Ingress, it will assign it an ngrok-cname.com address like so:

$ kubectl get ingressNAME               CLASS   HOSTS             ADDRESS                                               PORTS   AGEhelloworld         nginx   test.samwho.dev   192.168.49.2                                          80      41mhelloworld-ngrok   ngrok   test.samwho.dev   2awrvpxvfbuzgneam.5nqklokassiaw4xte.ngrok-cname.com   80      20m

That 2awrvpxvfbuzgneam.5nqklokassiaw4xte.ngrok-cname.com address is my ngrok-cname.com address, yours will be a different, unique value. You’ll use it later to update your DNS record.

Troubleshooting

If you don’t see an ngrok-cname.com adress, for example your “Address” field is empty, there are a few debugging steps you can follow to figure out the problem.

Your first port of call should be the Operator logs.

$ kubectl -n ngrok get podsNAME                                      READY   STATUS    RESTARTS   AGEngrok-operator-agent-5cbb74c95-6fhnm      1/1     Running   0          2d17hngrok-operator-manager-74b889db7c-8fbbw   1/1     Running   0          2d17h
$ kubectl -n ngrok logs ngrok-operator-manager-74b889db7c-8fbbw --all-containers -f$ kubectl -n ngrok logs ngrok-operator-agent-5cbb74c95-6fhnm --all-containers -f

These logs can be quite noisy, and you’re likely to see errors like “the object has been modified” but these are a normal part of the reconciliation model. If the logs don’t yield anything useful, we also put debugging information into the ngrok Operator’s resources. For example, “agent endpoints” are the connections the Operator makes to ngrok, that ngrok later uses to send traffic to your application. If these have failed to create, you can check their status:

$ kubectl get agentendpointsNAME                          URL                       UPSTREAM URL                   BINDINGS   READY   AGEc3fe2-helloworld-default-80   https://test.samwho.dev   http://helloworld.default:80              True    28m

Then getting more detail about a specific endpoint:

$ kubectl get agentendpoint c3fe2-helloworld-default-80 -o yaml

If something has gone wrong creating the endpoint, there will be an error message about it in this output. For example, early on in this post I had accidentally installed the ngrok operator with an authtoken from my work account and an API key from my personal account. The error message I found in my agentendpoint was:

Failed to create endpoint: This domain is reserved for another account. Failed to create an endpoint with the domain 'test.samwho.dev' for the account 'Sam Rose'. If you have reserved this name, make sure that you are using an authtoken credential for the appropriate account. Reserve a name on your dashboard: https://dashboard.ngrok.com/domains/new ERR_NGROK_320

This gave me the clue I needed to realise that I had used the wrong API key. If you’re deeply stuck and can’t figure out what’s wrong, you’re welcome to file an issue and we’ll try to help you out.

Switch DNS

Now is the time to change your DNS over to point at ngrok. Every DNS registrar will have a different way to do this, but the end result should be that your domain is a CNAME record that points to your <gibberish>.ngrok-cname.com address. To find it, run:

$ kubectl get ingressNAME               CLASS   HOSTS             ADDRESS                                               PORTS     AGEhelloworld         nginx   test.samwho.dev   192.168.49.2                                          80, 443   2d20hhelloworld-ngrok   ngrok   test.samwho.dev   2awrvpxvfbuzgneam.5nqklokassiaw4xte.ngrok-cname.com   80        3h41m

Here, 2awrvpxvfbuzgneam.5nqklokassiaw4xte.ngrok-cname.com is the address I should use when setting up my CNAME. Yours will be different, but will end with .ngrok-cname.com.

For folks who have opted to have ngrok automatically manage their certificates, the moment you change over to the ngrok CNAME your clients will start seeing that your domain is insecure. This will persist until ngrok provisions a certificate for the domain, which usually takes a few minutes. You can check the status of this process by clicking on the domain in your dashboard:

When your domain looks like the above, with a certificate ID near the bottom, the downtime should stop.

For those who created a TLS certificate and attached it to the domain, clients will seamlessly transition without noticing any change.

When all has settled you will be able to send a request and get a response as expected:

$ http https://test.samwho.dev/pingHTTP/1.1 200 OKContent-Length: 4Content-Type: text/plain; charset=utf-8Date: Fri, 14 Nov 2025 17:56:36 GMTPONG

You can verify that this is going through ngrok by checking Traffic Inspector in your dashboard:

You can now clean up any old nginx Ingress resources, your traffic is flowing through ngrok. 🎉

Switch over to ngrok managed certificates

If you set up your own certificate during domain creation, you can now change to having ngrok manage your certificates for you. Head to the Domains page and switch to “Automated TLS certificates” like so:

ngrok will provision a certificate in the background, making sure to keep using your supplied certificate until the new one is ready. This ensures a seamless transition without downtime, and the job of making sure the certificate is valid and renewed is taken care of by ngrok.

Conclusion

The ngrok Operator makes it easy to manage ngrok endpoints inside Kubernetes clusters, and could be a great choice for you or your company if you’re looking to migrate from ingress-nginx.

If you have any questions about what you’ve read, or you’ve tried to migrate and you’re stuck, please visit our support page or email us at support@ngrok.com.