Deploy NGINX Ingress Controller and cert-manager using Helm to automatically provision and manage SSL certificates for your Kubernetes applications with Let's Encrypt integration.
Prerequisites
- Running Kubernetes cluster with kubectl access
- Administrative privileges on cluster
- Domain names with DNS pointing to cluster IP
- 2+ CPU cores and 4GB RAM available
What this solves
Running web applications on Kubernetes requires external access and SSL certificates. This tutorial sets up NGINX Ingress Controller to route traffic to your services and cert-manager to automatically provision, renew, and manage SSL certificates from Let's Encrypt. You'll have a production-ready ingress setup that handles HTTPS termination and certificate lifecycle management without manual intervention.
Prerequisites
You need a working Kubernetes cluster with kubectl configured and administrative access. Your cluster should have at least 2 CPU cores and 4GB RAM available for the ingress components. DNS records for your domains must point to your cluster's external IP address.
Step-by-step installation
Install Helm package manager
Helm simplifies Kubernetes application deployment and management. Install the latest version and verify it can communicate with your cluster.
curl https://baltocdn.com/helm/signing.asc | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg > /dev/null
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
sudo apt update
sudo apt install -y helm
Verify Helm installation and cluster connectivity:
helm version
kubectl cluster-info
Add required Helm repositories
Add the official repositories for NGINX Ingress Controller and cert-manager. These repos contain the pre-configured charts for easy deployment.
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo add jetstack https://charts.jetstack.io
helm repo update
Create dedicated namespaces
Separate namespaces provide isolation and make resource management easier. Create namespaces for the ingress controller and certificate management.
kubectl create namespace ingress-nginx
kubectl create namespace cert-manager
Install NGINX Ingress Controller
Deploy the ingress controller with optimized settings for production use. This creates a LoadBalancer service that provides external access to your cluster.
helm install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx \
--set controller.metrics.enabled=true \
--set controller.podSecurityContext.runAsUser=101 \
--set controller.podSecurityContext.runAsNonRoot=true \
--set controller.service.type=LoadBalancer \
--set controller.config.use-forwarded-headers="true" \
--set controller.config.compute-full-forwarded-for="true"
Wait for ingress controller deployment
The ingress controller needs time to start and obtain an external IP address. Monitor the deployment status and note the external IP when available.
kubectl get pods -n ingress-nginx
kubectl get services -n ingress-nginx --watch
Wait until you see an external IP assigned to the ingress-nginx-controller service. Press Ctrl+C to stop watching once the IP appears.
Install cert-manager
Install cert-manager to handle automatic SSL certificate provisioning and renewal. This includes custom resource definitions for certificate management.
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--set crds.enabled=true \
--set prometheus.enabled=true \
--set webhook.timeoutSeconds=4
Verify cert-manager installation
Check that all cert-manager components are running properly before proceeding to certificate configuration.
kubectl get pods -n cert-manager
kubectl get crd | grep cert-manager
Create Let's Encrypt cluster issuer
Configure cert-manager to use Let's Encrypt for certificate issuance. Replace the email address with your own for certificate notifications.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@example.com
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
class: nginx
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: admin@example.com
privateKeySecretRef:
name: letsencrypt-staging
solvers:
- http01:
ingress:
class: nginx
Apply the issuer configuration:
kubectl apply -f /tmp/letsencrypt-issuer.yaml
Configure ingress with SSL certificates
Create a sample application
Deploy a test application to verify the ingress and certificate setup works correctly.
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello-world
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: hello-world
template:
metadata:
labels:
app: hello-world
spec:
containers:
- name: hello-world
image: nginxdemos/hello
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: hello-world
namespace: default
spec:
selector:
app: hello-world
ports:
- port: 80
targetPort: 80
type: ClusterIP
kubectl apply -f /tmp/sample-app.yaml
Create ingress resource with TLS
Configure an ingress resource that automatically provisions SSL certificates. Replace example.com with your actual domain name.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: hello-world-ingress
namespace: default
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-staging
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
spec:
tls:
- hosts:
- hello.example.com
secretName: hello-world-tls
rules:
- host: hello.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: hello-world
port:
number: 80
kubectl apply -f /tmp/sample-ingress.yaml
Monitor certificate provisioning
Watch cert-manager automatically request and provision the SSL certificate. This process typically takes 1-2 minutes.
kubectl get certificates -n default
kubectl describe certificate hello-world-tls -n default
kubectl get certificaterequests -n default
Set up monitoring and alerting
Enable ingress controller metrics
Configure monitoring endpoints for the ingress controller to track request metrics and performance.
kubectl get service -n ingress-nginx ingress-nginx-controller-metrics
kubectl port-forward -n ingress-nginx service/ingress-nginx-controller-metrics 10254:10254 &
Test metrics endpoint:
curl http://localhost:10254/metrics | head -20
pkill -f "kubectl port-forward"
Create certificate monitoring
Set up monitoring for certificate expiration and renewal status to ensure continuous HTTPS availability.
apiVersion: v1
kind: ConfigMap
metadata:
name: cert-monitor-script
namespace: cert-manager
data:
monitor.sh: |
#!/bin/bash
echo "Certificate Status Summary:"
kubectl get certificates --all-namespaces -o custom-columns="NAMESPACE:.metadata.namespace,NAME:.metadata.name,READY:.status.conditions[0].status,AGE:.metadata.creationTimestamp"
echo "\nCertificate Details:"
kubectl get certificates --all-namespaces -o yaml | grep -E "(name:|notAfter:|issuerRef:)"
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: cert-monitor
namespace: cert-manager
spec:
schedule: "0 8 *"
jobTemplate:
spec:
template:
spec:
serviceAccountName: cert-manager
containers:
- name: monitor
image: bitnami/kubectl:latest
command: ["/bin/sh"]
args: ["/scripts/monitor.sh"]
volumeMounts:
- name: script-volume
mountPath: /scripts
volumes:
- name: script-volume
configMap:
name: cert-monitor-script
defaultMode: 0755
restartPolicy: OnFailure
kubectl apply -f /tmp/cert-monitor.yaml
Verify your setup
Test that your ingress controller and certificate management are working correctly:
# Check ingress controller status
kubectl get pods -n ingress-nginx
kubectl get services -n ingress-nginx
Verify cert-manager components
kubectl get pods -n cert-manager
kubectl get clusterissuers
Check certificate status
kubectl get certificates --all-namespaces
kubectl describe certificate hello-world-tls -n default
Test SSL endpoint (replace with your domain)
curl -I https://hello.example.com
openssl s_client -connect hello.example.com:443 -servername hello.example.com
Configure multiple domains and services
Create multi-service ingress
Configure ingress to handle multiple applications and domains with individual SSL certificates.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: multi-service-ingress
namespace: default
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/rewrite-target: /$1
spec:
tls:
- hosts:
- api.example.com
secretName: api-example-tls
- hosts:
- app.example.com
secretName: app-example-tls
rules:
- host: api.example.com
http:
paths:
- path: /(.*)
pathType: Prefix
backend:
service:
name: api-service
port:
number: 8080
- host: app.example.com
http:
paths:
- path: /(.*)
pathType: Prefix
backend:
service:
name: web-service
port:
number: 80
Troubleshooting and maintenance
Configure log monitoring
Set up log collection for ingress controller debugging and traffic analysis.
# View ingress controller logs
kubectl logs -n ingress-nginx deployment/ingress-nginx-controller -f
Check cert-manager logs
kubectl logs -n cert-manager deployment/cert-manager -f
Monitor certificate challenges
kubectl get challenges --all-namespaces
kubectl describe challenge -n default
Set up automatic certificate renewal testing
Create a job to periodically test certificate renewal processes and verify ACME challenge handling.
apiVersion: batch/v1
kind: Job
metadata:
name: cert-renewal-test
namespace: cert-manager
spec:
template:
spec:
serviceAccountName: cert-manager
containers:
- name: cert-test
image: bitnami/kubectl:latest
command: ["/bin/sh", "-c"]
args:
- |
echo "Testing certificate renewal..."
kubectl get certificates --all-namespaces
kubectl get certificaterequests --all-namespaces
kubectl get challenges --all-namespaces
echo "Certificate test completed"
restartPolicy: Never
backoffLimit: 3
kubectl apply -f /tmp/cert-test.yaml
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Certificate stuck in pending | DNS not pointing to ingress IP | Verify DNS records with nslookup domain.com |
| 502 Bad Gateway errors | Backend service not reachable | Check service endpoints with kubectl get endpoints |
| Let's Encrypt rate limiting | Too many certificate requests | Use staging issuer for testing, wait for rate limit reset |
| ACME challenge failures | Ingress controller not routing challenges | Check ingress controller logs and verify .well-known/acme-challenge path |
| Certificate not auto-renewing | cert-manager webhook issues | Restart cert-manager pods: kubectl rollout restart -n cert-manager deployment/cert-manager |
| External IP not assigned | LoadBalancer service pending | Check cloud provider load balancer quota and configuration |
Next steps
- Monitor Kubernetes clusters with Prometheus and Grafana for container orchestration insights
- Configure Kubernetes network policies with Calico CNI for microsegmentation and security enforcement
- Setup Kubernetes cluster autoscaling with External DNS for production workloads
- Implement Kubernetes RBAC and security policies for multi-tenant environments
- Configure Istio service mesh with ingress gateway for advanced traffic management
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# Global variables
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TEMP_DIR=""
EMAIL=""
EXTERNAL_IP=""
# Usage function
usage() {
echo "Usage: $0 --email <email> [--external-ip <ip>]"
echo " --email Email address for Let's Encrypt notifications"
echo " --external-ip Optional: External IP for validation (auto-detected if not provided)"
echo " --help Show this help message"
exit 1
}
# Logging functions
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Cleanup function
cleanup() {
if [[ -n "${TEMP_DIR:-}" && -d "${TEMP_DIR:-}" ]]; then
rm -rf "$TEMP_DIR"
fi
}
# Error handler
error_handler() {
local line_no=$1
log_error "Script failed at line $line_no. Cleaning up..."
cleanup
exit 1
}
trap 'error_handler $LINENO' ERR
trap cleanup EXIT
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--email)
EMAIL="$2"
shift 2
;;
--external-ip)
EXTERNAL_IP="$2"
shift 2
;;
--help)
usage
;;
*)
log_error "Unknown argument: $1"
usage
;;
esac
done
# Validate required arguments
if [[ -z "$EMAIL" ]]; then
log_error "Email address is required"
usage
fi
if [[ ! "$EMAIL" =~ ^[^@]+@[^@]+\.[^@]+$ ]]; then
log_error "Invalid email format"
exit 1
fi
# Check prerequisites
echo -e "${BLUE}[1/10]${NC} Checking prerequisites..."
if [[ $EUID -eq 0 ]]; then
SUDO=""
else
if ! command -v sudo >/dev/null 2>&1; then
log_error "This script requires sudo or root privileges"
exit 1
fi
SUDO="sudo"
fi
# Detect distribution
if [[ ! -f /etc/os-release ]]; then
log_error "/etc/os-release not found. Cannot detect distribution."
exit 1
fi
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="$SUDO apt update"
PKG_INSTALL="$SUDO apt install -y"
;;
almalinux|rocky|centos|rhel|ol)
PKG_MGR="dnf"
PKG_UPDATE="$SUDO dnf check-update || true"
PKG_INSTALL="$SUDO dnf install -y"
;;
fedora)
PKG_MGR="dnf"
PKG_UPDATE="$SUDO dnf check-update || true"
PKG_INSTALL="$SUDO dnf install -y"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="$SUDO yum check-update || true"
PKG_INSTALL="$SUDO yum install -y"
;;
*)
log_error "Unsupported distribution: $ID"
exit 1
;;
esac
log_success "Detected distribution: $PRETTY_NAME"
# Check kubectl
if ! command -v kubectl >/dev/null 2>&1; then
log_error "kubectl is not installed or not in PATH"
exit 1
fi
# Test kubectl connectivity
if ! kubectl cluster-info >/dev/null 2>&1; then
log_error "kubectl cannot connect to cluster. Please check your kubeconfig"
exit 1
fi
log_success "Prerequisites check passed"
# Update package manager
echo -e "${BLUE}[2/10]${NC} Updating package manager..."
$PKG_UPDATE
log_success "Package manager updated"
# Install required packages
echo -e "${BLUE}[3/10]${NC} Installing required packages..."
if [[ "$PKG_MGR" == "apt" ]]; then
$PKG_INSTALL curl gnupg software-properties-common
else
$PKG_INSTALL curl gnupg
fi
log_success "Required packages installed"
# Install Helm
echo -e "${BLUE}[4/10]${NC} Installing Helm..."
TEMP_DIR=$(mktemp -d)
if [[ "$PKG_MGR" == "apt" ]]; then
curl -fsSL https://baltocdn.com/helm/signing.asc | $SUDO gpg --dearmor -o /usr/share/keyrings/helm.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] https://baltocdn.com/helm/stable/debian/ all main" | $SUDO tee /etc/apt/sources.list.d/helm-stable-debian.list > /dev/null
$SUDO apt update
$PKG_INSTALL helm
else
curl -fsSL -o "$TEMP_DIR/get_helm.sh" https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod 755 "$TEMP_DIR/get_helm.sh"
HELM_INSTALL_DIR=/usr/local/bin $SUDO "$TEMP_DIR/get_helm.sh"
fi
# Verify Helm installation
if ! helm version >/dev/null 2>&1; then
log_error "Helm installation failed"
exit 1
fi
log_success "Helm installed successfully"
# Add Helm repositories
echo -e "${BLUE}[5/10]${NC} Adding Helm repositories..."
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo add jetstack https://charts.jetstack.io
helm repo update
log_success "Helm repositories added"
# Create namespaces
echo -e "${BLUE}[6/10]${NC} Creating namespaces..."
kubectl create namespace ingress-nginx --dry-run=client -o yaml | kubectl apply -f -
kubectl create namespace cert-manager --dry-run=client -o yaml | kubectl apply -f -
log_success "Namespaces created"
# Install NGINX Ingress Controller
echo -e "${BLUE}[7/10]${NC} Installing NGINX Ingress Controller..."
helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx \
--set controller.metrics.enabled=true \
--set controller.podSecurityContext.runAsUser=101 \
--set controller.podSecurityContext.runAsNonRoot=true \
--set controller.service.type=LoadBalancer \
--set controller.config.use-forwarded-headers="true" \
--set controller.config.compute-full-forwarded-for="true" \
--wait --timeout=300s
log_success "NGINX Ingress Controller installed"
# Wait for external IP
echo -e "${BLUE}[8/10]${NC} Waiting for external IP assignment..."
if [[ -z "$EXTERNAL_IP" ]]; then
log_info "Waiting for LoadBalancer to assign external IP (this may take a few minutes)..."
for i in {1..60}; do
EXTERNAL_IP=$(kubectl get service ingress-nginx-controller -n ingress-nginx -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || echo "")
if [[ -n "$EXTERNAL_IP" ]]; then
break
fi
sleep 5
echo -n "."
done
echo
fi
if [[ -n "$EXTERNAL_IP" ]]; then
log_success "External IP assigned: $EXTERNAL_IP"
else
log_warning "External IP not yet assigned. You may need to wait longer or configure your cloud provider"
fi
# Install cert-manager
echo -e "${BLUE}[9/10]${NC} Installing cert-manager..."
helm upgrade --install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--set crds.enabled=true \
--set prometheus.enabled=true \
--set webhook.timeoutSeconds=4 \
--wait --timeout=300s
log_success "cert-manager installed"
# Create Let's Encrypt ClusterIssuer
echo -e "${BLUE}[10/10]${NC} Creating Let's Encrypt ClusterIssuer..."
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
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
class: nginx
EOF
log_success "Let's Encrypt ClusterIssuer created"
# Verification
echo -e "\n${GREEN}=== Installation Complete ===${NC}"
echo -e "\n${BLUE}Verification Results:${NC}"
# Check ingress controller
if kubectl get pods -n ingress-nginx | grep -q "Running"; then
log_success "NGINX Ingress Controller is running"
else
log_warning "NGINX Ingress Controller pods may not be ready yet"
fi
# Check cert-manager
if kubectl get pods -n cert-manager | grep -q "Running"; then
log_success "cert-manager is running"
else
log_warning "cert-manager pods may not be ready yet"
fi
# Check ClusterIssuer
if kubectl get clusterissuer letsencrypt-prod >/dev/null 2>&1; then
log_success "Let's Encrypt ClusterIssuer created"
else
log_warning "ClusterIssuer not found"
fi
echo -e "\n${YELLOW}Next Steps:${NC}"
echo "1. Ensure your DNS records point to the external IP: ${EXTERNAL_IP:-'<pending>'}"
echo "2. Create Ingress resources with TLS configuration"
echo "3. Use annotation 'cert-manager.io/cluster-issuer: letsencrypt-prod' for automatic SSL"
echo "4. Monitor certificate status with: kubectl get certificates -A"
echo -e "\n${GREEN}Setup completed successfully!${NC}"
Review the script before running. Execute with: bash install.sh