Docker and Podman orchestration, limitation and emergence of Kubernetes

Continuation from here . Here’s the summary.

To optimize a data center, effective control over computing resources is essential, and containers are vital for this. They encapsulate applications with all necessary components, allowing them to run on any server that supports containers. This on-demand operation aligns with serverless architecture, enabling resource allocation efficiently to balance performance, cost, and sustainability.

Tools like Docker and Podman manage container lifecycles—creating, running, stopping, and restarting them. They facilitate automation, support deployment, and offer fault tolerance features, such as Docker Swarm for automatic restarts of failed containers and Podman for service recovery.

The key difference lies in their operation: Docker relies on a background service (dockerd), while Podman interacts directly with the host system, making it lighter and more secure. In summary, containers are essential for efficient infrastructure, with Docker and Podman enabling reliable resource management.

The Declarative Model in Docker: A Step Toward Smarter Automation

Lets focus on Docker only.

As container-based systems grow more complex, managing them manually becomes harder. That’s where the declarative model starts to shine — and Docker, while traditionally imperative, has started to support this idea through tools like Docker Compose and Docker Swarm.

In an imperative model, you tell the system (i.e. Docker) how to do something step by step — for example, start this container, then expose this port. But in a declarative model, you describe what the final state should look like — such as I want three replicas of this service running, and the system figures out how to get there.

For example:

Docker run hello-world

This is an example of docker imperative model. Whereas, the command

docker service scale my-service=5

manages 5 docker instances in the cluster. This is an example of declarative model.

Docker Compose lets you define your desired setup — in a YAML file. The benefit for that, you can declare your expected behavior from the docker in a structured file, which makes it easy to convey and share the user defined policies in a structured declarative way. You can simply use that YAML file to point to any other docker environment. For example, instead of docker run hello-world, you can do the following,

version: '3.8'

services:
  hello:
    image: hello-world

save this as hello.yaml file. Check if docker compose installed (i.e. docker compose version). Run docker compose -f hello.yaml up, and check the output.

Docker Swarm

Lets work with docker service scale my-service=5 part. Save the following content in a file named nginx.yaml. And paste the following.

version: '3.8'
services:
  my-service:
    image: nginx
    deploy:
      replicas: 5
    ports:
      - "8080:80"

 

Lets set up the minimal docker swarm cluster on localhost. Find your localhost discover able IP address. Run ifconfig to find local network interfaces, and use one of them. Please note 127.0.0.1 or localhost will not work for the below command.

docker swarm init --advertise-addr 192.168.122.1

Please note, 192.168.122.1 is the ip address of one my local interfaces. Direct interface name also works (for example docker swarm init --advertise-addr 1virbr0).

This would provide you a output like this:

docker swarm join --token SWMTKN-1-xxxxx 192.168.122.1:2377. This is useful when you want to deploy multiple worker nodes to attach with the interface. In each Node/server, run docker swarm join --token SWMTKN-1-xxxxx 192.168.1.100:2377 command to connect the server with the swarm cluster.

Next, Deploy the swarn stack with following command

docker stack deploy -c nginx.yaml mystack

Then run docker service ls command to check the replicas.

Need more fine grained control on cloud resources, Emergence of Kubernetes

Kubernetes is a free, open-source platform for managing containers. Originally created by Google in 2014, it was designed to support cloud-native applications and microservice-based architectures.

In simple terms, Kubernetes works in a way that’s familiar to Docker users. Each machine in the system—called a node—runs a small program called a kubelet. This is somewhat similar to Docker’s dockerd, as it helps manage the container workloads running on that node. These kubelets report to a central control unit, the kube-apiserver, much like how Docker uses the command line or Compose as its interface.

Kubernetes simplifies using cloud systems, giving users detailed control over managing their containers. Like Docker, it uses a declarative model defined through YAML files, allowing users to describe the desired state of their containerized applications. It follows a clear model like Docker or Podman, making it flexible.

Another strength of Kubernetes is that it supports multiple container engines like runc, containerd, CRI-O, and even the Docker Engine. This makes it highly compatible across different environments.

The true power of Kubernetes lies in its ability to abstract the entire cloud environment. While Docker gives you the concept of a container, Kubernetes builds on top of that by introducing higher-level abstractions like Pods, Deployments, and ReplicaSets. These not only group containers together but also enable intelligent scheduling, self-healing, and scaling—all without you needing to micromanage each container.

In Kubernetes, everything is modeled as a Resource. These resources represent different parts of your infrastructure and application. For example, a Pod is a resource that groups one or more containers, allowing them to share CPU, memory, and networking. A ReplicaSet is another resource that ensures a certain number of Pods are always running. With a simple YAML file, you can declare that you want five Pods active at all times, and Kubernetes will automatically make sure that happens — even restarting them if something fails. Here is an example of a yaml file:

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: nginx
spec:                         # ← This is the ReplicaSet spec
  replicas: 5
  selector:
    matchLabels:
      app: nginx
  template:                   # ← This defines the pod template
    metadata:
      labels:
        app: nginx
    spec:                     # ← This is the Pod spec
      containers:
        - name: nginx
          image: nginx

