Matt DermanFebruary 20, 2025Updated September 5, 2025

How to Deploy Your App to Kubernetes with Free and Automatic SSL

Tech
How to Deploy Your App to Kubernetes with Free and Automatic SSL

By the end of this article you’ll have the tools at your disposal to deploy however many apps and services your heart desires, completely for free with automatic SSL to your shiny k8s cluster.

You still won’t be able to talk to your loved ones at the dinner table about what you do, but at least you’ll be able to rest at night knowing you’ll never need to buy and renew SSL certificates ever again.

Got a new app you want to deploy? Just ensure the DNS record hosting is on one of these supported providers: https://cert-manager.io/docs/configuration/acme/dns01/#supported-dns01-providers (we will be using Cloudflare in this tutorial, with the cluster hosted by Digital Ocean), and you have an X in your cluster, then all you need to do is mark your ingress resource for your new deployment with some annotations and you’re done!

Let’s begin by defining terms. If you’re experienced with Kubernetes skip ahead, but I’m a first principles kinda guy, and it’s never harmful to ensure that we’re all talking about the same thing.

Terms

Resource

Think of a K8s (Kubernetes) resource as a class - it’s a type of entity. For example:

  • Pod
  • Deployment
  • Secret

Object

An object is an instance of a resource define by the Kubernetes API (above). For example, later on we will be creating a “cert-manager” deployment in the “cert-manager” namespace (a namespace is how objects are isolated / grouped in a cluster).

Deployment

A deployment is a Kubernetes object that manages a set of replica Pods based on a template. It ensures a defined number of Pods are running and can handle rolling updates.

Pod

A pod is a single running container

Service

A service is a resource that uses selectors to route traffic to the right pods even if the pods restart / their IPs change. This is known as stable networking.

Ingress

An Ingress is a Kubernetes API object that defines rules for exposing HTTP/S services to external users. It allows defining URL-based routing, host-based routing, and SSL termination using certificates. Ingress resources rely on an Ingress Controller to function.

Secret

A Secret is a Kubernetes object used to store sensitive data, such as API keys.

Certificate

A Certificate is a Kubernetes resource (managed by Cert Manager) that requests and renews TLS certificates from a Cluster Issuer or Issuer. The generated TLS certificate is stored in a Secret, which can be referenced by Ingress or other services. In other words, the K8s Ingress resource does not “understand” what a certificate is, only secrets (which will contain TLS certificates), which in turn will be managed by Cert Manager.

Ingress Controller

An Ingress Controller is a Kubernetes component (i.e. a deployment with usually 1 running pod) responsible for directing HTTP/S traffic to the appropriate services (as identified by their ingress resource - see below) within the cluster.

It acts as a reverse proxy, managing routing, SSL termination, and load balancing for incoming requests.

It’s important to note that this is not the same as your cluster’s external load balancer. In order for your cluster to be accessible from the web, you still need to have a load balancer outside the cluster. For instance, with Digital Ocean (DO), if you create a service of type “LoadBalancer” DO will provision a cloud managed load balancer that will receive a public IP and forward requests to your cluster’s worker nodes.

Your ingress controller can also be behind a “NodePort” service, but we will not be covering that today.

The ACME Protocol & Cert Manager

The Automated Certificate Management Environment protocol (ACME) defines a standard for managing (issuing and renewing) TLS certificates.

  • The ACME client (we will be using and deploying Cert Manager) requests a certificate from an ACME server (Let’s Encrypt). The Cluster Issuer (below) determines what CA (ACME server) will be used.
  • The ACME server challenges the client to prove control over the domain (e.g., via DNS-01 or HTTP-01 challenge). This challenge type and other required info (e.g. API key) is provided by the Cluster Issuer.
  • If successful, the ACME server issues a TLS certificate.
  • The ACME client installs the certificate and handles automatic renewal before expiration.

Cluster Issuer

A Cluster Issuer is a Kubernetes Cert Manager resource that defines a certificate authority (CA) at the cluster level. It allows issuing TLS certificates to workloads across multiple namespaces. A Cluster Issuer can have multiple “solvers” each with selectors. A selector will define for what domains will this solver be used to issue certificate requests. A solver can be one of two challenge types:

  • HTTP-01
  • DNS-01

Let’s get started

This assumes you are already connected to your cluster (i.e. have kubectl installed on your system and are in the context of your shiny k8s cluster defined in your Kubeconfig yaml).

Please note that you may want to install more recent versions of all the below deployments that we will be installing, and thus you will need to ensure compatibility between them.

1. Get your DNS Provider’s API key

I will be using Cloudflare in this tutorial to host the DNS records (i.e. the DNS provider). Your Domain Registrar (where you bought your domain) may not have an API which we can use for DNS-01 resolution. Have a look here: https://cert-manager.io/docs/configuration/acme/dns01/#supported-dns01-providers. It may take a few hours for your domain records to propagate from your existing provider to Cloudflare, so get this process under way before starting the next steps. This process involves setting (changing) your nameservers in the dashboard of your registrar, and then adding a site (within Cloudflare) and choosing to import DNS records from another provider.

2. Nginx Ingress Controller + Load Balancer

Ensure you have an external load balancer and ingress controller. If you are using Digital Ocean you can run:

kubectl apply -f 

This will install ingress-nginx-controller in the ingress-nginx namespace.

