Skip to main contentSkip to navigation
[email protected]
Client AreaSupport
Hosting Mammoth
HostingMammothYour Data, Our Responsibility
Home
Solutions
Hosting Services
Store
Pricing
About
Blog
API
Contact

Stay Ahead of the Curve

Get the latest insights on cybersecurity, AI innovations, and enterprise data solutions delivered to your inbox.

Hosting Mammoth
HostingMammothEnterprise Solutions

Enterprise-grade data solutions. Hosting, recovery, cybersecurity, and AI-powered services for businesses worldwide.

[email protected]
Sun - Fri, 9:00am - 5:00pm

Services

  • Cloud Hosting
  • Data Recovery
  • Cybersecurity
  • Legal Support
  • MSP Services
  • Web Development
  • AI Services
  • Free Server Migration

Hosting

  • VPS Hosting (NVMe SSD)
  • VDS Hosting (NVMe)
  • Storage VPS (High SSD)
  • GPU Servers
  • Managed Services
  • Cloud Firewall
  • Load Balancer
  • One-Click Apps
  • n8n Hosting
  • Object Storage
  • FAQ

Company

  • Store
  • Pricing
  • About Us
  • Locations
  • Blog
  • Testimonials
  • Contact
  • Affiliate Program
  • White-Label
  • Terms of Service
  • Privacy Policy
  • Browser Cookies
  • SLA

Support

  • Client Area
  • Submit Ticket
  • Knowledge Base
  • Server Status
  • API Documentation

© 2026 Hosting Mammoth. All rights reserved.

Knowledge Base
Getting StartedAccount ManagementVPS HostingGPU ServersStorage VPSCloud FirewallLoad BalancerServer ManagementBilling & PaymentsSupport & TicketsAffiliate ProgramReseller ProgramMarketplace & Appsn8n HostingManaged ServicesServer MigrationAPI & DevelopersSecurityTroubleshootingGlossaryInstall Guides
  1. Home
  2. /
  3. Support
  4. /
  5. Install Guides
  6. /
  7. How To Install Ingress Nginx Ubuntu
GUIDEInstall Guides

How to Install Ingress-Nginx Controller on Ubuntu 24.04 VPS: Kubernetes Ingress for Self-Hosted Clusters

