Skip to content

Deploy nginx container with Let's Encrypt certificate on K3s Kubernetes distribution

Introduction

This guide demonstrates how to create nginx container in K3s Kubernetes distribution with valid Let's Encrypt certificate for HTTPS. Let's Encrypt certificate will be obtained using DNS challenge method with a domain registered on Cloudflare.

OS used: Debian 12
Software used: K3s 1.30.5, Helm 3.16.2, cert-manager 1.16.1

Source

Running simple container

In order to run nginx container few Kubernetes files need to be created. Order in which Kubernetes elements are created is important (for example Persistent Volume Claim must be created before Deployment). Following files will be created with Kubernetes objects listed in each file:

  • 01_nginx-test-namespace.yml
    • Namespace
  • cloudflare-api-token-secret.yml
    • Secret with Cloudflare API Token
  • 02_lets-encrypt-cert.yml
    • Issuer for certificate
  • 03_nginx-test-volume.yml
    • Persistent Volume
    • Persistent Volume Claim
  • 04_nginx-test-deployment.yml
    • Deployment
    • Service
    • Ingress

Note

cloudflare-api-token-secret.yml doesn't have a number because in production it's better to store this file in other location designated for secrets and remember to apply it before 02_lets-encrypt-cert.yml.

Creating namespace

First create folder in which you will put your Kubernetes files:

$ mkdir -m 700 ~/test-stack

Next prepare file with new namespace which will be used for grouping Kubernetes elements:

$ vim ~/test-stack/01_nginx-test-namespace.yml
~/test-stack/01_nginx-test-namespace.yml
apiVersion: v1
kind: Namespace
metadata:
  name: test-space

Installing Helm and cert-manager

Install cert-mananger using Helm package manager for Kubernetes with following commands (commands taken from Official Helm documentation and Official cert-manager documentation):

  • Export variable KUBECONFIG for Helm to work correctly with K3s under your user by putting it in .bashrc:
$ echo -e "\n# K3s environment variables" >> ~/.bashrc && \
  echo export KUBECONFIG=/etc/rancher/k3s/k3s.yaml >> ~/.bashrc

echo -e - enable interpretation of backslash escapes

  • Login again or run following command to enable kubectl in current session:
$ source ~/.bashrc
  • Install Helm:
$ curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 && \
  chmod 700 get_helm.sh && \
  ./get_helm.sh && \
  rm ./get_helm.sh
  • Install cert-manager:
$ helm repo add jetstack https://charts.jetstack.io --force-update
$ helm install \
  cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --version v1.16.1 \
  --set crds.enabled=true

Enable autocompletion for helm

To enable helm bash autocompletion for user (when using Tab key) run command to add entry to your .bashrc:

$ echo -e "\n# helm completion bash" >> ~/.bashrc && \
  echo 'source <(helm completion bash)' >>~/.bashrc

echo -e - enable interpretation of backslash escapes

Login again or run following command to enable bash autocompletion in current session:

$ source ~/.bashrc

Creating Issuer for Let's Encrypt certificate

  • Create Kubernetes secret with Cloudflare API Token:

$ vim ~/test-stack/cloudflare-api-token-secret.yml
~/test-stack/cloudflare-api-token-secret.yml
apiVersion: v1
kind: Secret
metadata:
  name: cloudflare-api-token-secret-mydomain.com
  namespace: cert-manager
type: Opaque
stringData:
  api-token: <API_Token>

...-mydomain.com - your domain name, for easy identification of the domain this secret concerns. Instruction how to set domain using Cloudflare is here Obtaining Let's Encrypt certificate using Cloudflare - Cloudflare DNS.
<API_Token> - enter you Cloudflare API Token. Instruction how to generate API Token is here Obtaining Let's Encrypt certificate using Cloudflare - Cloudflare API Token.

  • Secure file with secret with stricter access permissions:
$ chmod 600 ~/test-stack/cloudflare-api-token-secret.yml
  • Create config for Let's Encrypt certificate Issuer:

$ vim ~/test-stack/02_lets-encrypt-cert.yml
~/test-stack/02_lets-encrypt-cert.yml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: lets-encrypt-mydomain.com
  namespace: cert-manager
spec:
  acme:
    email: [email protected] # Email address used for ACME registration
    server: https://acme-staging-v02.api.letsencrypt.org/directory # Let's Encrypt staging - use for testing
    #server: https://acme-v02.api.letsencrypt.org/directory # Let's Encrypt production - use to get final certificate
    privateKeySecretRef:
      # Secret resource that will be used to store the account's private key.
      # The account private key identifies you as a user of the ACME service
      # and is used to sign all requests/communication with the ACME server
      # to validate your identity.
      name: lets-encrypt-account-key-mydomain.com
    solvers:
      - dns01:
          cloudflare:
            apiTokenSecretRef:
              # Remember to first create and apply a secret containing following fields
              name: cloudflare-api-token-secret-mydomain.com
              key: api-token

Tip

On you first run use line with comment Let's Encrypt staging. This issues a non valid certificate that is good for testing because it's not rate limited, and if you make a mistake you won't need to wait for some time before next try.

After you deployed nginx and all is working then:

  • uncomment line Let's Encrypt production
  • comment line Let's Encrypt staging
  • apply file 02_lets-encrypt-cert.yml

Note

cert-manager issuer can be type Issuer or ClusterIssuer. Here is description from official documentation:

An Issuer is a namespaced resource, and it is not possible to issue certificates from an Issuer in a different namespace. This means you will need to create an Issuer in each namespace you wish to obtain Certificates in.

If you want to create a single Issuer that can be consumed in multiple namespaces, you should consider creating a ClusterIssuer resource. This is almost identical to the Issuer resource, however is non-namespaced so it can be used to issue Certificates across all namespaces.

Creating Kubernetes Volume

Create folder for data available to container:

  • html - folder with web page
$ sudo mkdir -p /mnt/nginx-test/html

Create volume configuration file:

$ vim ~/test-stack/03_nginx-test-volume.yml
~/test-stack/03_nginx-test-volume.yml
apiVersion: storage.k8s.io/v1
kind: storageClass
metadata:
  name: nginx-test-storage
  namespace: test-space
provisioner: kubernetes.io/no-provisioner  # storageClass setting for local storage
volumeBindingMode: WaitForFirstConsumer  # storageClass setting for local storage
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nginx-test-html-pv
  namespace: test-space
spec:
  capacity:
    storage: 1Gi # for local storage it's just a label and don't cap space available for container
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Delete
  storageClassName: nginx-test-storage
  local:
    path: /mnt/nginx-test/html # path on your server
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: kubernetes.io/hostname
              operator: In
              values:
                - myhostname # host name of your server
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nginx-test-html-pvc
  namespace: test-space
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: nginx-test-storage
  resources:
    requests:
      storage: 1Gi # for local storage it's just a label and don't cap space available for container

Creating Kubernetes Deployment

$ vim ~/test-stack/04_nginx-test-deployment.yml
~/test-stack/04_nginx-test-deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-test-deployment
  namespace: test-space
  labels:
    app: nginx-test-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx-test-deployment
  template:
    metadata:
      labels:
        app: nginx-test-deployment
    spec:
      containers:
        - name: nginx
          image: nginx:1.27.2 # image you want to use
          ports:
            - containerPort: 80
          volumeMounts: # VolumeMounts are the paths that your application uses inside container
            - name: nginx-test-html-volume
              mountPath: /usr/share/nginx/html # the path that your application uses inside container
      volumes:
        - name: nginx-test-html-volume
          persistentVolumeClaim:
            claimName: nginx-test-html-pvc # should match name of the PersistentVolumeClaim
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-test-service
  namespace: test-space
spec:
  selector:
    app: nginx-test-deployment
  ports:
    - name: http
      port: 80
      targetPort: 80 # should match containerPort in Deployment
      protocol: TCP # protocol that will be used
---
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
  name: redirect
  namespace: test-space
spec:
  # Redirect traffic from port 80 to 443 - use only HTTPS
  redirectScheme:
    scheme: https
    permanent: true
    port: "443" # must be a string, not a numeric value
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: nginx-test-ingress
  namespace: test-space
  annotations:
    cert-manager.io/cluster-issuer: "lets-encrypt-mydomain.com" # should match the name of Issuer or ClusterIssuer
    # Traefik Middleware reference - syntax <namespace>-<name>@kubernetescrd
    traefik.ingress.kubernetes.io/router.middlewares: test-space-redirect@kubernetescrd
spec:
  tls:
  - hosts:
    - myservername.mydomain.com # replace with your domain
    secretName: nginx-test-tls # this is the name of certificate that will be created
  rules:
    - host: myservername.mydomain.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: nginx-test-service # reference to created Service
                port:
                  number: 80 # Service port number

Info

  • If using an Issuer update the Ingress annotation to cert-manager.io/issuer.
  • If using a ClusterIssuer update the Ingress annotation to cert-manager.io/cluster-issuer.

Running Kubernetes Deployment

It's important to create Kubernetes elements in a specific order, that is why files are numbered and should be run in sequence:

$ kubectl apply -f ~/test-stack/01_nginx-test-namespace.yml && \
  kubectl apply -f ~/test-stack/cloudflare-api-token-secret.yml && \
  kubectl apply -f ~/test-stack/02_lets-encrypt-cert.yml && \
  kubectl apply -f ~/test-stack/03_nginx-test-volume.yml && \
  kubectl apply -f ~/test-stack/04_nginx-test-deployment.yml

Checking if certificate was issued

Use these commands to check if certificate was issued:

$ kubectl -n cert-manager get issuers.cert-manager.io
$ kubectl -n cert-manager get clusterissuer.cert-manager.io
$ kubectl -n test-space describe issuers
$ kubectl -n test-space describe clusterissuer
$ kubectl -n test-space describe certificaterequest
$ kubectl -n test-space describe order
$ kubectl -n test-space get challenges
$ kubectl -n test-space describe challenges
$ kubectl -n test-space get secrets

Serving your own webpage on nginx

Create your simple webpage:

$ sudo vim /mnt/nginx-test/html/index.html
/mnt/nginx-test/html/index.html
<!DOCTYPE html>
<html lang="en">

<head>
  <title>Page Title</title>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>

<body>

  <h1>My Simple Website</h1>
  <p>Website on K3s.</p>

</body>

</html>

Enter the address of your service into web browser: https://myservername.mydomain.com. Check certificate. It should be Let's Encrypt certificate. If it is working change certificate from Let's Encrypt staging to production. Apply file after changes:

$ vim ~/test-stack/02_lets-encrypt-cert.yml
~/test-stack/02_lets-encrypt-cert.yml
apiVersion: cert-manager.io/v1
(...)
spec:
  acme:
    (...)
    #server: https://acme-staging-v02.api.letsencrypt.org/directory # Let's Encrypt staging - use for testing
    server: https://acme-v02.api.letsencrypt.org/directory # Let's Encrypt production - use to get final certificate
    (...)
$ kubectl apply -f ~/test-stack/02_lets-encrypt-cert.yml

Appendix

Upgrade helm

To upgrade helm to new version just perform install operation. Helm is a single binary file and this will replace it to new version:

$ curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 && \
  chmod 700 get_helm.sh && \
  ./get_helm.sh && \
  rm ./get_helm.sh

Upgrade cert-manager

Use following command to upgrade cert-manager:

$ helm repo update && \
  helm upgrade --reset-then-reuse-values --namespace cert-manager --version <version> cert-manager jetstack/cert-manager

<version> - enter new version of cert-manager