You can verify that ingress controller is behind a LoadBalancer service with a public IP (under External IP) by running this command.

kubectl get svc -n ingress-nginx

Now you should be able to see a 404 Not Found from Nginx when accessing your public IP.

3. Install Cert Manager

Now you will need to install Cert Manager. This is incredibly complicated.

kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.12.0/cert-manager.yaml

Just kidding. Now you’re done.

4. Install API key

Our Cluster Issuer will need a secret for the api key. Install it using the below in your terminal.

cat <<EOF | kubectl apply -f - 
apiVersion: v1
kind: Secret 
metadata:
  name: cloudflare-api-token-secret
  namespace: cert-manager
type: Opaque 
stringData:
  api-token: TOKENGOESHERE
EOF

5. Install Cluster Issuer

Use the below command to install the Cluster Issuer referencing the above certificate. Note that you can leave out the “selector” yaml segment if you want this dns01 solver to be used for all domains in your cluster (in which case, if you ensure all your records are hosted by Cloudflare this will be the easiest solution).

cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
  namespace: cert-manager
spec:
  acme:
    email: [email protected]
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-issuer-account-key
    solvers:
    - dns01:
        cloudflare:
          email: [email protected]
          apiTokenSecretRef:
            name: cloudflare-api-token-secret
            key: api-token
      selector:
        dnsZones:
        - 'example.com'
EOF

6. Create your deployment

This is very specific to your application. Here is an example of a deployment we have (which will variable replacement performed on the yaml by Octopus Deploy).

apiVersion: apps/v1
kind: Deployment
metadata:
  name: example-app
  annotations:
    application: example-app
    version: ''
  namespace: '#{Library.Environment.Alias}'
spec:
  selector:
    matchLabels:
      octopusexport: OctopusExport
  replicas: 1
  strategy: {}
  template:
    metadata:
      labels:
        octopusexport: OctopusExport
      annotations:
        application: example-app
        version: ''
    spec:
      volumes:
        - name: example-app-config
          configMap:
            name: configmapname
            items:
              - key: app-env
                path: .env
        - name: ca-cert-volume
          emptyDir: {}
      containers:
        - name: example-app
          image: registry.digitalocean.com/your-container-registry/example-app
          ports:
            - name: http3000
              containerPort: 3000
              protocol: TCP
          volumeMounts:
            - name: example-app-config
              mountPath: /app/config
              subPath: ''
            - name: ca-cert-volume
              mountPath: /certs
              subPath: ''
      initContainers:
        - name: download-ca-cert
          image: index.docker.io/curlimages/curl
          command:
            - /bin/sh
            - '-c'
            - >
              echo "Downloading DigitalOcean CA certificate..."

              curl -sSL -o /certs/ca.crt
              https://www.digicert.com/CACerts/DigiCertGlobalRootCA.crt.pem
          volumeMounts:
            - name: ca-cert-volume
              mountPath: /certs
              subPath: ''

7. Create Service

apiVersion: v1
kind: Service
metadata:
  name: example-app
  namespace: '#{Library.Environment.Alias}'
spec:
  type: ClusterIP
  ports:
    - name: http-example-app
      port: 80
      targetPort: 3000
      protocol: TCP
  selector:
    octopusexport: OctopusExport

8. Deploy Ingress

The important annotation here is:

“cert-manager.io/cluster-issuer: letsencrypt-prod”

This notifies Cert Manager that “Hey, for hosts specified in this ingress, we need to manage the certificates and dump them in e.g. example-app-tls”.

Check here for the latest supported annotations: https://cert-manager.io/docs/usage/ingress/#supported-annotations

You should not need to ever create a certificate resource if you have configured this correctly.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: example-app-ingress
  annotations:
    meta.helm.sh/release-name: example-app
    kubernetes.io/ingress.class: nginx
    cert-manager.io/cluster-issuer: letsencrypt-prod
    ingress.kubernetes.io/ssl-redirect: 'true'
    ingress.kubernetes.io/proxy-body-size: 7m
    nginx.ingress.kubernetes.io/proxy-body-size: 7m
    nginx.ingress.kubernetes.io/ssl-redirect: 'true'
    nginx.ingress.kubernetes.io/proxy-buffering: 'off'
    nginx.ingress.kubernetes.io/proxy-body-size: 7m
    nginx.org/client-max-body-size: '9'
    nginx.ingress.kubernetes.io/ssl-redirect: 'true'
  namespace: '#{Library.Environment.Alias}'
spec:
  ingressClassName: nginx
  rules:
    - host: 'example.com'
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: example-app
                port:
                  number: 3000
  tls:
    - hosts:
        - 'example.com'
      secretName: example-app-tls

9. Verify Installation

Replace #{Library.Environment.Alias} with the namespace of your deployment.

kubectl get certificate -n #{Library.Environment.Alias}

You should see something like:

NAME               READY   SECRET             AGE
example-app-tls    True    example-app-tls     5m

That’s it! You’re done!

For subsequent deployments (assuming you have the DNS records with Cloudflare), all you need to do is mark your ingress with “cert-manager.io/cluster-issuer: letsencrypt-prod” and Cert Manager will dump the TLS cert in the secret you specify.

More writing

    How to Deploy Your App to Kubernetes with Free and Automatic SSL