24 min read

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?
  • Why Self-Host Ingress vs Cloud Load Balancers?
  • Prerequisites
  • Step 1: Prepare the Ubuntu 24.04 Node
  • Step 2: Add the Ingress-Nginx Helm Repository
  • Step 3: Install the Ingress-Nginx Controller
  • Step 4: Verify the Installation
  • Step 5: Create a Test Backend Service
  • Step 6: Define Ingress Resources (Host and Path Routing)
  • Step 7: Add TLS with cert-manager
  • Step 8: Rewrite Rules and URL Manipulation
  • Step 9: Rate Limiting and Connection Limits
  • Step 10: Authentication Annotations
  • Step 11: Production Hardening
  • Troubleshooting
  • FAQ
  • Next Steps
  • 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:

  • Routes traffic based on Host headers and URL paths to backend Kubernetes Services.
  • Terminates TLS using certificates stored in Kubernetes Secrets, supporting SNI for multiple domains on the same IP.
  • Applies middleware through annotations -- rewrites, redirects, CORS headers, authentication, rate limiting, IP allowlists, client body size, and more.
  • Load-balances requests across Pods backing each Service using NGINX's built-in algorithms (round-robin, least connections, IP hash, or EWMA).
  • 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 hostNetwork or NodePort with an external externalIPs entry). 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

    ScenarioCloud Load Balancer (AWS/GCP)Managed k8s + Cloud LBSelf-Hosted Ingress-Nginx (VPS)
    Monthly base cost$18-25/mo per LB$70+ (control plane) + $18 LBEUR 7.99/mo (flat)
    10 services exposed10 LBs = $180-250/mo1 shared LB + ingress1 VPS (no extra cost)
    TLS certificatesPaid (ACM/Certificate Manager)Paid or Let's EncryptFree (Let's Encrypt + cert-manager)
    Bandwidth overage$0.08-0.12 per GB$0.08-0.12 per GBIncluded/unmetered
    Custom NGINX configRestrictedFull (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:

    bash
    ssh root@your-server-ip

    Step 1: Prepare the Ubuntu 24.04 Node

    Start by updating the system to ensure all security patches are in place:

    bash
    sudo apt update && sudo apt upgrade -y

    Verify that your cluster is reachable and healthy:

    bash
    kubectl get nodes

    Expected output:

    text
    NAME           STATUS   ROLES                  AGE   VERSION
    vps-k3s-node   Ready    control-plane,master   3d    v1.30.1+k3s1

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

    bash
    # 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 k3s

    Confirm Traefik is gone:

    bash
    kubectl -n kube-system get pods | grep -i traefik

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

    bash
    helm version

    Expected output:

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

    bash
    helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
    helm repo update

    Expected output:

    text
    "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:

    bash
    helm search repo ingress-nginx --versions | head -5

    Expected output (abbreviated):

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

    bash
    kubectl create namespace ingress-nginx

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

    bash
    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=false

    Replace your-server-ip with the public IP of the VPS. A few notes on this command:

    • controller.hostNetwork=true lets the NGINX pod listen on the node's real ports 80 and 443 rather than requiring a cloud load balancer.
    • controller.kind=DaemonSet ensures one ingress pod per node -- on a single-node setup this gives you a predictable binding.
    • controller.ingressClassResource.default=true marks this controller as the default for Ingress resources that do not specify an ingressClassName.
    • use-forwarded-headers and compute-full-forwarded-for preserve the real client IP when the controller is behind Cloudflare or another upstream proxy.
    If you are running a multi-node cluster with MetalLB or a cloud provider integration, use the simpler default install instead:

    bash
    helm install ingress-nginx ingress-nginx/ingress-nginx \
      --namespace ingress-nginx \
      --set controller.service.type=LoadBalancer

    Expected output:

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

    bash
    kubectl -n ingress-nginx get pods --watch

    Expected output once healthy:

    text
    NAME                                        READY   STATUS    RESTARTS   AGE
    ingress-nginx-controller-abcde              1/1     Running   0          45s

    Check that ports 80 and 443 are listening on the host:

    bash
    sudo ss -tlnp | grep -E ':80|:443'

    Expected output:

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

    bash
    curl -I http://your-server-ip

    Expected output:

    text
    HTTP/1.1 404 Not Found
    Date: Thu, 16 Apr 2026 10:02:00 GMT
    Content-Type: text/html
    Server: nginx

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

    bash
    kubectl get ingressclass

    Expected output:

    text
    NAME    CONTROLLER             PARAMETERS   AGE
    nginx   k8s.io/ingress-nginx   <none>       2m

    Step 5: Create a Test Backend Service

    Deploy a simple httpbin backend to route traffic to:

    bash
    kubectl create namespace demo

    Create the deployment and service:

    bash
    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
    EOF

    Verify the pods are running:

    bash
    kubectl -n demo get pods

    Expected output:

    text
    NAME                       READY   STATUS    RESTARTS   AGE
    httpbin-7f8b9c4d5-abcde    1/1     Running   0          20s
    httpbin-7f8b9c4d5-fghij    1/1     Running   0          20s

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

    bash
    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 }
    EOF

    Point the DNS A record for api.example.com to your VPS IP, wait for propagation, then test:

    bash
    curl -H "Host: api.example.com" http://your-server-ip/status/200

    Expected output:

    text
    HTTP/1.1 200 OK

    Path-Based Routing

    Route multiple path prefixes on the same hostname to different backends. Assume you also have a frontend service in the demo namespace:

    bash
    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 }
    EOF

    Requests to https://app.example.com/api/* are routed to httpbin, while everything else falls through to frontend.

    Path Types Explained

    pathTypeMatching Behavior
    ExactMatches the URL path exactly, case-sensitive.
    PrefixMatches based on URL path prefix split by /. /foo matches /foo and /foo/bar.
    ImplementationSpecificDefers to the ingress controller. Ingress-Nginx treats this as a regex match.
    For most production use cases, stick to 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:

    bash
    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
    EOF

    Now update the Ingress to request a certificate:

    bash
    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 }
    EOF

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

    bash
    kubectl -n demo describe certificate api-example-com-tls

    Within 60-90 seconds you should see:

    text
    Status:
      Conditions:
        Type:    Ready
        Status:  True

    Test HTTPS:

    bash
    curl -v https://api.example.com/status/200

    Expected output includes:

    text
    *  subject: CN=api.example.com
    
    • issuer: C=US; O=Let's Encrypt; CN=R11
    HTTP/2 200

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

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

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

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

    yaml
    metadata:
      annotations:
        nginx.ingress.kubernetes.io/server-snippet: |
          if ($http_user_agent ~* (curl|wget)) {
            return 403;
          }
    Note: starting in ingress-nginx v1.9, server-snippet and configuration-snippet are disabled by default for security. Enable them by setting --set controller.allowSnippetAnnotations=true when 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:

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

    yaml
    metadata:
      annotations:
        nginx.ingress.kubernetes.io/limit-rpm: "600"

    Concurrent Connections

    Cap simultaneous connections per client IP:

    yaml
    metadata:
      annotations:
        nginx.ingress.kubernetes.io/limit-connections: "20"

    Allowlist Trusted Clients

    Exempt internal networks or monitoring agents from rate limits:

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

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

    bash
    sudo apt install -y apache2-utils
    htpasswd -c auth admin
    

    Enter a password when prompted

    kubectl -n demo create secret generic basic-auth --from-file=auth

    Reference the Secret from the Ingress:

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

    bash
    curl -u admin:yourpassword https://admin.example.com/get

    External Auth (OAuth2 Proxy, Authelia, etc.)

    Delegate authentication to an external service such as Authelia or oauth2-proxy:

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

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

    yaml
    # values.yaml
    controller:
      resources:
        requests:
          cpu: 200m
          memory: 256Mi
        limits:
          cpu: 1000m
          memory: 512Mi

    Apply:

    bash
    helm upgrade ingress-nginx ingress-nginx/ingress-nginx \
      -n ingress-nginx -f values.yaml

    Enable Access Logs in JSON

    Structured logs are easier to ship to Loki or Elasticsearch:

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

    yaml
    controller:
      metrics:
        enabled: true
        serviceMonitor:
          enabled: true
          namespace: monitoring

    Harden TLS

    Force modern TLS versions and ciphers:

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

    yaml
    controller:
      config:
        enable-modsecurity: "true"
        enable-owasp-modsecurity-crs: "true"

    Per-Ingress override:

    yaml
    metadata:
      annotations:
        nginx.ingress.kubernetes.io/enable-modsecurity: "true"
        nginx.ingress.kubernetes.io/modsecurity-snippet: |
          SecRuleEngine On
          SecRequestBodyAccess On

    Troubleshooting

    ProblemCauseSolution
    404 Not Found on all requestsNo Ingress resource matches the Host headerList Ingress: kubectl get ingress -A. Check the Host rule spelling.
    Controller pod stuck in PendingHost ports 80/443 already in use by another processsudo ss -tlnp \</td><td>grep -E ':80\</td><td>:443'. Stop Traefik, Apache, or host-level NGINX.
    upstream connect error in logsBackend Service has no healthy endpointskubectl -n demo get endpoints httpbin. Verify pod readiness probes.
    Let's Encrypt certificate stuck PendingHTTP-01 challenge cannot reach the VPSEnsure DNS is correct and port 80 is reachable publicly. Check kubectl describe challenge -A.
    Rate limit not applyingReal client IP hidden behind Cloudflare/CDNEnable use-forwarded-headers: "true" and set nginx.ingress.kubernetes.io/limit-connections to trust the forwarded IP.
    413 Request Entity Too Large on uploadsDefault proxy-body-size is 1 MBAdd nginx.ingress.kubernetes.io/proxy-body-size: "50m" to the Ingress.
    Changes to annotations not taking effectController caches configuration for a few secondsWatch the controller logs: kubectl -n ingress-nginx logs -f deploy/ingress-nginx-controller. Look for Configuration changes detected.

    Viewing Controller Logs

    bash
    kubectl -n ingress-nginx logs -f deploy/ingress-nginx-controller

    Reloading Configuration Manually

    The controller reloads automatically when Ingress objects change, but you can force a reload by deleting the pod:

    bash
    kubectl -n ingress-nginx rollout restart deploy/ingress-nginx-controller

    FAQ

    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_seconds and nginx_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-url annotation 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.
    For the full list of supported annotations and configuration keys, see the official Ingress-Nginx documentation.

    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.

    Was this article helpful?

    ← Back to Install GuidesBrowse all categories →

    Still have questions?

    Contact Support →Submit a Ticket