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
- Containers versus virtual machines.
- Anatomy of Docker: images, containers, and registries.
- Building an image with a Dockerfile.
- Why we need an orchestrator.
- Key Kubernetes concepts: Pod, Deployment, and Service.
- A Kubernetes manifest step by step.
- Common mistakes and tips.
- Exercises and solutions.
- 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]
endExplanation 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.
- 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.-ttags it with the namemyappand version1.0; the.indicates that the Dockerfile is in the current directory.docker run -d -p 8080:8080: starts a container.-druns it in the background (detached);-p 8080:8080maps 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.
- 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 itbuildso 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 thepom.xmlfirst 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.
- 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.
- 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.-> P3Explanation: 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.
- 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 clusterExplanation field by field:
kind: Deploymentandreplicas: 3: we request a Deployment that maintains three replicas.selector.matchLabels+template.metadata.labels: the Deployment binds the Pods that have the labelapp: myapp. Labels are the glue of Kubernetes.image: the registry image we saw earlier.resources.requests/limits:requestsis what the Pod reserves as a minimum;limitsis the maximum it can consume.250mmeans 0.25 of a CPU;256Miis 256 mebibytes of RAM. This prevents one Pod from starving the others.readinessProbe: Kubernetes queries/healthto know when the Pod is ready. Until it responds OK, the Service does not send it traffic.- In the Service,
selector: app: myappconnects the Service to those Pods;port: 80is where the Service listens andtargetPort: 8080is the container port;type: ClusterIPmakes it accessible only internally (to expose it externally you useLoadBalanceror anIngress). - 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
USERin 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 onlatestin production because it is ambiguous and makes rollbacks harder.
Exercises
- Explain in your own words why a container starts in seconds and a VM in minutes.
- In the example manifest, what happens if you manually delete one of the Deployment's three Pods? Why?
- 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
- 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.
- 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.
- You would change
type: ClusterIPtotype: LoadBalancer(which provisions a cloud provider's load balancer with a public IP) or, more commonly and flexibly, keep the Service asClusterIPand 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
- What Is Application Architecture?
- The Role of the Software Architect
- Quality Attributes and Non-Functional Requirements
- Architectural Decisions and Trade-offs
- Architecture Documentation: Views and the C4 Model
Module 2: Design Principles and Tactics
- Coupling, Cohesion and Separation of Concerns
- SOLID Principles Applied to Architecture
- DRY, KISS, YAGNI and Other Design Principles
- Architectural Tactics for Quality Attributes
- Managing Technical Debt
Module 3: Architectural Styles and Patterns
- Monolithic Architecture
- Layered Architecture (N-Tier)
- Client-Server Architecture
- Hexagonal Architecture (Ports and Adapters)
- Clean and Onion Architecture
Module 4: Distributed Architectures and Microservices
- Introduction to Distributed Systems
- Microservices Architecture
- Service Decomposition and Bounded Contexts
- API Gateway, Service Discovery and Inter-Service Communication
- Resilience Patterns: Circuit Breaker, Retry and Bulkhead
- The CAP Theorem and Data Consistency
Module 5: Event-Driven Architectures and Messaging
- Fundamentals of Event-Driven Architecture
- Asynchronous Messaging: Queues and Brokers
- Event Patterns: Event Sourcing and CQRS
- Managing Distributed Transactions: The Saga Pattern
- Real-Time Data Streaming
Module 6: Domain-Driven Design (DDD)
- Core DDD Concepts
- Strategic Design: Bounded Contexts and Ubiquitous Language
- Tactical Design: Entities, Aggregates and Repositories
- Context Mapping
Module 7: Data and Persistence
- Persistence Strategies: SQL vs NoSQL
- Data Access Patterns: Repository, Unit of Work and DAO
- Database per Service and Distributed Data Management
- Caching and Invalidation Strategies
Module 8: Cloud Architecture and Deployment
- Cloud Computing Fundamentals (IaaS, PaaS, SaaS)
- Containers and Orchestration with Docker and Kubernetes
- Serverless Architecture
- Cloud-Native Design Patterns
- Infrastructure as Code (IaC)
Module 9: Quality, Security and Observability
- Scalability: Horizontal vs Vertical and Load Balancing
- High Availability and Fault Tolerance
- Security by Design and Authentication/Authorization
- Observability: Logging, Metrics and Tracing
- Performance and Load Testing
