Deploying Nginx on Kubernetes#
Nginx shows up in Kubernetes in two completely different roles. First, as a regular Deployment serving static content or acting as a reverse proxy for your application. Second, as an Ingress controller that watches Ingress resources and dynamically reconfigures itself. These are different deployments with different images and different configuration models. Knowing when to use which saves you from over-engineering or under-engineering your setup.
Nginx as a Web Server (Deployment + Service + ConfigMap)#
For serving static files or acting as a reverse proxy in front of your application pods, deploy nginx as a standard Deployment.
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-conf
data:
default.conf: |
server {
listen 80;
server_name _;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://api-service:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
ports:
- containerPort: 80
volumeMounts:
- name: nginx-conf
mountPath: /etc/nginx/conf.d/
- name: static-files
mountPath: /usr/share/nginx/html
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 2
periodSeconds: 5
volumes:
- name: nginx-conf
configMap:
name: nginx-conf
- name: static-files
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: nginx
spec:
selector:
app: nginx
ports:
- port: 80
targetPort: 80
type: ClusterIPMount your custom nginx.conf or default.conf via ConfigMap. The key insight: mount to /etc/nginx/conf.d/ for server blocks, or replace /etc/nginx/nginx.conf entirely if you need to change worker processes or global settings. Do not try to do both – if you replace nginx.conf, make sure it includes conf.d/*.conf or your ConfigMap server blocks will be ignored.
After updating a ConfigMap, pods do not automatically reload nginx. You need to either restart the Deployment or add a checksum annotation to trigger a rollout:
kubectl rollout restart deployment/nginxNginx as an Ingress Controller#
The nginx Ingress controller is a fundamentally different deployment. It runs the community ingress-nginx controller, which watches Ingress resources across the cluster and dynamically generates nginx configuration.
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx \
--create-namespace \
--set controller.replicaCount=2 \
--set controller.resources.requests.cpu=100m \
--set controller.resources.requests.memory=256MiThis creates a LoadBalancer Service (on cloud) or NodePort (on bare metal / minikube). The controller automatically creates an IngressClass named nginx.
Verify the installation:
kubectl get ingressclass
kubectl get pods -n ingress-nginx
kubectl get svc -n ingress-nginxSSL Termination#
With the Ingress controller, TLS is handled via Ingress resources and Secrets:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- app.example.com
secretName: app-tls
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-app
port:
number: 80Create the TLS secret from your certificate and key:
kubectl create secret tls app-tls --cert=tls.crt --key=tls.key -n defaultFor automatic certificates, pair with cert-manager using the cert-manager.io/cluster-issuer annotation.
Rate Limiting#
Apply rate limiting per Ingress resource using annotations:
annotations:
nginx.ingress.kubernetes.io/limit-rps: "10"
nginx.ingress.kubernetes.io/limit-rpm: "300"
nginx.ingress.kubernetes.io/limit-burst-multiplier: "5"
nginx.ingress.kubernetes.io/limit-whitelist: "10.0.0.0/8"This limits each client IP to 10 requests per second with a burst of 50 (5x multiplier). The whitelist excludes internal IPs from rate limiting. Rate limiting state is per-controller-pod, so with multiple replicas behind a LoadBalancer, the effective limit is multiplied by the number of replicas.
Custom Error Pages#
Serve custom error pages by deploying a default backend:
helm upgrade ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx \
--set controller.customErrors.enabled=true \
--set defaultBackend.enabled=true \
--set defaultBackend.image.repository=your-registry/custom-errors \
--set defaultBackend.image.tag=latestOr per-Ingress, point errors to a custom backend:
annotations:
nginx.ingress.kubernetes.io/custom-http-errors: "404,503"
nginx.ingress.kubernetes.io/default-backend: custom-error-svcThe custom error service must return appropriate content based on the X-Code header that nginx-ingress forwards.
When to Use Which#
Use a plain nginx Deployment when you need a dedicated reverse proxy or static file server for a single application. You control the full nginx.conf, can add Lua modules, and it scales independently of other services. This is the right choice for an application-specific sidecar or frontend server.
Use the nginx Ingress controller when you need cluster-wide HTTP routing across multiple services. It provides a single entry point, shared TLS termination, and consistent annotations across all your Ingress resources. Do not deploy one per application – deploy one (or two for HA) per cluster and let all services share it.
Do not mix them up. The nginx Docker image does not process Ingress resources. The Ingress controller uses registry.k8s.io/ingress-nginx/controller, a purpose-built binary that watches the Kubernetes API. Conversely, do not use the Ingress controller just to reverse-proxy one service – a Deployment with a ConfigMap is lighter.