Save this as nginx.yaml, then run kubectl apply -f nginx.yaml. This will perform the same function as Docker Swarm’s command [docker stack deploy -c nginx.yaml mystack]. Both ensure that 5 nginx replica containers are always active.

Pod and Replicaset are two types of Resources in Kubernetes. There are many different resource concept in Kubernetes world. There is another resource concept named Deployment, which offers more fine grained control to the replica containers.

Probes and Fault Tolerance: Kubernetes vs. Docker Swarm

Deploying five replicas and maintaining their state continuously is a key aspect of the declarative model. Both Kubernetes and Docker Swarm support this. However, native Kubernetes offers greater fault tolerance and high availability compared to Docker Swarm.

In Docker Swarm, health checks (probes) indicate whether a container is alive — meaning the process is running. But Kubernetes was designed with more detailed health control: it introduces both liveness probes, which check if a container is healthy, and readiness probes, which check if the container is ready to serve traffic.

For those who find it hard to distinguish between a “healthy” and a “ready” container, imagine this scenario:


You have an application that connects to a remote database and sets up a DBMS instance or ORM object on startup. Once initialized, the app can forward query requests to the database without reinitializing every time.

Now, when the container just starts, there’s a delay while the app initializes the DB connection or ORM layer. During this warm-up period, your application is running — so a liveness probe would report success — but it’s not yet ready to handle traffic. If the load balancer starts sending requests during this time, they might fail or timeout because the app isn’t fully initialized.

That’s where readiness probes come in. They signal Kubernetes to hold off on routing traffic until the container is truly ready. Liveness probes ensure the container is still functioning. Only when both probes pass does Kubernetes allow the service to forward traffic to the pod.

Docker Swarm uses a model similar to liveness probes, but lacks native support for readiness and startup probes. That’s one of the reasons Kubernetes is considered more fault tolerant — it can intelligently manage the replacement and removal of containers using a combination of these probes, offering more fine-grained control and smoother recovery in real-world applications.

Rolling update

Imagine you have five containers running the same version of a SQL ORM, and a load balancer is distributing incoming query traffic evenly across them. Now, you need to upgrade the ORM version — but without causing any downtime. That means all five instances must remain available at all times to meet your Quality of Service (QoS) requirements. You can’t afford to stop a container, update it, and restart — doing so would risk service disruption.

Here’s how you can perform the update without any performance hit:

  1. Create a temporary container (let’s call it tc1) with the updated ORM version.
  2. Wait until both liveness and readiness probes for tc1 succeed — this confirms it’s healthy and ready to serve traffic.
  3. Add tc1 to the load balancer so it begins handling real traffic.
  4. Remove one of the existing containers from the load balancer and shut it down.

This strategy — which might seem manual — is exactly what Kubernetes automates through its built-in rolling update mechanism. By configuring your deployment with a rolling update strategy and tuning parameters like maxSurge and maxUnavailable, Kubernetes takes care of the entire process. It ensures your application stays fully available and performs reliably while containers are updated or patched in the background.

And yes — this functionality comes from the Deployment resource in Kubernetes, not the ReplicaSet itself. While the ReplicaSet is responsible for maintaining the desired number of Pods, it’s the Deployment that defines how updates are handled. Simply changing the kind from ReplicaSet to Deployment in your YAML file unlocks this powerful behavior, without needing to rewrite the rest of the configuration.

Here’s the block you need to include in your YAML file to enable this behavior:

strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 2
maxUnavailable: 0

This tells Kubernetes:

  • It may create up to 2 extra Pods (i.e.containers) during the update,
  • It should not remove any old Pods until the new ones are confirmed to be ready.

Let’s say your current Deployment runs 5 replicas of an Nginx-based service, and the Deployment is named nginx. When you want to upgrade to a newer version (e.g., nginx:1.24), you’d simply run:

kubectl set image deployment/nginx nginx=nginx:1.24

Kubernetes will then perform the following steps automatically:

  1. Create two new temporary containers (e.g., tc1 and tc2) using the new nginx:1.24 image,
  2. Wait for both containers to pass liveness and readiness probes,
  3. Add tc1 and tc2 to the load balancer and begin routing traffic to them,
  4. Remove two old containers from the load balancer and shut them down,
  5. Repeat the process until all the old containers are replaced with updated ones.

By the end of the rollout, you’re back to 5 up-to-date replicas — with zero disruption to users and no manual intervention. That’s the power of Kubernetes’ declarative model and rolling update strategy.

This post is already getting a bit long, so I’ll pause here. In my next post, I’ll dive deeper into some of the core strengths of Kubernetes, including autoscaling, stateful workloads, and service discovery. Stay tuned!

Leave a comment