Paper 01: The Transition Guide from Docker to Orchestration
Table of Contents

I. The Limits of a Single Machine #
Docker works well on one machine, but an app spread across several needs a layer of software that runs the whole cluster. This post explains that layer through K3s: its pieces, the path of one request, and what to prepare before installing.
After the intro’s saga of a network provider blocking a server’s connections, I ended up with two servers for an app that needed one. Leaving one idle was a waste, so I wanted both to run as one system. The catch: Docker, the tool I had been using, was not built for that.
Most apps today run as containers. A container is a package holding an app plus everything it needs, so it runs the same on any machine. Docker, the most popular tool for building and running containers, handles a single VPS very well. A VPS is a virtual server rented monthly from a cloud provider.
But Docker manages each machine on its own. If one machine suddenly goes down, who moves the app to the other? With plain Docker, that is me, by hand, at whatever hour the failure happens.
Picture a lunch restaurant that opens a few more kitchens around town. The owner cannot rush between them every time a cook calls in sick; the restaurant needs a manager who keeps the roster and sends someone to whichever kitchen is short.
In software, that manager’s job is orchestration: deciding which app runs on which machine and in how many copies, rebuilding copies that die, and rolling out new versions without users noticing. Kubernetes is the most widely used standard for it.
Kubernetes groups machines into a cluster with two roles. The control plane is the manager: it remembers the desired state and keeps adjusting the cluster to match. Workers are the kitchens, where apps actually run.
As the intro told, I tried full Kubernetes first, and its control plane alone ate nearly all the VPS’s RAM and CPU. I rented a server to run my app; the server ended up mostly running the machinery that manages the app. K3s is a slimmed-down Kubernetes for exactly this case. In this series the control plane sits on a VPS I call the master, and at least one worker is my home computer.
So what did K3s cut to get this light, and where did Docker go?
II. Why K3s, and Where Docker Went #
K3s is Kubernetes packed into a single program file under 100MB. It drops the integrations built specifically for the big cloud providers, and swaps the default database from etcd to SQLite.
The database swap is the main reason K3s is light. Kubernetes needs somewhere to store the state of the whole cluster: what exists, where it runs, how it is configured. That is the manager’s roster book. Standard Kubernetes uses etcd, a database built for several machines to keep that book in sync: dependable, but it costs RAM and several background processes. SQLite is a single file on disk, and for a few machines the swap saves a lot while costing almost nothing.
That makes K3s a good fit for a home-built cluster, hardware out in the field, or any system that must stay compact while keeping the Kubernetes core.
Then came the detail that made me pause: I left Docker for K3s, and K3s (like current Kubernetes) does not use Docker either.
- Docker’s remaining job is building images on your own machine. An image is a mold; each container is a running copy cast from it.
- On a K3s machine, containers are run by containerd, the same engine Docker uses underneath. K3s calls containerd directly and skips Docker.
- I no longer start containers one by one with commands, but declare the desired state in a YAML file. From there, K3s simply tells containerd to pull the image and run it. YAML is a plain-text config format with one “name: value” setting per line.
This is the biggest shift in thinking: describe the destination, not the steps. In the restaurant, instead of phoning each cook (“go to kitchen 2 today”), you write “always 3 cooks on shift” on the board. The manager moves people and finds a replacement when someone is out. My job shrinks to writing that board correctly, using some oddly named entries.
III. The Pieces of K3s: What You Write, What the System Handles #
Kubernetes names every small piece, and many names sound scarier than the job: even the slip for requesting storage space is called a PersistentVolumeClaim. Each piece you declare is called a resource. I split them into two groups: the ones you write YAML for daily, and the ones the system handles.
Group 1: hands-on daily
- Pod: the smallest unit Kubernetes runs. A Pod is not the same as a container: one Pod can hold several containers sharing a network, so they reach each other over localhost like two programs on one machine. Localhost is the address that always points back to the machine itself.
- Deployment: you declare “3 copies of this app,” and the Deployment keeps 3 Pods running. It also does rolling updates: Pods are replaced one at a time, each old one retired only once its replacement runs, so users see no interruption.
- Service: Pods are short-lived and get a new IP address each time they are rebuilt. A Service provides a fixed IP and name, then forwards requests to whichever Pods are running. A request is one ask sent to the app by a browser or another app.
- Ingress: the entry point for requests from outside, working at the application layer: it reads which domain and path a request asks for, then routes it to the right Service. K3s ships Traefik, a routing program, for this.
- Gateway API: the newer routing standard. It fixes Ingress’s weak spot by separating the infrastructure admin’s settings from each app team’s settings. This series uses Envoy Gateway, a program implementing this standard, instead of Traefik.
- ConfigMap and Secret: keep settings and passwords outside the image, so changing a password needs no image rebuild. Pods read them at startup.
- PersistentVolumeClaim: the slip mentioned above. Without it, whatever a Pod writes is lost when the Pod dies.
Group 2: infrastructure, understanding is enough
- ReplicaSet: sits between Deployment and Pod, keeping the copy count right. Deployments create it, so you rarely write one.
- LoadBalancer: a type of Service that lets internet traffic into the cluster, working at the network layer: it sees only IP address and port, not content. A port is a numbered door on a machine; each service listens on its own.
- ServiceLB: cloud providers offer a load-balancing service that gives a LoadBalancer its address and spreads traffic across machines. Physical servers and plain VPSes lack one, so K3s bundles ServiceLB (formerly Klipper) as a stopgap.
- MetalLB: a proper load balancer for those environments, able to hand out real local-network IPs like the big clouds do. For better control over IPs, I will disable the default ServiceLB and use MetalLB.
In restaurant terms:
| Piece in K3s | In the restaurant |
|---|---|
| Control plane | The manager |
| Worker node | The kitchens |
| Pod | One cook |
| Deployment | The board saying “always 3 cooks” |
| Service | The counter’s fixed phone number |
| Ingress or Gateway API | The host reading the slip, pointing to a counter |
| LoadBalancer | The restaurant’s address and door |
The analogy breaks here: when a machine dies, its Pods are not reassigned to another kitchen like cooks; they are discarded, and Kubernetes creates identical new Pods on another machine.
One more thing: my kitchens are not in the same place.
IV. Joining Cloud and Home Machines Into One Private Network #
This is a hybrid cloud cluster. Hybrid cloud means mixing rented cloud machines with your own hardware; here, a VPS and my home computer. The job of connecting these distant machines safely, without opening ports carelessly, goes to Tailscale.
Tailscale is a VPN service. A VPN is an encrypted private network running over the internet. With it, machines in Saigon or Singapore talk to each other as if they shared one office network.
I picked a ready-made service for a reason. So far in this series, my networking record is one server with UDP blocked and one VPN that kept dropping. UDP is the fast way of sending data that most VPNs rely on, so when UDP is blocked, the VPN drops with it.
The hardest job Tailscale handles is getting through NAT. NAT lets every device behind your home router share one public IP address. Because of that, devices inside can call out, while nothing outside can call directly into a specific device.
Think of an apartment building with one street address. Replies reach the right flat because the guard remembers who just sent mail out; a stranger’s letter addressed only to the building is thrown away. The VPS calling my home machine is that stranger’s letter. Tailscale has both machines send toward each other at almost the same moment, so each guard takes the incoming letter for a reply and lets it through.
When NAT is too strict, even that fails, and traffic must detour through a DERP server, Tailscale’s relay. Both machines can reach DERP, so it passes data between them, like two residents forwarding letters through the same post office. By default DERP is a Tailscale machine somewhere in the world; self-hosting DERP keeps the detour on my own infrastructure, closer and faster.
The result: to K3s, the home machine is just one more machine on the VPS’s network. All the pieces are here; time to assemble them.
V. Following One Request Through the Cluster #
Let’s follow one request from a browser. In the diagram, solid arrows cover the internet and the master; dashed arrows cover the Tailscale leg to my home.
- The request passes through Cloudflare, a service in front of websites that speeds them up and filters attacks, then reaches the master’s public IP on port 443, the default port for HTTPS sites.
- MetalLB assigned that address to Envoy Gateway, so a request for it goes straight into Envoy. MetalLB only makes sure the address has someone answering; moving the packets is Kubernetes’ own job.
- Envoy reads the request’s header. The header is information attached to the request, including the domain the visitor typed. Seeing
vinhmdev.com, Envoy forwards the request to the matching Service. - The Service finds its Pods on the worker at my home. The request is split into small chunks of data called packets and encrypted. They travel through the Tailscale tunnel from the VPS to the home machine; if the network blocks too strictly, the DERP server steps in. A tunnel is an encrypted link between two machines: the data still crosses the internet, but outsiders cannot read it.
The visitor just sees a page open, with no idea the request went from a VPS all the way to my house and back. Like a customer ordering at the front counter, unaware the meal was cooked in a kitchen across town.
What if the master machine catches fire?
On the Kubernetes side, losing the master means losing the brain: no new Pods can be created. But Pods already running on workers do not die, just as kitchens keep cooking the dishes already assigned while the manager is out.
The real problem is the network. With no independent external load balancer, I point Cloudflare straight at the master’s IP, so the master is also the front door. If the master goes down, apps on the workers keep running, but internet users have no way in. The kitchens keep cooking; the only door is locked.
That is a single point of failure: one component whose failure alone takes the whole system offline. Fixing it later takes an external load balancer, or a mechanism that moves the IP to another machine when the master dies.
VI. What to Prepare Before Installing #
The next post starts the installation. If you plan to follow along, prepare:
Clean Ubuntu LTS servers and SSH keys. Ubuntu LTS is a Linux release for servers that gets updates for many years. SSH is how you log in to a server remotely to type commands; an SSH key replaces the password.
Default components switched off. When installing K3s, I will add options that disable Traefik and ServiceLB, making room for Envoy Gateway and MetalLB.
Swap turned off. Swap is disk space the operating system borrows as RAM when RAM runs out: cheap, but far slower. This command turns off all active swap:
1sudo swapoff -aWhy: standard Kubernetes refuses to run on a machine with swap on by default. K3s relaxes that check and runs anyway, but I still turn swap off. Kubernetes budgets memory assuming RAM is real RAM: when RAM runs out, a Pod is stopped cleanly and recreated. With swap, the whole machine can crawl as it keeps shuffling data to and from disk, and nothing reports an error. The command lasts only until the next reboot; to turn swap off for good, remove the swap line from the /etc/fstab file.
Some comfort with vi or nano, two text editors that run inside a command-line window. Editing YAML in vi over SSH is a survival skill where there is no graphical interface. The first skill to learn is getting out: type
:qand press Enter. The Stack Overflow question “How do I exit Vim?” once passed a million views, so if you have ever been stuck in there, you are in good company.
Hard-won note: do NOT install Docker and K3s together on the master.
The two clash at the network level: both compete for ports, and both write into the system’s routing table and iptables. These are the two rule sets Linux uses to decide which way a packet goes and whether it may pass. Each side adds rules for its own container network without knowing the other exists, so packets go astray in ways that are very hard to trace. Untangling that mess is exhausting and pointless.
K3s already has the lightweight containerd, so Docker adds nothing. Start from a genuinely clean server, uninstalling Docker if it is there.
Even if you never install anything yourself, you can take one question to your infrastructure team: “Which machine, if it dies, locks users out?” If the answer is a single machine, that is a single point of failure, like my master right now.
See you in Paper 02: setting up the VPN with Tailscale.