Kubernetes for the rest of us

Part 1 of 1

From docker-compose to Kubernetes with k3s

If you can write a docker-compose.yml, you already understand most of Kubernetes. The concepts translate almost one-to-one — they’re just spelled differently. k3s is a lightweight distribution that runs the same Kubernetes you’d find in production, without needing a beefy server or a complicated setup. It works great on a laptop and equally well on a small VPS.

By the end of this article you’ll have nginx running with a Deployment, a Service, and an Ingress. Those three resources are the foundation of pretty much every app you’ll deploy on Kubernetes.

Getting your cluster up

Depending on where you want to run things, there are two paths. Both end up in the same place.

Local: Rancher Desktop

Rancher Desktop is a desktop app that runs a k3s cluster on your machine. Install it, let it finish starting up, and then point kubectl at it:

kubectl config use-context rancher-desktop
kubectl get nodes

You should see something like this:

NAME                   STATUS   ROLES                  AGE   VERSION
lima-rancher-desktop   Ready    control-plane,master   5m    v1.29.3+k3s1

One node, status Ready — you’re good to go.

VPS: install k3s

SSH into your server and run the install script:

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

k3s will install itself, register as a systemd service, and start. Give it a moment, then verify:

sudo kubectl get nodes
NAME       STATUS   ROLES                  AGE   VERSION
your-vps   Ready    control-plane,master   1m    v1.29.3+k3s1

To control the cluster from your local machine, grab the kubeconfig from the server:

# Run this on the server
sudo cat /etc/rancher/k3s/k3s.yaml

Copy the output into ~/.kube/config on your local machine. Just make sure to replace 127.0.0.1 in that file with your server’s public IP address. If you have a firewall, also open port 6443 — that’s the Kubernetes API port.

The starting point

Here’s the docker-compose setup we’re going to translate into Kubernetes:

# docker-compose.yml
services:
  web:
    image: nginx:alpine
    ports:
      - "80:80"

In Kubernetes, this becomes three separate files — a Deployment, a Service, and an Ingress. Each one does a single job, which makes them easier to reason about once you get used to it.

Deployment — replaces services:

A Deployment is where you define what container to run and how many replicas to keep alive. If a pod crashes, Kubernetes will automatically start a new one.

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:alpine
          ports:
            - containerPort: 80

The labels field is important — it’s how the Service and Ingress will find these pods later. Think of it as a name tag that other resources can query.

Service — replaces ports:

A Service gives your pods a stable internal address. Without one, there’s no way to reach them from elsewhere in the cluster (or from an Ingress).

# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  selector:
    app: nginx
  ports:
    - port: 80
      targetPort: 80

The selector here matches the label from the Deployment. Any traffic that hits port 80 on the Service gets forwarded to port 80 on the matching pods.

Ingress — your reverse proxy

An Ingress handles incoming HTTP traffic from outside the cluster and routes it to the right Service. k3s comes with Traefik pre-installed as the Ingress controller, so there’s nothing extra to set up.

# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: nginx
spec:
  rules:
    - host: nginx.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: nginx
                port:
                  number: 80

Replace nginx.example.com with your actual domain. On a VPS, point the domain’s DNS to your server’s IP. If you’re running locally, you can fake it by adding a line to /etc/hosts:

127.0.0.1 nginx.example.com

Applying everything

Put the three files in a folder and apply them all at once:

kubectl apply -f ./
deployment.apps/nginx created
service/nginx created
ingress.networking.k8s.io/nginx created

Now check that everything came up as expected:

kubectl get pods
NAME                     READY   STATUS    RESTARTS   AGE
nginx-6d4cf56db6-xk2p9   1/1     Running   0          30s
kubectl get svc
NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
kubernetes   ClusterIP   10.43.0.1       <none>        443/TCP   10m
nginx        ClusterIP   10.43.128.42    <none>        80/TCP    30s
kubectl get ingress
NAME    CLASS     HOSTS               ADDRESS        PORTS   AGE
nginx   traefik   nginx.example.com   192.168.1.10   80      30s

Once the pod shows Running and the Ingress has an address, open http://nginx.example.com in your browser. You should see the nginx welcome page.

If something isn’t working, these two commands will usually point you in the right direction:

kubectl describe pod <pod-name>
kubectl logs <pod-name>

What maps to what

docker-composeKubernetes
services:Deployment
ports:Service
reverse proxy / port bindingIngress

Once this pattern clicks, deploying any app follows the same structure. Swap out the image, adjust the ports, update the hostname — and you’re there.

Where to go from here

This setup is already closer to production than most docker-compose workflows. Your app is self-healing, routable by hostname, and running on infrastructure you can scale. From here, the natural next steps are adding TLS with cert-manager, managing configuration with ConfigMaps and Secrets, and setting up a CI pipeline that deploys on push. But those are stories for another time — for now, you have a working cluster and a running app, and that’s a solid place to be.