Deploy a Simple k3s Cluster on an Ubuntu Server (with HTTPS)

Written by

in

You don’t need a managed cloud or a three-node control plane to run Kubernetes in production. A single Ubuntu VPS and k3s — a lightweight, certified Kubernetes distribution — will happily serve several apps with automatic HTTPS. This is the exact setup we use for our own sites, boiled down to the steps that matter.

Key Takeaways

  • k3s installs a full Kubernetes cluster on one Ubuntu box with a single command, bundling the Traefik ingress controller and local-path storage.
  • cert-manager plus Let’s Encrypt gives you free, auto-renewing HTTPS — as long as port 80 stays reachable.
  • The whole bootstrap is idempotent: safe to re-run, easy to script.

What are we building?

One Ubuntu server running k3s, with three things layered on top: Traefik as the ingress controller (bundled with k3s), cert-manager for TLS certificates, and a ufw firewall exposing only SSH, HTTP, and HTTPS. Everything installs from the official upstream scripts and manifests — no third-party tooling.

You’ll need a fresh Ubuntu server you can SSH into as root, and (for the HTTPS part) a domain you control.

Step 1: Base packages and Docker

First, the essentials. rsync for copying files to the server, ca-certificates for TLS trust, and ufw for the firewall:

export DEBIAN_FRONTEND=noninteractive
apt-get update -q
apt-get install -yq rsync ca-certificates ufw

Docker is optional for running k3s. If you plan to build container images on the server itself, though, install Docker CE from the official convenience script:

if ! command -v docker >/dev/null; then
  curl -fsSL https://get.docker.com | sh
fi

Note the if guard: every step in this guide is written to be idempotent, so you can re-run the whole thing safely.

Step 2: Install k3s

This is the step that feels too easy. One command installs the k3s server, sets it up as a systemd service, and starts a fully working Kubernetes cluster:

curl -sfL https://get.k3s.io | sh -

k3s bundles two components you’d otherwise install yourself: the Traefik ingress controller (listening on ports 80/443) and the local-path storage provisioner (PersistentVolumes backed by the node’s disk, under /var/lib/rancher/k3s/storage/).

The node object can take a few seconds to appear after first start, so wait for it before continuing:

for i in $(seq 1 30); do kubectl get nodes >/dev/null 2>&1 && break; sleep 2; done
kubectl wait --for=condition=Ready node --all --timeout=120s

When kubectl get nodes shows Ready, you have a working cluster. As root, kubectl works out of the box — the installer writes the cluster credentials to /etc/rancher/k3s/k3s.yaml and wires the CLI up to them.

Step 3: Install cert-manager

cert-manager automates the entire TLS lifecycle: it requests certificates from Let’s Encrypt, solves the validation challenges, and renews certificates before they expire. Install it from the official release manifest and wait for the rollout:

CERT_MANAGER_VERSION="v1.18.2"
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/${CERT_MANAGER_VERSION}/cert-manager.yaml
kubectl -n cert-manager rollout status deploy/cert-manager --timeout=180s
kubectl -n cert-manager rollout status deploy/cert-manager-webhook --timeout=180s

Then give it a ClusterIssuer — the resource that tells cert-manager how to talk to Let’s Encrypt. HTTP-01 is the simplest challenge type: Let’s Encrypt makes an HTTP request to your domain on port 80, and cert-manager answers it through Traefik.

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: you@example.com
    privateKeySecretRef:
      name: letsencrypt-prod-account-key
    solvers:
      - http01:
          ingress:
            class: traefik

Tip from experience: create a second issuer pointing at Let’s Encrypt’s staging endpoint (https://acme-staging-v02.api.letsencrypt.org/directory) and use it for your first attempt on a new domain. Staging avoids the production rate limit of 5 failed validations per hour — a limit that’s easy to hit while you’re still debugging DNS.

Step 4: Firewall

Lock the box down to exactly three ports:

ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw --force enable

One rule to tattoo somewhere visible: port 80 stays open forever. It’s not just for redirects — Let’s Encrypt’s HTTP-01 renewal challenges arrive on port 80. Close it and your certificates will silently fail to renew a few weeks later.

Step 5: DNS and your first HTTPS app

Point an A record at your server’s IP and wait until dig +short yourdomain.com returns it. If your DNS provider is Cloudflare, keep the record set to DNS only (grey cloud, proxy off) — with the proxy on, Let’s Encrypt’s validation request never reaches your server and issuance breaks.

Now any Ingress you deploy gets HTTPS with two additions — the issuer annotation and a tls: block:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  ingressClassName: traefik
  rules:
    - host: app.yourdomain.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: my-app
                port:
                  number: 80
  tls:
    - hosts: [app.yourdomain.com]
      secretName: my-app-tls

Watch the certificate get issued — it’s usually Ready within a minute:

kubectl get certificate -w

If a certificate stays un-Ready, kubectl describe certificaterequest tells you why, and kubectl describe challenge is where HTTP-01 failures actually surface. The first thing to check: does port 80 actually reach Traefik from the internet?

Frequently Asked Questions

Is single-node k3s really production-ready?

For small workloads, yes — we run our company site, a WordPress blog, an API, and a cluster dashboard on one node. You give up high availability, so a node failure means downtime. Know that trade-off, take backups, and single-node k3s is a pragmatic, low-cost choice.

Why k3s instead of full Kubernetes (kubeadm)?

k3s is a certified Kubernetes distribution packaged as a single small binary. You get the same API with far less setup: ingress and storage come bundled, and upgrades are a re-run of the install script. For a single server there’s little reason to take on kubeadm’s extra moving parts.

How do apps survive pod restarts?

Use PersistentVolumeClaims with the bundled local-path storage class. Data lands on the node’s disk under /var/lib/rancher/k3s/storage/ and survives pod restarts and re-applies. Remember it’s node-local: back it up separately, because it lives and dies with the server.

Wrapping up

That’s the whole stack: five steps from a bare Ubuntu box to a Kubernetes cluster serving HTTPS traffic. The same pattern scales from a hobby project to real client workloads — this blog is being served by exactly this setup right now.

Want to see what we run on ours? Read about our cluster dashboard, or what MashupSoft does with infrastructure like this for clients.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *