A container is a unit of software that packages an application together with all its dependencies (libraries, runtime, configuration files) so that it runs the same in any environment: the developer's laptop, a test server, or the cloud in production. It solves the classic "it works on my machine." Docker is the most popular tool for building and running containers, and Kubernetes is the industry-standard orchestrator for managing thousands of containers in production: it deploys them, scales them, restarts them if they fail, and connects them to one another. Mastering both is essential in cloud-native architecture, because almost every modern application is delivered in containers. This lesson takes you from the concept to a real deployment in Kubernetes.

Contents

  1. Containers versus virtual machines.
  2. Anatomy of Docker: images, containers, and registries.
  3. Building an image with a Dockerfile.
  4. Why we need an orchestrator.
  5. Key Kubernetes concepts: Pod, Deployment, and Service.
  6. A Kubernetes manifest step by step.
  7. Common mistakes and tips.
  8. Exercises and solutions.

  1. Containers versus virtual machines

Both isolate applications, but in very different ways. A virtual machine (VM) virtualizes the complete hardware and includes an entire guest operating system. A container virtualizes only the operating system: it shares the host's kernel and packages only the application and its dependencies.

Aspect Virtual Machine Container
Isolation Complete hardware (hypervisor) Process (kernel namespaces)
Operating system A complete one per VM Shares the host kernel
Size Gigabytes Megabytes
Startup time Minutes Seconds or less
Density (how many per host) Few Many
Portability Medium Very high
graph TB
    subgraph "Virtual Machines"
        HW1[Hardware] --> HYP[Hypervisor]
        HYP --> VM1[Guest OS + App A]
        HYP --> VM2[Guest OS + App B]
    end
    subgraph "Containers"
        HW2[Hardware] --> OS[Host OS]
        OS --> DOCK[Docker Engine]
        DOCK --> C1[App A]
        DOCK --> C2[App B]
    end

Explanation of the diagram: in VMs each application carries its own complete operating system on top of a hypervisor, which consumes a lot of resources. In containers, the engine (Docker) shares a single host operating system, so containers are lightweight and start in seconds. They are not enemies: containers are often run inside VMs.

  1. Anatomy of Docker

Three concepts you should not confuse:

  • Image: an immutable, read-only template that contains your application and dependencies. It is like a "snapshot" or mold.
  • Container: a running instance of an image. From one image you can start many containers.
  • Registry: a store of images (Docker Hub, GitHub Container Registry, AWS ECR). You upload (push) and download (pull) images from there.
# Build an image from the Dockerfile in the current directory
docker build -t myapp:1.0 .

# Run a container from the image, publishing port 8080
docker run -d -p 8080:8080 --name myapp myapp:1.0

# View the running containers
docker ps

# Push the image to a registry
docker push myregistry.io/myapp:1.0

Explanation of the commands:

  • docker build -t myapp:1.0 .: builds the image. -t tags it with the name myapp and version 1.0; the . indicates that the Dockerfile is in the current directory.
  • docker run -d -p 8080:8080: starts a container. -d runs it in the background (detached); -p 8080:8080 maps the host port to the container port so it can be accessed from outside.
  • docker ps: lists the active containers, their status, and ports.
  • docker push: publishes the image to a registry so that others (or a cluster) can download it.

  1. Building an image with a Dockerfile

A Dockerfile is a text file with instructions for building the image, step by step. Each instruction creates a cacheable "layer." Let's look at a realistic example with a multi-stage build for a Java application:

# --- Stage 1: build ---
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests

# --- Stage 2: final, lightweight image ---
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=build /app/target/myapp.jar app.jar
EXPOSE 8080
USER 1000
ENTRYPOINT ["java", "-jar", "app.jar"]

Explanation instruction by instruction:

  • FROM maven:... AS build: starts from a base image with Maven and Java to build. We name it build so we can refer to it later.
  • WORKDIR /app: sets the working directory inside the image.
  • COPY pom.xml . + RUN mvn dependency:go-offline: we copy only the pom.xml first and download dependencies. This way, if the code changes but the dependencies do not, Docker reuses this layer from the cache and the build is faster.
  • COPY src ./src + RUN mvn package: copies the source code and generates the .jar.
  • Second FROM eclipse-temurin:17-jre-alpine: starts a much smaller final image (only the Java runtime, on top of Alpine Linux). This technique leaves out Maven and the source code, reducing size and attack surface.
  • COPY --from=build ...: copies only the compiled artifact from the previous stage.
  • EXPOSE 8080: documents the port the app uses.
  • USER 1000: runs the process as an unprivileged user (not root), a good security practice.
  • ENTRYPOINT [...]: the command that runs when the container starts.

  1. Why we need an orchestrator

Running one or two containers with docker run is easy. But what about hundreds of containers across dozens of servers? You need to solve: on which server do I place each container? What do I do if a container goes down? How do I scale from 3 to 30 replicas during a traffic spike? How do I update without downtime? How do they find each other? Doing this by hand is unfeasible. An orchestrator like Kubernetes automates all of this.

  1. Key Kubernetes concepts

Kubernetes (abbreviated K8s) organizes containers with several abstractions. The three fundamental ones:

Object What it is Analogy
Pod Smallest deployable unit: one or more containers that share network and storage A "capsule" with your app
Deployment Manages Pod replicas and controlled updates The "manager" that keeps N copies alive
Service Stable network access point to a set of Pods The "front desk" with a fixed address
  • Pod: the smallest thing Kubernetes deploys. Usually one container per Pod. Pods are ephemeral: they can die and be recreated with a different IP.
  • Deployment: you declare "I want 3 replicas of this image" and Kubernetes keeps them. If a Pod dies, it creates another. It enables rolling updates (updates without downtime).
  • Service: since Pods change IPs, the Service provides a stable address and distributes traffic among the healthy Pods (internal load balancing).
