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-compose | Kubernetes |
|---|---|
services: | Deployment |
ports: | Service |
| reverse proxy / port binding | Ingress |
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.