How to Install cert-manager on Ubuntu 24.04 VPS: Automatic TLS Certificates for Kubernetes
Manually renewing TLS certificates is a leading cause of avoidable outages. cert-manager removes the problem entirely: it issues, renews, and rotates certificates inside your Kubernetes cluster as native Kubernetes resources, and it is free for unlimited domains when paired with Let's Encrypt. This guide walks you through installing cert-manager on an Ubuntu 24.04 VPS running Kubernetes (k3s or full k8s), configuring ClusterIssuers for Let's Encrypt staging and production, issuing your first certificate, and wiring it into an Ingress with a single annotation.
Need a Kubernetes-ready VPS? The CloudCore Starter plan at EUR 7.99/month gives you 4 vCPU, 6 GB RAM and 100 GB NVMe — plenty to run k3s, cert-manager and a handful of Ingress-backed workloads.
Table of Contents
What is cert-manager?
cert-manager is the de-facto certificate controller for Kubernetes. It extends the Kubernetes API with custom resources — Issuer, ClusterIssuer, Certificate, CertificateRequest, Order, and Challenge — and runs a reconciliation loop that turns declarative certificate requests into real, signed TLS certificates stored as Kubernetes Secret objects. Once a Certificate resource exists, cert-manager handles everything: generating the private key, building the CSR, solving the ACME challenge, fetching the signed certificate, and renewing it 30 days before expiry.
cert-manager supports a broad list of certificate sources. On the public CA side, it speaks ACME natively and therefore works with Let's Encrypt, ZeroSSL, Buypass Go SSL, Google Public CA, and any other ACME v2 endpoint. For private PKI, it integrates with HashiCorp Vault, Venafi TPP/TLS Protect Cloud, AWS Certificate Manager Private CA, Google Cloud Certificate Authority Service, and self-signed CAs for internal services. It can also act as its own CA to issue internal certificates for service mesh workloads (Istio, Linkerd) and mTLS between microservices.
The typical use cases are straightforward. Platform teams use cert-manager to give every developer's Ingress a working TLS certificate without tickets, spreadsheets, or shared passwords. SaaS operators use it to auto-issue per-customer certificates on custom domains — the exact workflow behind multi-tenant platforms that offer "bring your own domain". DevOps teams use it to issue short-lived internal certificates for mTLS inside the cluster, rotating them every few hours. And regulated environments use it with Vault or a private CA to meet compliance requirements while keeping the same declarative Kubernetes-native workflow.
If you are running Kubernetes — whether it is k3s on a single VPS, managed GKE/EKS/AKS, or bare-metal k8s — cert-manager is almost always the correct answer for TLS.
Why Self-Host Free Automatic TLS?
Commercial certificate vendors still charge USD 50-500 per domain per year for DV and OV certificates, plus extra for wildcards and renewal services. cert-manager with Let's Encrypt eliminates the entire line item while removing the human-in-the-loop renewal step that causes outages. Concrete benefits:
- Truly free — Let's Encrypt issues DV certificates at no cost, with no volume caps beyond the public rate limits (50 certificates per registered domain per week, which is more than almost anyone needs).
- Fully automatic renewal — cert-manager renews 30 days before expiry by default. You never touch a cert again after the
Certificateresource is created. - Declarative and GitOps-friendly — Certificates live in YAML, alongside your Deployments and Ingresses. They flow through Argo CD, Flux, and PR reviews like any other manifest.
- Wildcards included — With DNS-01 (for example via Cloudflare), you can issue
*.example.comin the same pipeline with no extra cost. - No single point of failure — No shared admin portal, no expiring credit card, no forgotten renewal emails. The controller runs inside the cluster and keeps working.
- Ingress-native integration — One annotation on your Ingress and the certificate appears. Works with NGINX Ingress, Traefik, HAProxy, Contour, Istio Gateway, and others.
- Short-lived internal certs for mTLS — For zero-trust workloads, cert-manager can issue hours- or minutes-long certificates from a private CA, rotated automatically.
Cost Comparison: cert-manager vs. Paid Certificate Providers
| Scenario | Paid DV (e.g. DigiCert, Sectigo) | Managed DNS+TLS SaaS | cert-manager + Let's Encrypt |
|---|---|---|---|
| Cost per domain / year | USD 50-150 | USD 20-60 (bundled) | Free |
| Wildcard certificate | USD 150-500 extra | Often extra | Free (DNS-01) |
| Renewal workflow | Manual CSR + upload | Vendor UI | Fully automatic |
| GitOps / declarative | No | Partial | Yes (native CRDs) |
| Unlimited subdomains | Per-cert pricing | Plan-limited | Free |
| Private CA / mTLS | Separate product | Rarely supported | Included |
| Monthly cost at 20 domains | USD 100-250 | USD 40-100 | EUR 7.99/mo (VPS only) |
Prerequisites
Before you begin, make sure you have:
- A VPS running Ubuntu 24.04 LTS with root or sudo access
- A running Kubernetes cluster — this guide assumes k3s (see our k3s install guide), but any cluster on v1.27+ works
kubectlconfigured and working —kubectl get nodesshould return at least oneReadynode- An Ingress controller installed — NGINX Ingress, Traefik, or similar (if you prefer a non-Kubernetes reverse proxy, see NGINX Proxy Manager)
- A domain name with DNS records you can modify (A or CNAME pointing at your VPS)
- Ports 80 and 443 open on the VPS firewall (required for HTTP-01 challenges)
Recommended Plan: CloudCore Starter>
For a single-node k3s cluster running cert-manager plus a handful of small workloads, the CloudCore Starter plan is the sweet spot:>
- 4 vCPU cores
- 6 GB RAM
- 100 GB NVMe SSD
- Unmetered bandwidth
- EUR 7.99/month>
Upgrade to CloudCore Professional later if you run more Ingresses and heavier workloads — cert-manager itself uses very little RAM (around 50-150 MB across its pods).
Connect to your server via SSH and confirm your cluster is healthy:
ssh root@your-server-ip
kubectl get nodes
kubectl get pods -AStep 1: Update System Packages
Start by updating your package index and upgrading installed packages. This ensures you have the latest security patches and that the Helm and kubectl client installs below pull stable dependencies.
sudo apt update && sudo apt upgrade -yExpected output (abbreviated):
Hit:1 http://archive.ubuntu.com/ubuntu noble InRelease
Reading package lists... Done
Building dependency tree... Done
Calculating upgrade... Done
0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.If the kernel was updated, reboot and reconnect:
sudo rebootStep 2: Install Helm
cert-manager is distributed as a Helm chart. Helm is the Kubernetes package manager — it takes the guesswork out of installing cert-manager's CRDs, RBAC rules, webhooks, and Deployments in the correct order.
Install Helm using the official script:
curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bashExpected output:
Downloading https://get.helm.sh/helm-v3.14.4-linux-amd64.tar.gz
Verifying checksum... Done.
Preparing to install helm into /usr/local/bin
helm installed into /usr/local/bin/helmVerify the installation:
helm versionExpected output:
version.BuildInfo{Version:"v3.14.4", GitCommit:"...", GoVersion:"go1.21.9"}Add the Jetstack Helm repository (Jetstack is the company that maintains cert-manager) and refresh the local chart index:
helm repo add jetstack https://charts.jetstack.io --force-update
helm repo updateExpected output:
"jetstack" has been added to your repositories
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "jetstack" chart repository
Update Complete. ⎈Happy Helming!⎈Step 3: Install cert-manager via Helm
cert-manager ships CustomResourceDefinitions (CRDs) for Certificate, Issuer, ClusterIssuer, Order, Challenge, and CertificateRequest. You can install them either as part of the Helm release (simplest) or separately (recommended for production so that helm uninstall does not delete your certificates).
For most setups, the one-liner install with --set crds.enabled=true is the right call:
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--version v1.15.3 \
--set crds.enabled=trueExpected output:
NAME: cert-manager LAST DEPLOYED: Thu Apr 16 10:15:00 2026 NAMESPACE: cert-manager STATUS: deployed REVISION: 1 TEST SUITE: None NOTES: cert-manager v1.15.3 has been deployed successfully!In order to begin issuing certificates, you will need to set up a ClusterIssuer or Issuer resource (for example, by creating a 'letsencrypt-staging' issuer).
More information on the different types of issuers and how to configure them can be found in our documentation:
https://cert-manager.io/docs/configuration/
The chart deploys three Deployments in the cert-manager namespace:
cert-manager— the main controller that watchesCertificateandIssuerresources and drives the issuance state machine.cert-manager-webhook— a validating and mutating admission webhook that enforces the schema of cert-manager CRDs.cert-manager-cainjector— injects CA bundles into webhooks, APIServices, and CRDs that need them.
Optional Install Tuning
For production clusters, pin resource limits and enable Prometheus metrics with a values file:
cat > cert-manager-values.yaml <<EOF crds: enabled: true replicaCount: 2 resources: requests: cpu: 10m memory: 64Mi limits: memory: 256Mi prometheus: enabled: true servicemonitor: enabled: true webhook: replicaCount: 2 cainjector: replicaCount: 2 EOF
helm upgrade --install cert-manager jetstack/cert-manager \ --namespace cert-manager \ --create-namespace \ --version v1.15.3 \ -f cert-manager-values.yaml
This bumps each Deployment to two replicas for HA, caps memory, and exposes a ServiceMonitor for Prometheus Operator to scrape.
Step 4: Verify the Installation
Confirm all three cert-manager Deployments are ready:
kubectl get pods -n cert-managerExpected output:
NAME READY STATUS RESTARTS AGE
cert-manager-5c6866597d-abcde 1/1 Running 0 2m
cert-manager-cainjector-6c94df44f7-fghij 1/1 Running 0 2m
cert-manager-webhook-d4f79d7c7-klmno 1/1 Running 0 2mCheck the CRDs were installed:
kubectl get crd | grep cert-managerExpected output:
certificaterequests.cert-manager.io 2026-04-16T10:15:01Z
certificates.cert-manager.io 2026-04-16T10:15:01Z
challenges.acme.cert-manager.io 2026-04-16T10:15:01Z
clusterissuers.cert-manager.io 2026-04-16T10:15:01Z
issuers.cert-manager.io 2026-04-16T10:15:01Z
orders.acme.cert-manager.io 2026-04-16T10:15:01ZSmoke-test the webhook by creating a self-signed test certificate:
cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: test-selfsigned
namespace: default
spec:
selfSigned: {}
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: selfsigned-cert
namespace: default
spec:
dnsNames:
- example.com
secretName: selfsigned-cert-tls
issuerRef:
name: test-selfsigned
EOFWait a few seconds then confirm the certificate is Ready=True:
kubectl get certificate selfsigned-certExpected output:
NAME READY SECRET AGE
selfsigned-cert True selfsigned-cert-tls 5sClean up the smoke test:
kubectl delete certificate selfsigned-cert
kubectl delete issuer test-selfsigned
kubectl delete secret selfsigned-cert-tlsStep 5: Create a Let's Encrypt Staging ClusterIssuer
Always start with Let's Encrypt staging. Production Let's Encrypt enforces strict rate limits (five duplicate certificates per week, 50 certs per registered domain per week). If you misconfigure your DNS or Ingress and spin a retry loop, you will exhaust the quota and be locked out for a week. Staging has much looser limits, issues certificates from a fake root that browsers will not trust, and lets you iterate safely.
Create letsencrypt-staging-clusterissuer.yaml:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: [email protected]
privateKeySecretRef:
name: letsencrypt-staging-account-key
solvers:
- http01:
ingress:
ingressClassName: nginxApply it:
kubectl apply -f letsencrypt-staging-clusterissuer.yamlVerify the issuer registered successfully with Let's Encrypt:
kubectl get clusterissuer letsencrypt-stagingExpected output:
NAME READY AGE
letsencrypt-staging True 15sIf READY shows False, describe the resource to see why — usually a missing Ingress controller, wrong ingressClassName, or firewall blocking outbound HTTPS to Let's Encrypt:
kubectl describe clusterissuer letsencrypt-stagingReplace nginx in ingressClassName with traefik if you are running Traefik instead.
Step 6: Create a Let's Encrypt Production ClusterIssuer
Once staging works end-to-end for a test domain (next step), create the production issuer. The only difference is the ACME server URL.
Create letsencrypt-prod-clusterissuer.yaml:
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-key
solvers:
- http01:
ingress:
ingressClassName: nginxApply:
kubectl apply -f letsencrypt-prod-clusterissuer.yaml
kubectl get clusterissuer letsencrypt-prodExpected output:
NAME READY AGE
letsencrypt-prod True 8sYou now have two ClusterIssuers. Any namespace can reference them by name. The convention across the cert-manager community is to always test with letsencrypt-staging first, then switch the issuerRef to letsencrypt-prod only once you have confirmed the order succeeds.
Step 7: Issue Your First Certificate (HTTP-01)
The HTTP-01 challenge works by Let's Encrypt sending an HTTP request to http://yourdomain.com/.well-known/acme-challenge/<token>. cert-manager creates a temporary Ingress and Service pointing to a solver Pod that serves the expected token. As long as your domain's A record points at the VPS and port 80 is open, the challenge completes in a few seconds.
Ensure DNS is pointing correctly:
dig +short example.com
should return your VPS public IP
Create a Certificate resource in the default namespace:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: example-com-tls
namespace: default
spec:
secretName: example-com-tls
issuerRef:
name: letsencrypt-staging
kind: ClusterIssuer
dnsNames:
- example.com
- www.example.comApply it:
kubectl apply -f example-com-certificate.yamlWatch the issuance progress:
kubectl get certificate,order,challenge -n defaultExpected output during issuance:
NAME READY SECRET AGE certificate.cert-manager.io/example-com-tls False example-com-tls 20sNAME STATE AGE order.acme.cert-manager.io/example-com-tls-1-1234567890 pending 20s
NAME STATE DOMAIN AGE challenge.acme.cert-manager.io/example-com-tls-1-1234567890-1-2345678901 pending example.com 20s
After 20-60 seconds, the certificate should become ready:
NAME READY SECRET AGE
certificate.cert-manager.io/example-com-tls True example-com-tls 2mInspect the resulting Secret — it contains tls.crt and tls.key:
kubectl describe secret example-com-tls -n defaultIf everything worked against staging, edit the Certificate and change issuerRef.name to letsencrypt-prod:
kubectl edit certificate example-com-tls -n defaultThen delete the staging Secret so cert-manager re-issues from production:
kubectl delete secret example-com-tls -n defaultStep 8: Wire Certificates into an Ingress
In practice, you rarely create Certificate resources by hand. Instead, you annotate the Ingress and cert-manager creates the Certificate for you. This is called the Ingress shim.
Example Ingress with automatic TLS:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp
namespace: default
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
tls:
- hosts:
- app.example.com
secretName: myapp-tls
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp-service
port:
number: 80Apply it:
kubectl apply -f myapp-ingress.yamlcert-manager sees the cert-manager.io/cluster-issuer annotation and the tls block, then automatically creates a Certificate named myapp-tls in the same namespace. Within a minute, https://app.example.com serves a valid Let's Encrypt certificate.
Supported annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod— use a ClusterIssuer (cluster-wide).cert-manager.io/issuer: letsencrypt-prod— use a namespaced Issuer.cert-manager.io/common-name: app.example.com— override the CN (rarely needed).cert-manager.io/duration: 2160h— request a specific lifetime (default 90 days for Let's Encrypt; not all CAs honour this).cert-manager.io/renew-before: 720h— renew this many hours before expiry (default 30 days).
Step 9: Configure DNS-01 with Cloudflare
HTTP-01 works beautifully for public HTTP endpoints, but it has two limitations:
*.example.com). Let's Encrypt only allows wildcards via DNS-01.DNS-01 solves both. cert-manager proves domain control by creating a TXT record at _acme-challenge.example.com, Let's Encrypt queries DNS to verify it, and the certificate issues. For DNS-01 you need API access to your DNS provider. Cloudflare is the simplest — a scoped API token takes 30 seconds to create.
Create a Scoped Cloudflare API Token
example.com) — never give it access to "All zones".Store the Token as a Kubernetes Secret
kubectl create secret generic cloudflare-api-token \
--namespace cert-manager \
--from-literal=api-token=YOUR_CLOUDFLARE_TOKEN_HEREImportant: the Secret must live in the cert-manager namespace, not the namespace where your Certificates live. The cert-manager controller reads it directly.
Create a ClusterIssuer Using DNS-01
Create letsencrypt-prod-dns01.yaml:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod-dns01
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: [email protected]
privateKeySecretRef:
name: letsencrypt-prod-dns01-account-key
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
selector:
dnsZones:
- "example.com"The selector.dnsZones block ensures this solver is only used for the example.com zone. You can add multiple solvers to a single issuer — one per zone, or a mix of HTTP-01 and DNS-01.
Apply:
kubectl apply -f letsencrypt-prod-dns01.yaml
kubectl get clusterissuer letsencrypt-prod-dns01Expected output:
NAME READY AGE
letsencrypt-prod-dns01 True 5sStep 10: Issue a Wildcard Certificate
With DNS-01 configured, wildcard certificates are a one-line change:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-example-com
namespace: default
spec:
secretName: wildcard-example-com-tls
issuerRef:
name: letsencrypt-prod-dns01
kind: ClusterIssuer
dnsNames:
- "example.com"
- "*.example.com"Apply:
kubectl apply -f wildcard-cert.yamlWatch the order progress:
kubectl get certificate wildcard-example-com -n default -wDNS-01 is slightly slower than HTTP-01 because cert-manager has to wait for the TXT record to propagate before asking Let's Encrypt to verify. Expect 30-120 seconds typically, longer if your DNS provider has high TTLs.
Once READY=True, the Secret wildcard-example-com-tls can be referenced by any Ingress in the default namespace:
spec:
tls:
- hosts:
- app.example.com
- api.example.com
- admin.example.com
secretName: wildcard-example-com-tlsThis is the ideal setup for multi-tenant SaaS platforms: one wildcard certificate, many subdomains, zero per-tenant certificate provisioning.
Monitoring and Renewal
cert-manager renews certificates 30 days before expiry by default, controlled by spec.renewBefore on the Certificate. The reconcile loop runs every 10 minutes, so renewals happen automatically with zero intervention.
Check All Certificates at a Glance
kubectl get certificate -AExpected output:
NAMESPACE NAME READY SECRET AGE
default example-com-tls True example-com-tls 45d
default wildcard-example-com True wildcard-example-com-tls 40d
monitoring grafana-tls True grafana-tls 12dForce an Immediate Renewal
kubectl cert-manager renew example-com-tls -n defaultThis requires the kubectl cert-manager plugin. Install it with krew:
kubectl krew install cert-managerPrometheus Metrics
If you enabled prometheus.enabled=true in the Helm values, cert-manager exposes:
certmanager_certificate_expiration_timestamp_seconds— per-certificate expiry time.certmanager_certificate_ready_status— 1 if Ready=True.certmanager_http_acme_client_request_count— ACME client request counter by status.
- alert: CertificateExpiringSoon
expr: (certmanager_certificate_expiration_timestamp_seconds - time()) / 86400 < 14
for: 1h
labels:
severity: warning
annotations:
summary: "Certificate {{ $labels.name }} expires in less than 14 days"Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
ClusterIssuer stays Ready=False with ACME account registration failed | Cluster cannot reach Let's Encrypt (firewall / outbound egress) | Test: kubectl run -it --rm test --image=curlimages/curl -- curl -v https://acme-v02.api.letsencrypt.org/directory. Fix egress rules. |
Certificate stuck Ready=False, Challenge shows pending with Waiting for HTTP-01 challenge propagation | Port 80 not reachable from internet, or wrong ingressClassName | curl http://yourdomain.com/.well-known/acme-challenge/test from outside. Confirm ingressClassName matches your installed controller. |
Challenge error: Error presenting challenge: Cloudflare API error: Unauthorized | API token wrong, expired, or missing Zone:DNS:Edit permission | Recreate the token, double-check it was stored in the cert-manager namespace, not the app namespace. |
Order fails with too many certificates already issued for exact set of domains | Let's Encrypt production rate limit (5 duplicates per week) | Wait 7 days, or use staging during testing. Never iterate against production. |
| Certificate renewed but Ingress still serves old cert | Ingress controller has cached the old Secret | Restart the Ingress controller Pods, e.g. kubectl rollout restart deploy/ingress-nginx-controller -n ingress-nginx. |
Certificate annotation ignored on Ingress | Ingress shim disabled or wrong annotation key | Use cert-manager.io/cluster-issuer (not certmanager.k8s.io/... — that key is deprecated). |
Webhook errors: x509: certificate signed by unknown authority | cainjector not running or crashed | kubectl logs -n cert-manager deploy/cert-manager-cainjector. Restart the Deployment. |
Inspecting a Stuck Issuance
cert-manager's state machine produces a chain of resources: Certificate → CertificateRequest → Order → Challenge. When something fails, describe each step to find the error message:
kubectl describe certificate example-com-tls -n default
kubectl describe certificaterequest -n default
kubectl describe order -n default
kubectl describe challenge -n defaultThe failing Challenge almost always has the clearest error in its status.reason.
Controller Logs
kubectl logs -n cert-manager deploy/cert-manager -fFollow logs while you re-apply the Certificate to watch the issuance in real time.
FAQ
What is the difference between an Issuer and a ClusterIssuer?
Issuer is namespaced — it can only issue certificates for Certificate resources in the same namespace. ClusterIssuer is cluster-scoped and can be referenced by any namespace. For a single shared Let's Encrypt account across the whole cluster, use ClusterIssuer. For per-team isolation (for example, different Vault mounts per team), use Issuer in each team's namespace. The spec is otherwise identical — ClusterIssuer simply lets you avoid duplicating the same ACME account config into every namespace.
Do I need cert-manager if I use Traefik or NGINX Proxy Manager?
If your reverse proxy is in Kubernetes and is serving cluster workloads, cert-manager is almost always the right answer — it is declarative, GitOps-friendly, and supports every ACME CA plus private CAs. If you are running a standalone reverse proxy outside Kubernetes (for example, Traefik as a systemd service or NGINX Proxy Manager in Docker), use the built-in Let's Encrypt client in those tools instead. Mixing cert-manager with a non-Kubernetes proxy adds complexity for no benefit.
HTTP-01 vs. DNS-01 — which should I use?
HTTP-01 is simpler to set up (no API tokens, works out of the box), requires port 80 open to the internet, and cannot issue wildcards. DNS-01 requires DNS provider API access but supports wildcards and works for internal services with no public HTTP endpoint. A good default: use HTTP-01 for public per-host certificates, and DNS-01 with Cloudflare for wildcard certificates or internal services. You can configure multiple solvers in a single ClusterIssuer using selector.dnsZones or selector.matchLabels.
Can I run cert-manager on k3s or a single-node cluster?
Yes — cert-manager is regularly run on k3s, kind, microk8s, and single-node production clusters. The default Helm install uses about 100-150 MB RAM across the three Deployments and practically no CPU at idle. On a single-node cluster you may want to reduce replicas to 1 for webhook and cainjector to save memory: --set webhook.replicaCount=1 --set cainjector.replicaCount=1. See our k3s install guide for the full stack.
How does cert-manager handle renewals for certificates issued to Ingresses?
When you annotate an Ingress with cert-manager.io/cluster-issuer, cert-manager creates a matching Certificate resource and then manages renewal on that Certificate. The renewal writes a fresh tls.crt and tls.key into the same Secret referenced by the Ingress tls.secretName field. Most Ingress controllers (NGINX, Traefik, HAProxy) watch their TLS Secrets and pick up the new certificate within seconds. If a controller caches the old certificate, a rolling restart of the controller Pods forces a reload. Renewals happen 30 days before expiry by default.
Is cert-manager production-ready? Who uses it?
cert-manager is a CNCF Graduated project — the same maturity level as Kubernetes, Prometheus, and Envoy. It is used in production by thousands of organisations including major SaaS platforms, banks, government deployments, and every major managed Kubernetes offering (GKE, EKS, AKS, DigitalOcean, Civo). For authoritative documentation and production best practices, consult the official docs at cert-manager.io/docs/.
Next Steps
Now that cert-manager is issuing certificates on your VPS, these are natural follow-ups:
- Install an Ingress controller if you skipped it — See our Traefik install guide for a Kubernetes-native option, or NGINX Ingress for the community default.
- Set up k3s properly for production — If you are running on a single VPS, follow the k3s install guide for persistence, backups, and firewall rules.
- Add mTLS between services — Use cert-manager as an internal CA to issue short-lived certificates for pod-to-pod mTLS. Combine with Istio or Linkerd for full service mesh identity.
- Automate Ingress creation with Helm charts — Package your applications as Helm charts where the Ingress annotation points at
letsencrypt-prod. Every new release gets a certificate automatically.
- Monitor certificate expiry with Prometheus and Alertmanager — Enable the ServiceMonitor, scrape cert-manager metrics, and alert on
certmanager_certificate_ready_status == 0to catch renewal failures before users do.
- Adopt GitOps with Argo CD or Flux — Commit your
ClusterIssuerandCertificatemanifests to Git. Every environment gets identical TLS configuration, reviewable via pull request.
Run cert-manager and Kubernetes on a VPS built for it>
The CloudCore Starter plan at EUR 7.99/month gives you 4 vCPU, 6 GB RAM, 100 GB NVMe, and unmetered bandwidth — enough to comfortably run k3s, cert-manager, an Ingress controller, and multiple workloads with free automatic TLS for every domain you own.>
- Full root access and sudo privileges
- Ubuntu 24.04 LTS pre-installed
- Public IPv4 with reverse DNS for Let's Encrypt HTTP-01
- Unmetered bandwidth — no surprise bills during renewal storms
- Upgrade to CloudCore Professional when your workloads outgrow Starter>
Deploy Your Kubernetes VPS Now — Plans start at EUR 7.99/month.