• Virtual Machines (VMs): Virtualize hardware. When you boot an Ubuntu VM, it bundles a full kernel AND the userland. The VM's kernel talks to a hypervisor, which mimics actual physical hardware.
• Containers: Virtualize the OS(k8).
A container does not have a kernel. An Ubuntu container is only the Ubuntu userland—a collection of file directories, configuration files, system libraries (glibc), and tools like apt, bash, and ls.
When an app inside an Ubuntu container needs to write a file or send a network packet, it passes that instruction to the Ubuntu system library (glibc), which then makes a system call directly to your host machine's Linux kernel
************************************************************************************
VM
Physical server: Has physical resources such as 16 CPU cores, 64 GB RAM, and 1 TB storage.
Hypervisor (Proxmox, VMware, KVM): Allocates virtual resources to VMs. For example, 4 vCPUs, 8 GB RAM, and 100 GB virtual disk.
VM (Ubuntu): Boots its own Linux kernel, which manages the VM's virtual CPU, memory, storage, processes, and networking.
Applications: Applications inside the Ubuntu VM make system calls to the Ubuntu VM's own kernel, not directly to the physical host's kernel.
Execution: The VM's instructions generally execute directly on physical CPU cores through hardware virtualization, while the hypervisor controls CPU scheduling and access to physical resources.
************************************************************************************
Container
A container does not have a kernel. An Ubuntu container is only the Ubuntu userland—a collection of file directories, configuration files, system libraries (glibc), and tools like apt, bash, and ls.
When an app inside an Ubuntu container needs to write a file or send a network packet, it passes that instruction to the Ubuntu system library (glibc), which then makes a system call directly to your host machine's Linux kernel
*********************************************
KVM: Provides VM virtualization.
Docker: Builds, runs, and manages containers on a machine.
Kubernetes: Orchestrates containers across machines, including deployment, scaling, networking, and recovery. Kubernetes typically communicates with a container runtime such as containerd rather than requiring Docker.
**************************************************
Kernel vs. Operating System
The kernel is the core component of an operating system, responsible for interacting with hardware and managing system resources.
An operating system includes the kernel plus other software, such as system libraries, utilities, and services.
For example, Ubuntu Linux consists of:
Ubuntu Operating System
Applications
nginx, MySQL, Chrome
Userland
bash, apt, glibc, system utilities
Linux Kernel
CPU scheduling, memory, networking, drivers
Physical Hardware
Ubuntu OS = Linux kernel + Ubuntu userland + system services and utilities.
***************************************************************
For standard Linux containers, yes. They require a Linux kernel to execute their system calls.
However, you can use Docker on Windows and macOS as well.
Windows Subsystem for Linux (WSL) can be either, depending on the version. WSL2 is essentially a lightweight virtual machine (VM), not a container.
A virtual machine (VM) requires a hypervisor or an equivalent virtualization layer to run. However, not every VM needs a separate, standalone hypervisor application.
The hypervisor is responsible for providing and managing virtual hardware, including CPU, memory, storage, and network devices.
*********************
Type 1 — Bare Metal
Virtual Machines
Ubuntu / Windows
Hypervisor
VMware ESXi / Hyper-V
Physical Hardware
Type 2 — Hosted
Virtual Machines
Ubuntu / Windows
VirtualBox / VMware Workstation
Host OS (Windows / Linux)
Physical Hardware
You install VMware ESXi directly onto the server, then create:
- VM1: Ubuntu, 4 vCPUs, 8 GB RAM
- VM2: Ubuntu, 4 vCPUs, 16 GB RAM
- VM3: Windows, 4 vCPUs, 16 GB RAM
ESXi schedules the VMs on the physical hardware.
Scenario B: Hosted virtualization
You install Windows 11 on the physical server, then install VirtualBox and create those same three VMs.
Windows 11 continues running its own desktop applications, services, antivirus, etc. VirtualBox uses the resources available through the host OS to operate the VMs.
Both arrangements can use hardware-assisted virtualization, so hosted does not mean every VM instruction must be interpreted by Windows.
---------------------------------------------------------------------------------------------------------------------------
k8 vs docker in terms container management
The easiest way to understand the difference:
- Docker: Creates, runs, and manages containers on an individual machine.
- Kubernetes (K8s): Orchestrates containers across one or many machines, handling scheduling, scaling, recovery, networking, and deployments.
Think of Docker as a container runtime and management platform, while Kubernetes is a container orchestration platform.
Feature Docker Kubernetes (K8s)
Run containers Yes Yes, through a container runtime
Build container images Yes No
Manage containers on one VM Yes Yes
Manage multiple VMs as a cluster Not with standalone Docker Yes
Automatically restart crashed containers Yes, with restart policies Yes, via controllers
Automatically move workloads when a VM fails No Yes, if capacity is available
Load balancing Basic networking and port publishing Services and traffic distribution
Auto-scaling Not built into standalone Docker Yes, with autoscaling components
Rolling deployments Limited with standalone Docker Built in
Service discovery Docker networks Kubernetes Services and DNS
---------------------------------------------------------------------------------------------------------------------------
Docker manages containers services in each vm independently
# VM 1
docker run -d --name api my-api:1.0
# VM 2
docker run -d --name api my-api:1.0
# VM 3
docker run -d --name api my-api:1.0
If VM 2 crashes, Docker on VM 2 cannot restart its application because the whole VM is unavailable.
You would need to manually recover it or use external automation.
---------------------------------------------------------------------------------------------------------------------------
Kubernetes Control Plane
Scheduling • Desired State • Recovery • Scaling
Worker VM 1
API Pod
containerd
Linux Kernel
Worker VM 2
API Pod
containerd
Linux Kernel
Worker VM 3
API Pod
containerd
Linux Kernel
---------------------------------------------------------------------------------------------------------------------------
No. Modern Kubernetes does not require Docker Engine.
It needs a compatible container runtime, typically containerd
Kubernetes Control Plane
|
kubelet
|
CRI Interface
|
containerd
|
runc
|
Linux Kernel
ubernetes introduces Pods
This is another important distinction.
Pod: Groups one or more containers together.
containerd: Container runtime that manages container execution on the worker node.
kubelet: Kubernetes agent running on the worker node, responsible for ensuring its assigned Pods and containers are running.
Linux kernel: Executes container processes and provides isolation.
Pod: my-api
|
|-- Container A: Go API
| listens on port 8080
|
|-- Container B: Log collector
| collects application logs
|
|-- Shared network
|-- Shared volumes (if configured)
Kubernetes Cluster
│
├── Control Plane (VM 1)
│ ├── API Server
│ ├── Scheduler
│ ├── Controller Manager
│ └── etcd
│
└── Worker Node (Ubuntu VM 2)
│
├── kubelet
│ └── Communicates with Control Plane
│
├── containerd
│ └── Creates and manages containers
│
├── Pod: my-api
│ ├── Container A: Go API (port 8080)
│ ├── Container B: Log Collector
│ ├── Shared Network (same Pod IP)
│ └── Shared Volumes (if configured)
│
└── Ubuntu Linux Kernel
└── Executes container processes
Control Plane
│
▼
kubelet
│
▼
containerd
│
▼
Pod
│
├── Go API Container
└── Log Collector Container
│
▼
Ubuntu Linux Kernel
│
▼
VM Virtual Hardware
│
▼
Hypervisor
│
▼
Physical Hardware
------------------------------
Docker Compose and Kubernetes both use YAML files, but their YAML structure, purpose, and configuration are different.
- Docker Compose: Uses compose.yaml or docker-compose.yml to define and run containers, typically on one machine.
- Kubernetes: Uses YAML manifests to define Pods, Deployments, Services, and other resources across a Kubernetes cluster.
Let's use your Go API + Log Collector example.
1. Docker Compose YAML
services:
api:
image: my-go-api:1.0
ports:
- "8080:8080"
volumes:
- logs:/var/log/app
restart: always
log-collector:
image: my-log-collector:1.0
volumes:
- logs:/var/log/app
restart: always
volumes:
logs:
Run it with:
docker compose up -d
Docker Compose creates two containers on the same Docker host. They share the logs volume but normally have separate IP addresses.
2. Kubernetes YAML
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-api
spec:
replicas: 2
selector:
matchLabels:
app: my-api
template:
metadata:
labels:
app: my-api
spec:
containers:
- name: api
image: my-go-api:1.0
ports:
- containerPort: 8080
volumeMounts:
- name: logs
mountPath: /var/log/app
- name: log-collector
image: my-log-collector:1.0
volumeMounts:
- name: logs
mountPath: /var/log/app
volumes:
- name: logs
emptyDir: {}
Deploy it with:
kubectl apply -f deployment.yaml
Kubernetes creates 2 Pods, and each Pod contains:
Kubernetes Deployment (replicas: 2)
│
├── Pod 1
│ ├── Go API container
│ ├── Log collector container
│ └── Shared logs volume
│
└── Pod 2
├── Go API container
├── Log collector container
└── Shared logs volume
! You install Kubernetes control-plane components on VM1 and Kubernetes worker-node components on VM2. Then you can deploy applications from VM1, and Kubernetes can run them on VM2.
Architecture
Physical Server / Hypervisor
│
├── VM1: Kubernetes Control Plane (Ubuntu)
│ ├── API Server
│ ├── Scheduler
│ ├── Controller Manager
│ ├── etcd
│ └── kubectl (management CLI)
│
└── VM2: Kubernetes Worker (Ubuntu)
├── kubelet
├── containerd
│
└── Pod: my-api
├── Go API Container
└── Log Collector Container
---------------------------------------------------------------------------------------
KVM deployment how it works
How deployment works
Step 1 — Install Kubernetes
On VM1, initialize the Kubernetes control plane:
sudo kubeadm init
On VM2, install the necessary worker components and join the cluster using the command generated during initialization:
sudo kubeadm join <VM1-IP>:6443 --token ... \
--discovery-token-ca-cert-hash sha256:...
These are simplified commands. Both machines need prerequisites such as a configured container runtime, kubelet, networking, and compatible Kubernetes versions.
Step 2 — Deploy from VM1
On VM1:
kubectl apply -f deployment.yaml
Step 3 — Kubernetes schedules the application
VM1: Control Plane
│
├── Receives deployment.yaml
├── Scheduler selects VM2
│
▼
VM2: Worker Node
│
├── kubelet receives Pod assignment
├── containerd pulls container images
└── Starts Go API + Log Collector
Step 4 — Verify the deployment
kubectl get nodes
kubectl get pods -o wide
The second command shows which worker node is running each Pod.
One important clarification
Kubernetes doesn't automatically install itself on VM2 from VM1.
You must first install and configure the Kubernetes worker components on VM2 and join it to the cluster.
After that, the control plane can schedule application Pods onto VM2 automatically.
Also, the control plane doesn't physically push your application binaries to VM2. The worker's container runtime normally pulls the required container images from a registry.
So the distinction is:
- VM1: Decides where and what should run.
- VM2: Actually runs the application containers.
----------------------------------------------------------------
ubernetes doesn't interactively ask you to add worker VMs. Instead, when you initialize the control plane using kubeadm, it generates a kubeadm join command that you run on each worker VM.
Step 1 — VM1: Initialize control plane
sudo kubeadm init
After successful initialization, Kubernetes prints something similar to:
kubeadm join 192.168.1.10:6443 \
--token abcdef.1234567890abcdef \
--discovery-token-ca-cert-hash sha256:xxxx
Save this command.
Step 2 — VM2: Join as worker
First, VM2 must have the required Kubernetes components (kubeadm, kubelet, and a container runtime such as containerd) installed and configured.
Then execute the generated command on VM2:
sudo kubeadm join 192.168.1.10:6443 \
--token abcdef.1234567890abcdef \
--discovery-token-ca-cert-hash sha256:xxxx
VM2 registers itself with the control plane.
Step 3 — VM1: Verify workers
kubectl get nodes
Example output:
NAME STATUS ROLES AGE VERSION
vm1 Ready control-plane 10m v1.xx.x
vm2 Ready <none> 2m v1.xx.x
You'll also need to install a compatible CNI networking plugin before nodes and Pods can operate normally.
Step 4 — Add more workers later
You can add VM3, VM4, or VM5 using the same process.
Kubernetes Cluster
│
├── VM1: Control Plane
│
├── VM2: Worker
│ └── Pods
│
├── VM3: Worker
│ └── Pods
│
└── VM4: Worker
└── Pods
If your original join token expires, generate a new join command on VM1:
sudo kubeadm token create --print-join-command
K8s is simply an abbreviation for Kubernetes.
- K = First letter
- 8 = Eight letters between K and s (ubernetes)
- s = Last letter
Kubernetes (K8s) is a real software platform, just like Docker and Hyper-V,
opensource
Kubernetes (K8s) is a real software platform, just like Docker and Hyper-V,
Yes, exactly! Bare-metal Kubernetes means running Kubernetes directly on physical servers instead of virtual machines.
Yes, exactly! Bare-metal Kubernetes means running Kubernetes directly on physical servers instead of virtual machines.
The Kubernetes architecture and functionality are essentially the same. The main difference is the infrastructure underneath.
1. Kubernetes on VMs
Physical Server
│
└── Hypervisor (Proxmox / VMware)
│
├── VM1: Ubuntu + K8s Control Plane
│
├── VM2: Ubuntu + K8s Worker
│ ├── kubelet
│ ├── containerd
│ └── Pods
│
└── VM3: Ubuntu + K8s Worker
├── kubelet
├── containerd
└── Pods
2. Bare-metal Kubernetes
Physical Servers
│
├── Server 1: Ubuntu + K8s Control Plane
│
├── Server 2: Ubuntu + K8s Worker
│ ├── kubelet
│ ├── containerd
│ └── Pods
│
└── Server 3: Ubuntu + K8s Worker
├── kubelet
├── containerd
└── Pods
------------------------------------------------------
production Kubernetes, separating the control plane and worker nodes is generally good practice.
In Kubernetes, a node usually means a VM or a physical machine.
Kubernetes Cluster
│
├── VM1: Control Plane
├── VM2: Control Plane
├── VM3: Control Plane
│
├── VM4: Worker
│ └── Application Pods
│
├── VM5: Worker
│ └── Application Pods
│
└── VM6: Worker
└── Application Pods
Kubernetes Cluster
│
└── Worker Node (VM / Machine)
│
└── Pod: my-app
│
├── Container A: Main App
│ └── e.g. Go API on port 8080
│
├── Container B: Helper
│ └── e.g. Log collector
│
├── Container C: Helper
│ └── e.g. Metrics exporter
│
├── Shared Network
│ └── same Pod IP, talk via localhost
│
└── Shared Volumes
└── if configured
Kubernetes Cluster
│
├── Control Plane (VM1)
│ ├── API Server → Entry point for Kubernetes API
│ ├── Scheduler → Decides which node runs a Pod
│ ├── Controller Manager → Maintains desired state
│ └── etcd → Stores cluster configuration/state
│
└── Worker Node (VM2)
│
├── kubelet → Communicates with API Server
│ Ensures assigned Pods are running
│
├── containerd → Creates and manages containers
│ using a runtime such as runc
│
└── Pod: my-app → Groups related containers
├── Container A: Go API
├── Container B: Log Collector
├── Shared Network
└── Shared Volumes (if configured
A Kubernetes Pod is somewhat similar to a group of containers defined in a Docker Compose YAML file.
But there's one important distinction:
Docker Compose manages a group of containers, while a Kubernetes Pod is a runtime unit containing one or more tightly coupled containers.
1. Docker Compose
docker-compose.yaml
│
├── Go API Container
├── MySQL Container
└── Redis Container
All three containers are managed together by Docker Compose, but each typically has its own IP address and lifecycle.
2. Kubernetes
deployment.yaml
│
└── Deployment
│
├── Pod 1
│ ├── Go API Container
│ └── Log Collector Container
│
├── Pod 2
│ ├── Go API Container
│ └── Log Collector Container
│
└── Pod 3
├── Go API Container
└── Log Collector Container
Each Pod has its own IP address, and the containers inside that Pod share its network.
Pod: my-app
│
├── IP: 10.244.1.5
│
├── Container A: Go API
│ └── 10.244.1.5:8080
│
└── Container B: Log Collector
└── 10.244.1.5:9090
How communication works
Inside the same Pod:
curl http://localhost:8080 # Go API
curl http://localhost:9090 # Log Collector
From another Pod:
curl http://10.244.1.5:8080 # Go API
curl http://10.244.1.5:9090 # Log Collector
For docker compose each container must have its own IP in bridge mode
By default, Docker assigns each container its own IP address on a Docker bridge network.
For example:
Ubuntu VM (192.168.1.10)
│
├── Docker Bridge Network: 172.18.0.0/16
│ │
│ ├── Container A: Go API
│ │ ├── IP: 172.18.0.2
│ │ └── Port: 8080
│ │
│ └── Container B: Log Collector
│ ├── IP: 172.18.0.3
│ └── Port: 9090
│
└── Linux Kernel
you still generally need a load balancer in front of Kubernetes if you want to expose your application to external users. But Kubernetes already provides internal load balancing, so you don't necessarily need HAProxy or Nginx for every application.
Also, one correction: Kubernetes doesn't require 3 VMs. You can run Kubernetes on 1 or 2 VMs. Three nodes become particularly useful when you want a highly available control plane using etcd quorum.
1. Example: Three VMs running Kubernetes
External Clients
|
v
External Load Balancer
(HAProxy)
|
v
Kubernetes Cluster
|
+-------------+-------------+
| | |
VM1 VM2 VM3
Control Plane Control Plane Control Plane
+ Worker + Worker + Worker
| | |
Pod A Pod B Pod C
Go API Go API Go API
:8080 :8080 :8080
Recommended high avaibility
Kubernetes Cluster
│
├── Server 1
│ └── VM1: Control Plane + Worker
│ └── Go API Pod 1
│
├── Server 2
│ └── VM2: Control Plane + Worker
│ └── Go API Pod 2
│
└── Server 3
└── VM3: Control Plane + Worker
└── Go API Pod 3
ONE Kubernetes Cluster
|
+-------------+-------------+
| | |
Control Plane Control Plane Control Plane
1 2 3
| | |
+-------------+-------------+
|
Shared Cluster State
(3-member etcd)
|
+-------------+-------------+
| | |
Worker 1 Worker 2 Worker 3
| | |
Go API 1 Go API 2 Go API 3