Learn to deploy NGINX Ingress Controller with cert-manager for automatic SSL certificate provisioning and renewal using Let's Encrypt in production Kubernetes clusters.
Prerequisites
- Running Kubernetes cluster with kubectl access
- Domain name with DNS management access
- LoadBalancer support (cloud) or NodePort access (bare metal)
What this solves
Managing SSL certificates manually in Kubernetes is time-consuming and error-prone. This tutorial shows you how to deploy NGINX Ingress Controller with cert-manager to automatically provision, renew, and manage SSL certificates from Let's Encrypt for your Kubernetes applications.
Step-by-step installation
Install Helm package manager
Helm simplifies Kubernetes application deployment and management. We'll use it to install both NGINX Ingress Controller and cert-manager.
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
Add required Helm repositories
Add the official repositories for NGINX Ingress Controller and cert-manager to your Helm configuration.
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo add jetstack https://charts.jetstack.io
helm repo update
Install NGINX Ingress Controller
Deploy the NGINX Ingress Controller with LoadBalancer service type for cloud environments or NodePort for bare metal clusters.
kubectl create namespace ingress-nginx
helm install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx \
--set controller.replicaCount=2 \
--set controller.nodeSelector."kubernetes\.io/os"=linux \
--set defaultBackend.nodeSelector."kubernetes\.io/os"=linux \
--set controller.admissionWebhooks.patch.nodeSelector."kubernetes\.io/os"=linux
Verify NGINX Ingress Controller deployment
Check that the ingress controller pods are running and the LoadBalancer service has an external IP assigned.
kubectl get pods -n ingress-nginx
kubectl get svc -n ingress-nginx
Install cert-manager CRDs
Install the Custom Resource Definitions (CRDs) required by cert-manager before installing the main application.
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.2/cert-manager.crds.yaml
Install cert-manager
Deploy cert-manager in its own namespace with webhook validation enabled for secure certificate management.
kubectl create namespace cert-manager
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--version v1.13.2 \
--set installCRDs=false \
--set nodeSelector."kubernetes\.io/os"=linux
Create ClusterIssuer for Let's Encrypt staging
Create a staging ClusterIssuer to test certificate issuance without hitting Let's Encrypt rate limits during development.
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
kubectl apply -f letsencrypt-staging.yaml
Create ClusterIssuer for Let's Encrypt production
Create a production ClusterIssuer for real SSL certificates. Only use this after testing with the staging issuer.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-production
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@example.com
privateKeySecretRef:
name: letsencrypt-production
solvers:
- http01:
ingress:
class: nginx
kubectl apply -f letsencrypt-production.yaml
Configure DNS-01 challenge solver (optional)
For wildcard certificates or when HTTP-01 validation isn't possible, configure DNS-01 challenge solver with your DNS provider. This example shows Cloudflare configuration.
apiVersion: v1
kind: Secret
metadata:
name: cloudflare-api-token-secret
namespace: cert-manager
type: Opaque
stringData:
api-token: your-cloudflare-api-token
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-dns01
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@example.com
privateKeySecretRef:
name: letsencrypt-dns01
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token-secret
key: api-token
kubectl apply -f dns01-cloudflare-secret.yaml
kubectl apply -f dns01-clusterissuer.yaml
Create test application with automatic SSL
Deploy a test application with an Ingress resource that automatically provisions an SSL certificate using the staging issuer.
apiVersion: apps/v1
kind: Deployment
metadata:
name: test-app
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: test-app
template:
metadata:
labels:
app: test-app
spec:
containers:
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: test-app-service
namespace: default
spec:
selector:
app: test-app
ports:
- protocol: TCP
port: 80
targetPort: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: test-app-ingress
namespace: default
annotations:
cert-manager.io/cluster-issuer: letsencrypt-staging
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- test.example.com
secretName: test-app-tls
rules:
- host: test.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: test-app-service
port:
number: 80
kubectl apply -f test-app.yaml
Configure automatic certificate renewal monitoring
Create a monitoring setup to track certificate renewal status and alert on failures using Prometheus metrics.
apiVersion: v1
kind: ServiceMonitor
metadata:
name: cert-manager-metrics
namespace: cert-manager
spec:
selector:
matchLabels:
app.kubernetes.io/name: cert-manager
endpoints:
- port: tcp-prometheus-servicemonitor
interval: 60s
path: /metrics
kubectl apply -f cert-monitor.yaml
Verify your setup
Check that all components are running correctly and certificates are being issued properly.
# Check NGINX Ingress Controller status
kubectl get pods -n ingress-nginx
kubectl get svc -n ingress-nginx
Check cert-manager status
kubectl get pods -n cert-manager
kubectl get clusterissuers
Check certificate status
kubectl get certificates -A
kubectl describe certificate test-app-tls
Check certificate request details
kubectl get certificaterequests -A
kubectl describe certificaterequest -n default
Test SSL certificate functionality by accessing your application:
curl -H "Host: test.example.com" https://YOUR_INGRESS_IP/ -k -v
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Certificate stuck in Pending state | DNS not pointing to ingress IP | Verify DNS records point to LoadBalancer external IP |
| HTTP-01 challenge fails | Ingress controller not receiving traffic | Check LoadBalancer/NodePort service and firewall rules |
| cert-manager webhook errors | Network policies blocking admission webhooks | Allow webhook traffic on port 10250 and DNS resolution |
| Rate limit errors from Let's Encrypt | Too many certificate requests | Use staging issuer for testing, wait for rate limit reset |
| ClusterIssuer not ready | Invalid email or ACME server unreachable | Check email format and network connectivity |
| Ingress shows 404 errors | Ingress class not specified correctly | Add ingressClassName: nginx to Ingress spec |
Advanced configuration options
Configure certificate renewal alerts
Set up PrometheusRule to alert when certificates are close to expiry or renewal failures occur.
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: cert-manager-alerts
namespace: cert-manager
spec:
groups:
- name: cert-manager
rules:
- alert: CertManagerCertExpirySoon
expr: certmanager_certificate_expiration_timestamp_seconds - time() < 86400 * 7
for: 5m
labels:
severity: warning
annotations:
summary: "Certificate expires soon"
description: "Certificate {{ $labels.name }} in namespace {{ $labels.namespace }} expires in less than 7 days"
- alert: CertManagerCertNotReady
expr: certmanager_certificate_ready_status == 0
for: 10m
labels:
severity: critical
annotations:
summary: "Certificate not ready"
description: "Certificate {{ $labels.name }} in namespace {{ $labels.namespace }} is not ready"
kubectl apply -f cert-alerts.yaml
Configure resource limits and requests
Set appropriate resource limits for cert-manager and NGINX Ingress Controller to ensure stable operation.
# Update cert-manager with resource limits
helm upgrade cert-manager jetstack/cert-manager \
--namespace cert-manager \
--reuse-values \
--set resources.requests.cpu=100m \
--set resources.requests.memory=128Mi \
--set resources.limits.cpu=200m \
--set resources.limits.memory=256Mi
Update ingress-nginx with resource limits
helm upgrade ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx \
--reuse-values \
--set controller.resources.requests.cpu=200m \
--set controller.resources.requests.memory=256Mi \
--set controller.resources.limits.cpu=500m \
--set controller.resources.limits.memory=512Mi
Next steps
- Monitor Kubernetes clusters with Prometheus and Grafana for container orchestration insights
- Implement Kubernetes Pod Security Standards and admission controllers for policy enforcement
- Configure Kubernetes network policies with Calico CNI for microsegmentation and security enforcement
- Setup Kubernetes backup automation with Velero for disaster recovery
- Configure Kubernetes External DNS for automatic DNS record management
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Script configuration
SCRIPT_NAME=$(basename "$0")
CERT_MANAGER_VERSION="v1.13.2"
EMAIL=""
SERVICE_TYPE="LoadBalancer"
REPLICAS=2
# Usage function
usage() {
echo "Usage: $SCRIPT_NAME -e EMAIL [OPTIONS]"
echo ""
echo "Required:"
echo " -e EMAIL Email address for Let's Encrypt notifications"
echo ""
echo "Options:"
echo " -t SERVICE_TYPE Service type: LoadBalancer (default) or NodePort"
echo " -r REPLICAS Number of ingress controller replicas (default: 2)"
echo " -h Show this help message"
echo ""
echo "Examples:"
echo " $SCRIPT_NAME -e admin@example.com"
echo " $SCRIPT_NAME -e admin@example.com -t NodePort"
exit 1
}
# Parse command line arguments
while getopts "e:t:r:h" opt; do
case $opt in
e) EMAIL="$OPTARG" ;;
t) SERVICE_TYPE="$OPTARG" ;;
r) REPLICAS="$OPTARG" ;;
h) usage ;;
*) usage ;;
esac
done
# Validate required arguments
if [[ -z "$EMAIL" ]]; then
echo -e "${RED}Error: Email address is required${NC}"
usage
fi
# Validate email format
if [[ ! "$EMAIL" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
echo -e "${RED}Error: Invalid email format${NC}"
exit 1
fi
# Validate service type
if [[ "$SERVICE_TYPE" != "LoadBalancer" && "$SERVICE_TYPE" != "NodePort" ]]; then
echo -e "${RED}Error: Service type must be LoadBalancer or NodePort${NC}"
exit 1
fi
# Print configuration
print_info() {
echo -e "${BLUE}================================================${NC}"
echo -e "${BLUE}Kubernetes Ingress NGINX + cert-manager Setup${NC}"
echo -e "${BLUE}================================================${NC}"
echo -e "Email: ${GREEN}$EMAIL${NC}"
echo -e "Service Type: ${GREEN}$SERVICE_TYPE${NC}"
echo -e "Replicas: ${GREEN}$REPLICAS${NC}"
echo -e "cert-manager Version: ${GREEN}$CERT_MANAGER_VERSION${NC}"
echo ""
}
# Cleanup function
cleanup() {
local exit_code=$?
if [[ $exit_code -ne 0 ]]; then
echo -e "\n${RED}Installation failed. Cleaning up...${NC}"
helm uninstall cert-manager -n cert-manager 2>/dev/null || true
helm uninstall ingress-nginx -n ingress-nginx 2>/dev/null || true
kubectl delete namespace cert-manager 2>/dev/null || true
kubectl delete namespace ingress-nginx 2>/dev/null || true
kubectl delete -f https://github.com/cert-manager/cert-manager/releases/download/$CERT_MANAGER_VERSION/cert-manager.crds.yaml 2>/dev/null || true
fi
exit $exit_code
}
trap cleanup ERR
# Auto-detect distribution
detect_distro() {
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update"
PKG_INSTALL="apt install -y"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf makecache"
PKG_INSTALL="dnf install -y"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum makecache"
PKG_INSTALL="yum install -y"
;;
*)
echo -e "${RED}Error: Unsupported distribution: $ID${NC}"
exit 1
;;
esac
else
echo -e "${RED}Error: Cannot detect distribution${NC}"
exit 1
fi
}
# Check prerequisites
check_prerequisites() {
echo -e "${YELLOW}[1/9] Checking prerequisites...${NC}"
# Check if running as root or with sudo
if [[ $EUID -eq 0 ]]; then
SUDO=""
elif command -v sudo >/dev/null 2>&1; then
SUDO="sudo"
else
echo -e "${RED}Error: This script requires root privileges or sudo${NC}"
exit 1
fi
# Check if kubectl is available and configured
if ! command -v kubectl >/dev/null 2>&1; then
echo -e "${RED}Error: kubectl is not installed or not in PATH${NC}"
exit 1
fi
# Check if kubectl can connect to cluster
if ! kubectl cluster-info >/dev/null 2>&1; then
echo -e "${RED}Error: kubectl cannot connect to Kubernetes cluster${NC}"
exit 1
fi
# Check required tools
for tool in curl gpg; do
if ! command -v $tool >/dev/null 2>&1; then
echo -e "${RED}Error: $tool is required but not installed${NC}"
exit 1
fi
done
echo -e "${GREEN}Prerequisites check passed${NC}"
}
# Install Helm
install_helm() {
echo -e "${YELLOW}[2/9] Installing Helm package manager...${NC}"
if command -v helm >/dev/null 2>&1; then
echo -e "${GREEN}Helm is already installed${NC}"
return 0
fi
case "$PKG_MGR" in
apt)
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 $PKG_UPDATE
$SUDO $PKG_INSTALL helm
;;
*)
curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
;;
esac
echo -e "${GREEN}Helm installation completed${NC}"
}
# Add Helm repositories
add_helm_repos() {
echo -e "${YELLOW}[3/9] Adding Helm repositories...${NC}"
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo add jetstack https://charts.jetstack.io
helm repo update
echo -e "${GREEN}Helm repositories added and updated${NC}"
}
# Install NGINX Ingress Controller
install_nginx_ingress() {
echo -e "${YELLOW}[4/9] Installing NGINX Ingress Controller...${NC}"
kubectl create namespace ingress-nginx --dry-run=client -o yaml | kubectl apply -f -
local helm_args=(
--namespace ingress-nginx
--set controller.replicaCount=$REPLICAS
--set controller.nodeSelector."kubernetes\.io/os"=linux
--set defaultBackend.nodeSelector."kubernetes\.io/os"=linux
--set controller.admissionWebhooks.patch.nodeSelector."kubernetes\.io/os"=linux
)
if [[ "$SERVICE_TYPE" == "NodePort" ]]; then
helm_args+=(--set controller.service.type=NodePort)
fi
helm install ingress-nginx ingress-nginx/ingress-nginx "${helm_args[@]}"
echo -e "${GREEN}NGINX Ingress Controller installation initiated${NC}"
}
# Verify NGINX Ingress Controller
verify_nginx_ingress() {
echo -e "${YELLOW}[5/9] Verifying NGINX Ingress Controller deployment...${NC}"
# Wait for pods to be ready
kubectl wait --namespace ingress-nginx \
--for=condition=ready pod \
--selector=app.kubernetes.io/component=controller \
--timeout=300s
kubectl get pods -n ingress-nginx
kubectl get svc -n ingress-nginx
echo -e "${GREEN}NGINX Ingress Controller verification completed${NC}"
}
# Install cert-manager CRDs
install_certmanager_crds() {
echo -e "${YELLOW}[6/9] Installing cert-manager CRDs...${NC}"
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/$CERT_MANAGER_VERSION/cert-manager.crds.yaml
echo -e "${GREEN}cert-manager CRDs installed${NC}"
}
# Install cert-manager
install_certmanager() {
echo -e "${YELLOW}[7/9] Installing cert-manager...${NC}"
kubectl create namespace cert-manager --dry-run=client -o yaml | kubectl apply -f -
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--version $CERT_MANAGER_VERSION \
--set installCRDs=false \
--set nodeSelector."kubernetes\.io/os"=linux
# Wait for cert-manager to be ready
kubectl wait --namespace cert-manager \
--for=condition=ready pod \
--selector=app.kubernetes.io/name=cert-manager \
--timeout=300s
echo -e "${GREEN}cert-manager installation completed${NC}"
}
# Create ClusterIssuers
create_cluster_issuers() {
echo -e "${YELLOW}[8/9] Creating Let's Encrypt ClusterIssuers...${NC}"
# Create staging ClusterIssuer
cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: $EMAIL
privateKeySecretRef:
name: letsencrypt-staging
solvers:
- http01:
ingress:
class: nginx
EOF
# Create production ClusterIssuer
cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-production
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: $EMAIL
privateKeySecretRef:
name: letsencrypt-production
solvers:
- http01:
ingress:
class: nginx
EOF
echo -e "${GREEN}ClusterIssuers created successfully${NC}"
}
# Final verification
final_verification() {
echo -e "${YELLOW}[9/9] Performing final verification...${NC}"
# Check NGINX Ingress Controller
echo "NGINX Ingress Controller status:"
kubectl get pods -n ingress-nginx -o wide
echo ""
# Check cert-manager
echo "cert-manager status:"
kubectl get pods -n cert-manager -o wide
echo ""
# Check ClusterIssuers
echo "ClusterIssuers:"
kubectl get clusterissuers
echo ""
echo -e "${GREEN}Installation completed successfully!${NC}"
echo ""
echo -e "${BLUE}Next steps:${NC}"
echo "1. Create an Ingress resource with TLS configuration"
echo "2. Test with staging issuer first: cert-manager.io/cluster-issuer: letsencrypt-staging"
echo "3. Switch to production issuer: cert-manager.io/cluster-issuer: letsencrypt-production"
echo ""
if [[ "$SERVICE_TYPE" == "NodePort" ]]; then
echo -e "${YELLOW}Note: Using NodePort service type. Configure your load balancer to route traffic to the node ports.${NC}"
fi
}
# Main execution
main() {
print_info
detect_distro
check_prerequisites
install_helm
add_helm_repos
install_nginx_ingress
verify_nginx_ingress
install_certmanager_crds
install_certmanager
create_cluster_issuers
final_verification
}
main "$@"
Review the script before running. Execute with: bash install.sh