graph LR
    USER[User] --> SVC[Service: stable IP]
    SVC --> P1[Pod 1]
    SVC --> P2[Pod 2]
    SVC --> P3[Pod 3]
    DEP[Deployment] -.manages.-> P1
    DEP -.manages.-> P2
    DEP -.manages.-> P3

Explanation: the Deployment creates and watches the three Pods. The Service receives the user's traffic and distributes it among the available Pods. If Pod 2 goes down, the Deployment creates a new one and the Service stops sending it traffic until it is ready.

  1. A Kubernetes manifest step by step

In Kubernetes you describe the desired state in declarative YAML files. Here is a Deployment and a Service for our application:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-deployment
spec:
  replicas: 3                  # number of desired Pods
  selector:
    matchLabels:
      app: myapp               # selects the Pods with this label
  template:                    # template for each Pod
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: myapp
          image: myregistry.io/myapp:1.0
          ports:
            - containerPort: 8080
          resources:           # resource limits
            requests:
              cpu: "250m"
              memory: "256Mi"
            limits:
              cpu: "500m"
              memory: "512Mi"
          readinessProbe:      # when it is ready to receive traffic
            httpGet:
              path: /health
              port: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: myapp-service
spec:
  selector:
    app: myapp                 # routes to the Pods with this label
  ports:
    - port: 80                 # Service port
      targetPort: 8080         # container port
  type: ClusterIP              # accessible only within the cluster

Explanation field by field:

  • kind: Deployment and replicas: 3: we request a Deployment that maintains three replicas.
  • selector.matchLabels + template.metadata.labels: the Deployment binds the Pods that have the label app: myapp. Labels are the glue of Kubernetes.
  • image: the registry image we saw earlier.
  • resources.requests/limits: requests is what the Pod reserves as a minimum; limits is the maximum it can consume. 250m means 0.25 of a CPU; 256Mi is 256 mebibytes of RAM. This prevents one Pod from starving the others.
  • readinessProbe: Kubernetes queries /health to know when the Pod is ready. Until it responds OK, the Service does not send it traffic.
  • In the Service, selector: app: myapp connects the Service to those Pods; port: 80 is where the Service listens and targetPort: 8080 is the container port; type: ClusterIP makes it accessible only internally (to expose it externally you use LoadBalancer or an Ingress).
  • The --- separates two YAML documents in the same file.
# Apply the manifest to the cluster
kubectl apply -f myapp.yaml

# View the status of the Pods
kubectl get pods

# Scale to 5 replicas live
kubectl scale deployment myapp-deployment --replicas=5

Explanation: kubectl apply sends the desired state to the cluster, which takes care of reaching it. kubectl get pods shows the Pods and their status. kubectl scale changes the number of replicas without editing the file.

Common Mistakes and Tips

  • Building huge images. Use multi-stage builds and lightweight base images (alpine, distroless). A 1 GB image is slow to deploy and a security risk.
  • Running as root. Define an unprivileged USER in the Dockerfile. A container compromised as root is more dangerous.
  • Not defining requests and limits. Without limits, a Pod can consume all the node's memory and take down its neighbors.
  • Forgetting probes. Without a readinessProbe/livenessProbe, Kubernetes sends traffic to Pods that are not yet ready or does not restart those that have hung.
  • Treating Pods as pets. Pods are cattle, not pets: they are ephemeral and replaceable. Do not store state inside them; use volumes or external services.
  • Tip: always version images with explicit tags (myapp:1.0); never rely on latest in production because it is ambiguous and makes rollbacks harder.

Exercises

  1. Explain in your own words why a container starts in seconds and a VM in minutes.
  2. In the example manifest, what happens if you manually delete one of the Deployment's three Pods? Why?
  3. You want to expose your application to external Internet traffic. The example Service is of type ClusterIP. What would you change and what alternatives do you have?

Solutions

  1. The container shares the host operating system's kernel and only starts the application process with its dependencies, whereas the VM must boot a complete guest operating system (kernel, services, etc.) on top of the hypervisor. Less to boot means less time.
  2. The Deployment detects that there are only 2 Pods when the desired state is 3 and automatically creates a new Pod to return to 3 replicas. Kubernetes continuously reconciles the actual state with the declared one.
  3. You would change type: ClusterIP to type: LoadBalancer (which provisions a cloud provider's load balancer with a public IP) or, more commonly and flexibly, keep the Service as ClusterIP and place an Ingress in front of it with host/path routing rules and TLS termination.

Conclusion

You have learned the difference between containers and VMs, how Docker packages applications using images and Dockerfiles, why an orchestrator is needed, and the three pillars of Kubernetes (Pod, Deployment, Service) expressed in a real YAML manifest. Containers give you portability and Kubernetes gives you scale and resilience. In the next lesson we will explore an even more abstract model in which you do not even manage containers or servers: serverless architecture.

Application Architecture Course

Module 1: Fundamentals of Application Architecture

Module 2: Design Principles and Tactics

Module 3: Architectural Styles and Patterns

Module 4: Distributed Architectures and Microservices

Module 5: Event-Driven Architectures and Messaging

Module 6: Domain-Driven Design (DDD)

Module 7: Data and Persistence

Module 8: Cloud Architecture and Deployment

Module 9: Quality, Security and Observability

Module 10: Evolution, Governance and Case Studies

© Copyright 2026. All rights reserved