By Joe Keegan and Anish Singh Walia

The Ingress NGINX controller is being deprecated. This guide provides a step-by-step process to migrate to the Gateway API on DigitalOcean Kubernetes (DOKS) with Cilium, ensuring zero downtime for your workloads. You’ll learn how to handle TLS certificates, safely switch over DNS, and configure the DigitalOcean LoadBalancer for the new gateway. The process allows you to run both Ingress and Gateway side by side, giving you time to validate production readiness before DNS cutover.
If you’re unfamiliar with Gateway concepts or best practices, review our tutorials on Gateway API with Cilium and HTTPS Traffic Routing. These resources will help you understand the new API model and configuration patterns, and highlight important differences from the traditional Ingress approach. Planning and proper testing are crucial for a smooth migration.
Important: This guide is tested on DOKS version 1.33.x. If you’re on an older version, upgrade your cluster before attempting migration.
spec.infrastructure.annotations in your Gateway resources, not metadata.annotations.Certificate resources. Cert-manager’s solver must be configured for Gateway, not Ingress.RequestRedirect filter.kubectl get gatewayclass cilium shows ACCEPTED: True)external-dns namespaceGateway creates a new DigitalOcean LoadBalancer with a new IP address.
Solution: Run both LoadBalancers simultaneously, switch DNS to the new Gateway LoadBalancer, then remove the old Ingress LoadBalancer after validation.
| Ingress NGINX | Gateway API Location |
|---|---|
kubernetes.io/ingress.class: nginx |
spec.gatewayClassName: cilium |
cert-manager.io/cluster-issuer |
Explicit Certificate resource |
external-dns.alpha.kubernetes.io/* |
HTTPRoute metadata.annotations |
nginx.ingress.kubernetes.io/force-ssl-redirect |
Separate HTTPRoute with redirect filter |
service.beta.kubernetes.io/do-loadbalancer-* |
Gateway spec.infrastructure.annotations |
DigitalOcean LoadBalancer annotations must be in spec.infrastructure.annotations, not metadata.annotations. This is a common mistake that prevents the LoadBalancer from being created correctly. See the Gateway API Infrastructure documentation for details on infrastructure annotations.
gatewayHTTPRoute solver instead of ingress solverGateway API requires a separate HTTPRoute resource with RequestRedirect filter (no annotation).
Blue-green approach: Deploy Gateway alongside Ingress, test via IP, execute DNS cutover, monitor for stability, then cleanup. Both systems run concurrently for zero downtime. You’ll pay for two LoadBalancers temporarily (typically 24-48 hours). This approach ensures you can validate the Gateway configuration before switching production traffic.
To enable cert-manager to work with the Gateway API, you need to configure it to support certificate issuance for Gateway-managed routes. The following Helm upgrade command updates your cert-manager installation with the required flag:
helm upgrade cert-manager jetstack/cert-manager \
--namespace cert-manager \
--reuse-values \
--set extraArgs="{--enable-gateway-api=true}"
What this does:
helm upgrade cert-manager jetstack/cert-manager: Upgrades (or installs) cert-manager using the Jetstack Helm chart.--namespace cert-manager: Targets the cert-manager namespace.--reuse-values: Keeps your existing cert-manager configuration; only new settings are changed.--set extraArgs="{--enable-gateway-api=true}": Adds an argument to cert-manager so it recognizes and manages Gateway API resources, not just traditional Kubernetes Ingress resources.Make sure to include this flag alongside any existing extraArgs you have set previously. This step is required for cert-manager to issue certificates to Gateway-managed HTTPS routes.
The following gateway.yaml example demonstrates how to define a Kubernetes Gateway resource for the Gateway API. Here’s what each section does:
Gateway), the API group, and the resource name (my-gateway).cilium).www.example.com.mode: Terminate, which tells the Gateway to decrypt incoming HTTPS traffic. It references a Kubernetes Secret (named www-tls) containing your TLS certificate.You should update hostname, any annotations, and the referenced TLS secret name (www-tls) to match your application’s configuration.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: my-gateway
spec:
gatewayClassName: cilium
infrastructure:
annotations:
# Copy DigitalOcean LoadBalancer annotations from your Ingress
service.beta.kubernetes.io/do-loadbalancer-name: "gateway-api-lb"
service.beta.kubernetes.io/do-loadbalancer-size-unit: "2"
service.beta.kubernetes.io/do-loadbalancer-healthcheck-path: "/"
listeners:
- name: http
protocol: HTTP
port: 80
hostname: "www.example.com"
- name: https
protocol: HTTPS
port: 443
hostname: "www.example.com"
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: www-tls
This YAML manifests a new Gateway resource that provisions a DigitalOcean LoadBalancer capable of handling both HTTP and HTTPS traffic, using custom annotations and referencing the appropriate TLS certificate for secure connections.
Apply and verify:
kubectl apply -f gateway.yaml
kubectl get gateway my-gateway # Should show PROGRAMMED: True, ADDRESS assigned
What this step does:
This step sets up a ClusterIssuer resource for cert-manager using the Gateway API’s HTTPRoute solver. The ClusterIssuer tells cert-manager how to request and manage Let’s Encrypt certificates for your domains routed through the new Gateway (instead of NGINX Ingress). By leveraging the gatewayHTTPRoute solver, certificate challenges are solved via HTTP on your Gateway, enabling automated certificate issuance and renewal for routes managed by Gateway API.
How to do it:
cluster-issuer-gateway.yaml.email to your real email address.metadata.name is unique and does not conflict with existing ClusterIssuers.parentRefs must match the name and namespace of your Gateway from previous steps.apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod-gateway
spec:
acme:
email: your-email@example.com # <- Replace with your real email address
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt-prod-gateway-key
solvers:
- http01:
gatewayHTTPRoute:
parentRefs:
- name: my-gateway # The Gateway name you defined earlier
namespace: default # The namespace of your Gateway
kind: Gateway
kubectl apply -f cluster-issuer-gateway.yaml
After applying, cert-manager will be able to issue and renew certificates using HTTP-01 challenges routed through your new Gateway, automating HTTPS for your workloads managed by the Gateway API.
Copy your Ingress TLS secret for Gateway use. Since DNS still points to Ingress at this stage, you cannot issue a new certificate yet. This step allows the Gateway to use your existing certificate temporarily.
First, find your Ingress TLS secret name:
kubectl get ingress <your-ingress-name> -o jsonpath='{.spec.tls[0].secretName}'
The Gateway TLS secret name should match what you specified in your Gateway resource’s spec.listeners[].tls.certificateRefs[].name field (e.g., www-tls from Step 2).
Copy the secret:
INGRESS_TLS_SECRET=<your-ingress-tls-secret> # From the command above
GATEWAY_TLS_SECRET=www-tls # Must match Gateway spec.listeners[].tls.certificateRefs[].name
kubectl get secret $INGRESS_TLS_SECRET -o yaml | \
sed "s/name: $INGRESS_TLS_SECRET/name: $GATEWAY_TLS_SECRET/" | \
sed '/uid:/d' | sed '/resourceVersion:/d' | sed '/creationTimestamp:/d' | \
kubectl apply -f -
Verify:
kubectl get secret $GATEWAY_TLS_SECRET # Should show kubernetes.io/tls type
Create a Certificate resource in Phase 4 (after DNS cutover) for proper cert-manager management and renewal. Without this, your certificate will expire after 90 days and won’t renew automatically.
The following httproute.yaml example demonstrates how to define a Kubernetes HTTPRoute resource for the Gateway API. Here’s what each section does:
HTTPRoute), the API group, and the resource name (www-https).my-gateway) and specifies the section name (https) to match the Gateway listener.www.example.com)./) to match incoming requests.my-www-service) and specifies the port (80) to route traffic to.Create httproute.yaml (customize hostname and backend service):
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: www-https
annotations:
# Add if using ExternalDNS
external-dns.alpha.kubernetes.io/hostname: www.example.com
external-dns.alpha.kubernetes.io/ttl: "300"
spec:
parentRefs:
- name: my-gateway
sectionName: https
hostnames:
- www.example.com
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: my-www-service
port: 80
kubectl apply -f httproute.yaml
The following code block defines a Kubernetes HTTPRoute resource that performs an HTTP to HTTPS redirect for your domain. Here’s what each part does:
HTTPRoute named http-redirect.my-gateway Gateway resource and associates it with the http section (listener) – typically listening on port 80.www.example.com.RequestRedirect filter. This filter instructs the Gateway to automatically redirect any HTTP request matching this route to HTTPS (by setting scheme: https). The statusCode: 301 ensures a permanent redirect (Moved Permanently).This resource tells the Gateway to catch all HTTP requests to your domain and send them to the secure HTTPS endpoint, improving security and ensuring consistent access over TLS.
Here’s the YAML manifest for the redirect route:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: http-redirect
spec:
parentRefs:
- name: my-gateway
sectionName: http
hostnames:
- www.example.com
rules:
- filters:
- type: RequestRedirect
requestRedirect:
scheme: https
statusCode: 301
Apply the redirect by running:
kubectl apply -f http-redirect.yaml
kubectl get httproute # Should show both www-https and http-redirect
kubectl get gateway my-gateway # Should show PROGRAMMED: True
kubectl describe httproute www-https
kubectl describe httproute http-redirect
In the describe output, check Status.Parents.Conditions for:
Type: Accepted with Status: TrueType: ResolvedRefs with Status: TrueCommon issues:
ResolvedRefs shows Status: False → Check backend Service name exists and port is correctAccepted shows Status: False → Verify sectionName and hostnames match Gateway listenersTest via IP before DNS cutover:
GATEWAY_IP=$(kubectl get gateway my-gateway -o jsonpath='{.status.addresses[0].value}')
# Test HTTP redirect (expect 301 to HTTPS, server: envoy)
curl -I --resolve www.example.com:80:$GATEWAY_IP http://www.example.com
# Test HTTPS traffic (expect 200, server: envoy)
curl -k -I --resolve www.example.com:443:$GATEWAY_IP https://www.example.com
# Verify content
curl -k --resolve www.example.com:443:$GATEWAY_IP https://www.example.com
Verify all tests return expected results before proceeding.
See the section relevant to how you manage DNS records, either Manually or via ExternalDNS
# Optional: Lower TTL for faster rollback
doctl compute domain records update example.com --record-id <a-record-id> --record-ttl 60
sleep 300 # Wait for old TTL to expire
# Update A record to Gateway IP
doctl compute domain records update example.com --record-id <a-record-id> --record-data "$GATEWAY_IP"
# Monitor DNS propagation (Ctrl+C to stop)
while true; do echo "$(date): $(dig +short www.example.com)"; sleep 5; done
Deploy second ExternalDNS for Gateway API (customize secretKeyRef as needed):
helm install external-dns-gateway external-dns/external-dns \
--namespace external-dns \
--set provider=digitalocean \
--set sources[0]=gateway-httproute \
--set txtOwnerId=gateway \
--set interval=1m \
--set env[0].name=DO_TOKEN \
--set env[0].valueFrom.secretKeyRef.name=external-dns \
--set env[0].valueFrom.secretKeyRef.key=token
Transfer TXT ownership (triggers DNS cutover): Ensure to replace example.com domains in these commands with your domain hosted on DigitalOcean.
# Find TXT record. replace update pattern to find your record: a-<hostname>
TXT_RECORD_ID=$(doctl compute domain records list example.com --format ID,Type,Name --no-header | grep "TXT.*a-<hostname>" | awk '{print $1}')
# Validate TXT_RECORD_ID has a value
echo $TXT_RECORD_ID
# Transfer ownership from default to gateway
doctl compute domain records update example.com \
--record-id $TXT_RECORD_ID \
--record-data "heritage=external-dns,external-dns/owner=gateway,external-dns/resource=gateway/default/my-gateway"
Monitor cutover (Ctrl+C to stop):
while true; do echo "$(date): $(dig +short www.example.com)"; sleep 5; done
Once the IP changes to the Gateway IP, verify with:
curl -I https://www.example.com # Should return 200, server: envoy
Create Certificate resource for proper cert-manager management and renewal:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: www-tls-gateway
spec:
secretName: www-tls
issuerRef:
name: letsencrypt-prod-gateway
kind: ClusterIssuer
dnsNames:
- www.example.com
kubectl apply -f certificate.yaml
kubectl get certificate www-tls-gateway -w # Wait for READY: True
Without this Certificate resource, certificate renewal will fail after 90 days. The Certificate resource tells cert-manager to automatically renew the certificate before expiration using the ClusterIssuer you created in Phase 1, Step 3.
Monitor for 24-48 hours before removing Ingress. Watch traffic volume, certificate READY status, error rates, and response times. This monitoring period ensures the Gateway is handling production traffic correctly before you remove the safety net of the old Ingress setup.
Verify certificate management before cleanup:
kubectl get certificate www-tls-gateway # Must show READY = True
# Must show letsencrypt-prod-gateway or the name of the ClusterIssuer you created in Phase 1 Step 3
kubectl get certificate www-tls-gateway -o jsonpath='{.spec.issuerRef.name}'
Execute cleanup:
helm uninstall external-dns -n external-dns # If using ExternalDNS
helm uninstall ingress-nginx -n ingress-nginx
kubectl delete namespace ingress-nginx
Verify:
curl -I https://www.example.com # Should return 200, server: envoy
doctl compute load-balancer list --format ID,Region,Name,IP # Should show only Gateway LoadBalancer
Rollback if the Gateway becomes unreachable, certificate failures occur, error rates increase significantly, or critical feature gaps are discovered. The rollback process is quick (2-6 minutes) since your Ingress NGINX setup remains intact during the monitoring period.
ExternalDNS rollback:
This command updates the TXT record managed by ExternalDNS for your domain (example.com) to transfer DNS ownership and traffic routing back to the Ingress NGINX controller. It uses doctl to set the TXT record’s data to reference the original Ingress resource, ensuring ExternalDNS will update the domain’s A record to point back to the old Ingress LoadBalancer IP.
doctl compute domain records update example.com \
--record-id $TXT_RECORD_ID \
--record-data "heritage=external-dns,external-dns/owner=default,external-dns/resource=ingress/default/sample-nginx"
# Wait ~60s for DNS update
Manual DNS rollback:
INGRESS_IP=$(kubectl get svc -n ingress-nginx ingress-nginx-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
doctl compute domain records update example.com --record-id <a-record-id> --record-data "$INGRESS_IP"
# Wait for DNS propagation (~60-300s)
Validate: dig +short www.example.com should show Ingress IP. Rollback time: 2-6 minutes total.
Yes, this is the recommended approach for zero-downtime migration. Both systems can run concurrently. The Gateway API creates a new DigitalOcean LoadBalancer with a different IP address, allowing you to test the Gateway configuration before switching DNS. Once you’ve validated the Gateway is working correctly, you update DNS to point to the new LoadBalancer IP, then remove the old Ingress NGINX setup after monitoring for stability.
Your existing TLS certificates from Ingress NGINX can be copied to the Gateway API initially. In Step 3 of Phase 1, you copy the TLS secret from your Ingress to the Gateway. However, after DNS cutover, you must create an explicit Certificate resource (as shown in Phase 4, Step 1) for proper cert-manager management and automatic renewal. Without this Certificate resource, your certificate will expire after 90 days and won’t renew automatically.
Gateway API requires a different certificate challenge solver than Ingress. While Ingress uses the ingress solver, Gateway API uses the gatewayHTTPRoute solver. This is why you need to create a new ClusterIssuer with the gatewayHTTPRoute configuration pointing to your Gateway resource. The cert-manager must also have the --enable-gateway-api=true flag enabled to support this.
The technical migration can be completed in a few hours, but you should plan for a 24-48 hour monitoring period before removing the old Ingress NGINX setup. The actual cutover time depends on DNS propagation, which typically takes 60-300 seconds after updating DNS records. The dual LoadBalancer period (when you’re paying for both) typically lasts 24-48 hours while you monitor stability.
The rollback procedure is straightforward. If you’re using ExternalDNS, you transfer the TXT ownership record back to the original Ingress. For manual DNS, you update the A record to point back to the Ingress LoadBalancer IP. The rollback process takes 2-6 minutes total, depending on DNS propagation. Your Ingress NGINX setup remains intact during the monitoring period, making rollback simple if needed.
You’ve successfully migrated from Ingress NGINX to Gateway API on DOKS with zero downtime. Your Gateway leverages Cilium’s built-in controller and provides modern, extensible traffic management with explicit configuration and advanced routing capabilities. The Gateway API offers better separation of concerns, role-based access control, and more expressive routing options compared to the traditional Ingress API.
Now that you’ve completed the migration, explore these resources to deepen your understanding and expand your Gateway API implementation:
For production deployments, consider implementing monitoring and observability for your Gateway API setup. You can also explore advanced Gateway API features like header-based routing, traffic splitting, and integration with service meshes for more sophisticated traffic management patterns.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
A Senior Solutions Architect at DigitalOcean focusing on Cloud Architecture, Kubernetes, Automation and Infrastructure-as-Code.
I help Businesses scale with AI x SEO x (authentic) Content that revives traffic and keeps leads flowing | 3,000,000+ Average monthly readers on Medium | Sr Technical Writer @ DigitalOcean | Ex-Cloud Consultant @ AMEX | Ex-Site Reliability Engineer(DevOps)@Nutanix
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.
Full documentation for every DigitalOcean product.
The Wave has everything you need to know about building a business, from raising funding to marketing your product.
Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.
New accounts only. By submitting your email you agree to our Privacy Policy
Scale up as you grow — whether you're running one virtual machine or ten thousand.
Sign up and get $200 in credit for your first 60 days with DigitalOcean.*
*This promotional offer applies to new accounts only.