How to Install Ingress-Nginx Controller on Ubuntu 24.04 VPS: Kubernetes Ingress for Self-Hosted Clusters
Running Kubernetes on a self-hosted Ubuntu VPS is a cost-effective way to orchestrate containers, but the cluster is not useful until you can route external HTTP traffic into it. The Ingress-Nginx Controller is the most widely deployed ingress solution in the Kubernetes ecosystem: it translates Kubernetes Ingress resources into NGINX configuration, terminates TLS, rewrites URLs, rate-limits requests, and protects services behind authentication -- all from a single entry point on port 80/443.
This tutorial walks through installing the Ingress-Nginx Controller on Ubuntu 24.04 with Helm, defining host-based and path-based Ingress resources, integrating cert-manager for automatic Let's Encrypt certificates, and applying production-grade annotations for rate limiting, rewrites, and basic authentication.
Skip the setup? Deploy a ready-to-use Kubernetes VPS with Ingress-Nginx pre-installed on the CloudCore Starter plan and start shipping workloads in minutes.
Table of Contents
What is Ingress-Nginx?
Ingress-Nginx is an open-source Kubernetes ingress controller maintained by the Kubernetes SIG Network community. It watches the Kubernetes API for Ingress objects and dynamically rewrites an internal NGINX configuration file whenever rules change. The controller runs as a regular Pod inside your cluster, typically exposed through a LoadBalancer, NodePort, or hostNetwork Service.
At its core the controller does four things:
Because it is built on top of open-source NGINX, it benefits from decades of battle-testing in production, thousands of configuration options, and deep integration with the broader NGINX tooling ecosystem. It is not the same project as F5's nginxinc/kubernetes-ingress -- that is a separate, commercially-backed controller with its own Custom Resource Definitions. Throughout this guide "Ingress-Nginx" refers to the community-maintained kubernetes/ingress-nginx controller.
Why Self-Host Ingress vs Cloud Load Balancers?
Managed Kubernetes services (EKS, GKE, AKS, DigitalOcean Kubernetes) provision a cloud load balancer for every Service of type LoadBalancer. That load balancer typically costs $18-25 per month per Service, before data transfer charges. For a self-hosted cluster on a single VPS, this is both unnecessary and wasteful.
Running Ingress-Nginx directly on your Ubuntu VPS delivers several concrete benefits:
- Zero load balancer fees -- The controller binds to ports 80 and 443 on the node itself (via
hostNetworkorNodePortwith an externalexternalIPsentry). No AWS NLB, no GCP forwarding rule, no monthly charges. - A single entry point for dozens of services -- One IP and one TLS handshake handle all traffic. You can host 50 microservices on the same VPS, each on its own subdomain or path, without spinning up 50 load balancers.
- Full configuration control -- Cloud load balancers expose a restricted subset of NGINX features. With Ingress-Nginx you get the entire NGINX configuration surface, including Lua scripting, custom error pages, and advanced rewrite rules.
- Predictable flat-rate cost -- An Ubuntu 24.04 VPS from vps-server.host starts at EUR 7.99/mo and includes unmetered bandwidth. A single node runs ingress, your apps, and cert-manager simultaneously.
- Data sovereignty -- TLS is terminated on your own hardware. No third party sees the plaintext traffic, which matters for GDPR, HIPAA, and regulated workloads.
- Faster iteration -- Changes to Ingress rules are applied in under a second. Cloud load balancer updates often take 30-120 seconds to propagate across regions.
Cost Comparison: Self-Hosted Ingress-Nginx vs Cloud Load Balancers
| Scenario | Cloud Load Balancer (AWS/GCP) | Managed k8s + Cloud LB | Self-Hosted Ingress-Nginx (VPS) |
|---|---|---|---|
| Monthly base cost | $18-25/mo per LB | $70+ (control plane) + $18 LB | EUR 7.99/mo (flat) |
| 10 services exposed | 10 LBs = $180-250/mo | 1 shared LB + ingress | 1 VPS (no extra cost) |
| TLS certificates | Paid (ACM/Certificate Manager) | Paid or Let's Encrypt | Free (Let's Encrypt + cert-manager) |
| Bandwidth overage | $0.08-0.12 per GB | $0.08-0.12 per GB | Included/unmetered |
| Custom NGINX config | Restricted | Full (via ingress controller) | Full |
Prerequisites
Before you start, make sure you have:
- A VPS running Ubuntu 24.04 LTS with at least 2 GB RAM and 2 vCPUs
- SSH access with sudo privileges
- A running Kubernetes or k3s cluster on the VPS (see our k3s install guide)
- Helm 3 installed (see our Helm install guide)
- kubectl configured with cluster admin access
- A domain name with an A record pointing to the VPS public IP
Recommended Plan: CloudCore Starter>
For a self-hosted k3s + Ingress-Nginx setup serving a handful of services, we recommend the CloudCore Starter plan:>
- 4 vCPU cores
- 8 GB RAM
- 75 GB NVMe SSD
- 32 TB bandwidth
- Full root access, no managed LB fees>
This is enough headroom to run k3s, Ingress-Nginx, cert-manager, and a dozen small services comfortably on a single node.
Connect to your server via SSH to begin:
ssh root@your-server-ipStep 1: Prepare the Ubuntu 24.04 Node
Start by updating the system to ensure all security patches are in place:
sudo apt update && sudo apt upgrade -yVerify that your cluster is reachable and healthy:
kubectl get nodesExpected output:
NAME STATUS ROLES AGE VERSION
vps-k3s-node Ready control-plane,master 3d v1.30.1+k3s1If you are using k3s, it ships with its own bundled ingress controller (Traefik) enabled by default. Disable it before installing Ingress-Nginx to avoid port conflicts:
# Edit /etc/systemd/system/k3s.service and add --disable=traefik to the ExecStart line
sudo sed -i 's|ExecStart=/usr/local/bin/k3s server|ExecStart=/usr/local/bin/k3s server --disable=traefik|' /etc/systemd/system/k3s.service
sudo systemctl daemon-reload
sudo systemctl restart k3sConfirm Traefik is gone:
kubectl -n kube-system get pods | grep -i traefikThere should be no output. If you plan to keep an additional NGINX reverse proxy on the host for non-cluster services, review our NGINX install guide -- make sure the host NGINX does not bind to ports 80/443 before we install the controller.
Check that Helm is functional:
helm versionExpected output:
version.BuildInfo{Version:"v3.14.4", GitCommit:"...", GoVersion:"go1.22.2"}Step 2: Add the Ingress-Nginx Helm Repository
The Ingress-Nginx project publishes an official Helm chart. Add the repository and refresh the local index:
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo updateExpected output:
"ingress-nginx" has been added to your repositories
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "ingress-nginx" chart repository
Update Complete. Happy Helming!List the available chart versions:
helm search repo ingress-nginx --versions | head -5Expected output (abbreviated):
NAME CHART VERSION APP VERSION DESCRIPTION
ingress-nginx/ingress-nginx 4.11.2 1.11.2 Ingress controller for Kubernetes using NGINX...
ingress-nginx/ingress-nginx 4.11.1 1.11.1 Ingress controller for Kubernetes using NGINX...Step 3: Install the Ingress-Nginx Controller
Create a dedicated namespace:
kubectl create namespace ingress-nginxOn a single-node VPS without a cloud load balancer, install the controller with hostNetwork: true so it binds directly to the node's ports 80 and 443:
helm install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx \
--set controller.hostNetwork=true \
--set controller.hostPort.enabled=true \
--set controller.kind=DaemonSet \
--set controller.service.type=ClusterIP \
--set controller.publishService.enabled=false \
--set controller.extraArgs.publish-status-address=your-server-ip \
--set controller.ingressClassResource.default=true \
--set controller.config.use-forwarded-headers=true \
--set controller.config.compute-full-forwarded-for=true \
--set controller.config.use-proxy-protocol=falseReplace your-server-ip with the public IP of the VPS. A few notes on this command:
controller.hostNetwork=truelets the NGINX pod listen on the node's real ports 80 and 443 rather than requiring a cloud load balancer.controller.kind=DaemonSetensures one ingress pod per node -- on a single-node setup this gives you a predictable binding.controller.ingressClassResource.default=truemarks this controller as the default for Ingress resources that do not specify aningressClassName.use-forwarded-headersandcompute-full-forwarded-forpreserve the real client IP when the controller is behind Cloudflare or another upstream proxy.
helm install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx \
--set controller.service.type=LoadBalancerExpected output:
NAME: ingress-nginx
LAST DEPLOYED: Thu Apr 16 10:00:00 2026
NAMESPACE: ingress-nginx
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
The ingress-nginx controller has been installed.
It may take a few minutes for the load balancer IP to be available.
...Step 4: Verify the Installation
Wait for the controller pod to become ready:
kubectl -n ingress-nginx get pods --watchExpected output once healthy:
NAME READY STATUS RESTARTS AGE
ingress-nginx-controller-abcde 1/1 Running 0 45sCheck that ports 80 and 443 are listening on the host:
sudo ss -tlnp | grep -E ':80|:443'Expected output:
LISTEN 0 511 :80 :* users:(("nginx",pid=12345,fd=7))
LISTEN 0 511 :443 :* users:(("nginx",pid=12345,fd=8))Test the controller with a curl to the VPS:
curl -I http://your-server-ipExpected output:
HTTP/1.1 404 Not Found
Date: Thu, 16 Apr 2026 10:02:00 GMT
Content-Type: text/html
Server: nginxA 404 here is the correct response: the controller is running but no Ingress rules are defined yet. The Server: nginx header confirms NGINX is terminating the request.
Inspect the IngressClass:
kubectl get ingressclassExpected output:
NAME CONTROLLER PARAMETERS AGE
nginx k8s.io/ingress-nginx <none> 2mStep 5: Create a Test Backend Service
Deploy a simple httpbin backend to route traffic to:
kubectl create namespace demoCreate the deployment and service:
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: httpbin
namespace: demo
spec:
replicas: 2
selector:
matchLabels: { app: httpbin }
template:
metadata:
labels: { app: httpbin }
spec:
containers:
- name: httpbin
image: kennethreitz/httpbin:latest
ports:
- containerPort: 80
apiVersion: v1
kind: Service
metadata:
name: httpbin
namespace: demo
spec:
selector: { app: httpbin }
ports:
- port: 80
targetPort: 80
EOFVerify the pods are running:
kubectl -n demo get podsExpected output:
NAME READY STATUS RESTARTS AGE
httpbin-7f8b9c4d5-abcde 1/1 Running 0 20s
httpbin-7f8b9c4d5-fghij 1/1 Running 0 20sStep 6: Define Ingress Resources (Host and Path Routing)
An Ingress resource tells the controller how to map incoming HTTP requests to backend Services. There are two dimensions you can route on: the Host header and the URL path.
Host-Based Routing
Create an Ingress that exposes httpbin at api.example.com:
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: httpbin-host
namespace: demo
spec:
ingressClassName: nginx
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: httpbin
port: { number: 80 }
EOFPoint the DNS A record for api.example.com to your VPS IP, wait for propagation, then test:
curl -H "Host: api.example.com" http://your-server-ip/status/200Expected output:
HTTP/1.1 200 OKPath-Based Routing
Route multiple path prefixes on the same hostname to different backends. Assume you also have a frontend service in the demo namespace:
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: multi-path
namespace: demo
spec:
ingressClassName: nginx
rules:
- host: app.example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: httpbin
port: { number: 80 }
- path: /
pathType: Prefix
backend:
service:
name: frontend
port: { number: 80 }
EOFRequests to https://app.example.com/api/* are routed to httpbin, while everything else falls through to frontend.
Path Types Explained
pathType | Matching Behavior |
|---|---|
Exact | Matches the URL path exactly, case-sensitive. |
Prefix | Matches based on URL path prefix split by /. /foo matches /foo and /foo/bar. |
ImplementationSpecific | Defers to the ingress controller. Ingress-Nginx treats this as a regex match. |
Prefix.Step 7: Add TLS with cert-manager
Serving HTTPS requires TLS certificates. cert-manager automates Let's Encrypt issuance and renewal for every Ingress in the cluster.
Assuming cert-manager is already installed (see the linked guide), create a ClusterIssuer for Let's Encrypt production:
cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: [email protected]
privateKeySecretRef:
name: letsencrypt-prod-account
solvers:
- http01:
ingress:
class: nginx
EOFNow update the Ingress to request a certificate:
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: httpbin-host
namespace: demo
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
tls:
- hosts:
- api.example.com
secretName: api-example-com-tls
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: httpbin
port: { number: 80 }
EOFcert-manager will detect the new Ingress, complete the HTTP-01 challenge through Ingress-Nginx, and store the issued certificate in the api-example-com-tls Secret. Watch the progress:
kubectl -n demo describe certificate api-example-com-tlsWithin 60-90 seconds you should see:
Status:
Conditions:
Type: Ready
Status: TrueTest HTTPS:
curl -v https://api.example.com/status/200Expected output includes:
* subject: CN=api.example.com
- issuer: C=US; O=Let's Encrypt; CN=R11
HTTP/2 200Ingress-Nginx automatically redirects HTTP to HTTPS when a TLS block is present. To disable the redirect, add the annotation nginx.ingress.kubernetes.io/ssl-redirect: "false".
Step 8: Rewrite Rules and URL Manipulation
Rewrite rules let you strip prefixes, redirect URLs, or transform paths before they hit the backend.
Strip a Path Prefix
To expose httpbin under /api but have the backend receive requests at /, use the rewrite-target annotation with a regex capture:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: rewrite-example
namespace: demo
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
ingressClassName: nginx
rules:
- host: app.example.com
http:
paths:
- path: /api(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: httpbin
port: { number: 80 }Now https://app.example.com/api/get is forwarded to the backend as /get.
Permanent Redirects
Redirect old URLs to a new location:
metadata:
annotations:
nginx.ingress.kubernetes.io/permanent-redirect: https://new.example.com$request_uri
nginx.ingress.kubernetes.io/permanent-redirect-code: "301"Force HTTPS Without TLS on the Ingress
If you terminate TLS upstream (e.g. at Cloudflare) and still want to enforce HTTPS:
metadata:
annotations:
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"Custom Server Snippet
For advanced NGINX directives not exposed as annotations, use a server-snippet:
metadata:
annotations:
nginx.ingress.kubernetes.io/server-snippet: |
if ($http_user_agent ~* (curl|wget)) {
return 403;
}Note: starting in ingress-nginx v1.9,server-snippetandconfiguration-snippetare disabled by default for security. Enable them by setting--set controller.allowSnippetAnnotations=truewhen installing the Helm chart, and only if you trust all Ingress authors in the cluster.
Step 9: Rate Limiting and Connection Limits
Ingress-Nginx supports per-client rate limiting out of the box. The default key is the client IP address.
Requests Per Second
Limit requests per second globally across all backing pods:
metadata:
annotations:
nginx.ingress.kubernetes.io/limit-rps: "10"
nginx.ingress.kubernetes.io/limit-burst-multiplier: "5"This allows 10 requests per second sustained, with a burst of up to 50 (10 x 5). Clients exceeding the limit receive HTTP 503.
Requests Per Minute
For endpoints with bursty usage, minute-granularity is more practical:
metadata:
annotations:
nginx.ingress.kubernetes.io/limit-rpm: "600"Concurrent Connections
Cap simultaneous connections per client IP:
metadata:
annotations:
nginx.ingress.kubernetes.io/limit-connections: "20"Allowlist Trusted Clients
Exempt internal networks or monitoring agents from rate limits:
metadata:
annotations:
nginx.ingress.kubernetes.io/limit-whitelist: "10.0.0.0/8,192.168.0.0/16"
nginx.ingress.kubernetes.io/limit-rps: "5"Client Body Size
The default request body limit is 1 MB. Increase it for file upload endpoints:
metadata:
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "100m"Step 10: Authentication Annotations
Ingress-Nginx provides two built-in authentication modes: HTTP Basic Auth and External Auth (via a subrequest to a separate service).
HTTP Basic Auth
Generate a htpasswd file and store it as a Secret:
sudo apt install -y apache2-utils htpasswd -c auth adminEnter a password when prompted
kubectl -n demo create secret generic basic-auth --from-file=auth
Reference the Secret from the Ingress:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: protected
namespace: demo
annotations:
nginx.ingress.kubernetes.io/auth-type: basic
nginx.ingress.kubernetes.io/auth-secret: basic-auth
nginx.ingress.kubernetes.io/auth-realm: "Authentication Required"
spec:
ingressClassName: nginx
rules:
- host: admin.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: httpbin
port: { number: 80 }Test it:
curl -u admin:yourpassword https://admin.example.com/getExternal Auth (OAuth2 Proxy, Authelia, etc.)
Delegate authentication to an external service such as Authelia or oauth2-proxy:
metadata:
annotations:
nginx.ingress.kubernetes.io/auth-url: "https://auth.example.com/api/verify"
nginx.ingress.kubernetes.io/auth-signin: "https://auth.example.com"
nginx.ingress.kubernetes.io/auth-response-headers: "Remote-User,Remote-Groups,Remote-Email"The controller issues a subrequest to auth-url. A 2xx response allows the request; anything else redirects the user to auth-signin. Response headers from the auth service are forwarded to the backend, useful for SSO identity propagation.
IP Allowlisting
Restrict access to an Ingress by CIDR:
metadata:
annotations:
nginx.ingress.kubernetes.io/whitelist-source-range: "203.0.113.0/24,198.51.100.42/32"Requests from any other IP receive HTTP 403.
Step 11: Production Hardening
A few adjustments turn a development install into a production-grade deployment.
Resource Requests and Limits
Edit your Helm values to guarantee CPU and memory for the controller:
# values.yaml
controller:
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: 1000m
memory: 512MiApply:
helm upgrade ingress-nginx ingress-nginx/ingress-nginx \
-n ingress-nginx -f values.yamlEnable Access Logs in JSON
Structured logs are easier to ship to Loki or Elasticsearch:
controller:
config:
log-format-escape-json: "true"
log-format-upstream: '{"time":"$time_iso8601","remote_addr":"$remote_addr","host":"$host","method":"$request_method","uri":"$request_uri","status":$status,"bytes_sent":$bytes_sent,"request_time":$request_time,"upstream_response_time":"$upstream_response_time","user_agent":"$http_user_agent"}'Enable Prometheus Metrics
Expose the /metrics endpoint for scraping:
controller:
metrics:
enabled: true
serviceMonitor:
enabled: true
namespace: monitoringHarden TLS
Force modern TLS versions and ciphers:
controller:
config:
ssl-protocols: "TLSv1.2 TLSv1.3"
ssl-ciphers: "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305"
ssl-session-tickets: "false"
hsts: "true"
hsts-max-age: "31536000"
hsts-include-subdomains: "true"ModSecurity Web Application Firewall
Ingress-Nginx ships with optional ModSecurity + OWASP Core Rule Set support:
controller:
config:
enable-modsecurity: "true"
enable-owasp-modsecurity-crs: "true"Per-Ingress override:
metadata:
annotations:
nginx.ingress.kubernetes.io/enable-modsecurity: "true"
nginx.ingress.kubernetes.io/modsecurity-snippet: |
SecRuleEngine On
SecRequestBodyAccess OnTroubleshooting
| Problem | Cause | Solution |
|---|---|---|
404 Not Found on all requests | No Ingress resource matches the Host header | List Ingress: kubectl get ingress -A. Check the Host rule spelling. |
Controller pod stuck in Pending | Host ports 80/443 already in use by another process | sudo ss -tlnp \</td><td>grep -E ':80\</td><td>:443'. Stop Traefik, Apache, or host-level NGINX. |
upstream connect error in logs | Backend Service has no healthy endpoints | kubectl -n demo get endpoints httpbin. Verify pod readiness probes. |
Let's Encrypt certificate stuck Pending | HTTP-01 challenge cannot reach the VPS | Ensure DNS is correct and port 80 is reachable publicly. Check kubectl describe challenge -A. |
| Rate limit not applying | Real client IP hidden behind Cloudflare/CDN | Enable use-forwarded-headers: "true" and set nginx.ingress.kubernetes.io/limit-connections to trust the forwarded IP. |
413 Request Entity Too Large on uploads | Default proxy-body-size is 1 MB | Add nginx.ingress.kubernetes.io/proxy-body-size: "50m" to the Ingress. |
| Changes to annotations not taking effect | Controller caches configuration for a few seconds | Watch the controller logs: kubectl -n ingress-nginx logs -f deploy/ingress-nginx-controller. Look for Configuration changes detected. |
Viewing Controller Logs
kubectl -n ingress-nginx logs -f deploy/ingress-nginx-controllerReloading Configuration Manually
The controller reloads automatically when Ingress objects change, but you can force a reload by deleting the pod:
kubectl -n ingress-nginx rollout restart deploy/ingress-nginx-controllerFAQ
What is the difference between Ingress-Nginx and NGINX Ingress (F5)?
Ingress-Nginx (kubernetes/ingress-nginx) is the community-maintained controller built on open-source NGINX and governed under the Kubernetes project. NGINX Ingress (nginxinc/kubernetes-ingress) is maintained by F5 NGINX and ships in both open-source and commercial (NGINX Plus) variants. The two projects use different annotation prefixes (nginx.ingress.kubernetes.io/... vs nginx.org/...) and different Custom Resource Definitions. This guide covers the community Ingress-Nginx controller, which is the most widely deployed ingress solution in Kubernetes and the one referenced by the CNCF's official documentation.
Do I need a cloud load balancer to use Ingress-Nginx?
No. On a self-hosted Ubuntu VPS you can expose the controller with hostNetwork: true (used in this guide) or with a NodePort Service and point DNS directly at the VPS IP. This avoids the $18-25 per month per-LB fee charged by cloud providers. For multi-node clusters on bare metal, MetalLB provides BGP or ARP-based load balancer functionality without vendor lock-in.
Can Ingress-Nginx handle TLS termination for multiple domains?
Yes. The controller supports Server Name Indication (SNI) and can terminate TLS for hundreds of distinct hostnames on the same IP. Each Ingress resource references its own TLS Secret. Combined with cert-manager, certificates are issued and renewed automatically by Let's Encrypt. There is no practical limit beyond the memory needed to cache certificate chains and session tickets.
How do I enable rate limiting in Ingress-Nginx?
Add one of the following annotations to an Ingress resource: nginx.ingress.kubernetes.io/limit-rps for requests per second, nginx.ingress.kubernetes.io/limit-rpm for requests per minute, or nginx.ingress.kubernetes.io/limit-connections for simultaneous connections. The controller tracks counters in a shared memory zone keyed by client IP. Use nginx.ingress.kubernetes.io/limit-whitelist to exempt trusted CIDR ranges. If the controller sits behind a CDN like Cloudflare, enable use-forwarded-headers in the ConfigMap so rate limits apply to the real client IP instead of the CDN edge IP.
How does Ingress-Nginx compare to Traefik?
Ingress-Nginx is built on the battle-tested NGINX engine and has the broadest annotation surface for fine-grained HTTP behavior. It uses Kubernetes native Ingress resources and is the default choice when your team is already fluent in NGINX. Traefik uses its own IngressRoute CRD (though it supports standard Ingress too) and is known for its dynamic service discovery across Kubernetes, Docker, and Consul. For a pure Kubernetes workload with heavy traffic and complex rewrites, Ingress-Nginx tends to offer more control; for multi-provider service meshes or built-in Let's Encrypt (without cert-manager), Traefik is ergonomic. Both are valid choices -- this guide focuses on Ingress-Nginx because it is the de-facto standard in production Kubernetes deployments.
Can I run Ingress-Nginx alongside another ingress controller?
Yes. Multiple ingress controllers can coexist if each has a unique IngressClass name. Set the ingressClassName field on each Ingress resource to route it to the correct controller. This is useful when migrating from Traefik to Ingress-Nginx or when running different controllers for public vs internal traffic.
Next Steps
With Ingress-Nginx routing traffic into your cluster, you can build on the foundation:
- Install cert-manager for automatic TLS -- Follow our cert-manager install guide to automate Let's Encrypt certificate issuance and renewal across every Ingress in the cluster.
- Deploy a monitoring stack -- Scrape Ingress-Nginx metrics with Prometheus and visualize request rates, latencies, and error codes in Grafana. The controller exposes over 40 metrics including
nginx_ingress_controller_request_duration_secondsandnginx_ingress_controller_requests. - Set up a WAF -- Enable ModSecurity with the OWASP Core Rule Set for layer-7 attack protection, or front the cluster with CrowdSec for behavioral blocking.
- Integrate SSO -- Deploy Authelia or Authentik behind the
auth-urlannotation to protect every internal service with a single sign-on portal. - Autoscale the controller -- On busier clusters, enable HorizontalPodAutoscaler on the ingress-nginx Deployment to scale up during traffic spikes.
- Explore advanced routing -- Session affinity (
affinity: cookie), canary releases (canary-weight: "10"), and gRPC backends (backend-protocol: GRPC) are all supported through annotations.
Skip the Manual Install -- Get a Kubernetes-Ready VPS>
Our CloudCore Starter plan ships with k3s, Helm, and Ingress-Nginx pre-installed. Deploy in under 60 seconds and start routing traffic immediately.>
- 4 vCPU, 8 GB RAM, 75 GB NVMe SSD
- k3s + Ingress-Nginx + cert-manager pre-configured
- Free Let's Encrypt certificates via cert-manager
- Prometheus metrics endpoint enabled out of the box
- Full root access, no hidden load balancer fees>
Launch a CloudCore Starter VPS -- Plans start at EUR 7.99/month.