Dev.to · 11 min read

Building Container Images and Container Orchestration Fundamentals

Building Container Images and Container Orchestration Fundamentals

If you have spent any time around modern software development, you have heard the phrase "it works on my machine." Docker was built to kill that excuse for good. By packaging everything an application needs into a single portable unit called a container image, Docker ensures that what runs on your laptop runs identically in staging, in CI, and in production. This post covers how container images are built, how to build them well, and how orchestration systems like Kubernetes solve the much harder problem of running them at scale. What Is a Docker Container Image? A Docker container image is a lightweight, standalone, executable package that includes everything needed to run an application: code, runtime, system tools, system libraries, and configuration. When you run an image, Docker creates a live instance of it called a container, an isolated process running on the host operating system's kernel, sharing that kernel but isolated from everything else via Linux namespaces and cgroups. The critical distinction: an image is static (think: a blueprint), while a container is dynamic (think: a running building). You can spin up dozens of containers from a single image, each completely isolated from the others. Building Images with a Dockerfile Images are built using a Dockerfile, a plain text file containing a set of instructions that closely resembles how you would manually set up a server, step by step. Docker reads these instructions top to bottom and builds the image layer by layer, caching each layer for faster subsequent builds. A typical Dockerfile does the following: Starts from a base image (a pre-built starting point, such as python:3.11-slim or ubuntu:22.04) Sets a working directory inside the container Copies dependency definitions and installs them Copies the application source code Documents which port the application listens on Defines the command to run when the container starts Here is a minimal but correct example for a Python Flask application: # Use the official slim Python 3.11 base image FROM python:3.11-slim # Set the working directory inside the container WORKDIR /app # Copy and install dependencies first (leverages layer caching) COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # Copy application source code COPY app.py . # Document the port the app listens on (informational only, does not publish it) EXPOSE 5000 # Define the default startup command CMD ["python", "app.py"] Important: EXPOSE does not actually publish the port to the host. It is documentation that tells Docker and developers which port the application uses. To make it accessible, you publish the port at runtime with docker run -p 5000:5000. Building and Tagging the Image docker build -t my-flask-app:1.0 . The -t flag tags the image with a name and version. Tags are critical for version management. Never rely on latest in production. Dockerfile Best Practices Worth Knowing in 2025 A Dockerfile that works is easy to write. One that is efficient, secure, and maintainable in production takes more thought. Here are the practices that matter most: 1. Pin Your Base Image Version # Bad: resolves to whatever "latest" is today FROM python:latest # Good: reproducible builds FROM python:3.11-slim Unpinned base images break builds silently when upstream releases change. 2. Order Instructions by Change Frequency Docker builds images by executing each instruction in a Dockerfile sequentially, creating a distinct layer for each step, and caches these layers for reuse in subsequent builds. Copy dependency files (requirements.txt, package.json) and install them before copying your application code. Application code changes constantly; dependencies change rarely. Keeping them in separate layers means a code change only invalidates the layers after it, not the slow dependency install layer. 3. Use Multi-Stage Builds Multi-stage builds involve using multiple FROM instructions within a single Dockerfile, allowing you to separate the build environment from the final runtime environment, resulting in significantly smaller images because they contain just the application and its essential runtime dependencies, not the entire compiler and SDK. Companies implementing multi-stage builds report average image size reductions of 50 to 85%. # Stage 1: Build FROM python:3.11 AS builder WORKDIR /app COPY requirements.txt . RUN pip install --user -r requirements.txt # Stage 2: Runtime (only what is needed to run) FROM python:3.11-slim WORKDIR /app COPY --from=builder /root/.local /root/.local COPY app.py . CMD ["python", "app.py"] 4. Never Run as Root A survey by Sysdig found that 58% of production containers still run as root, creating significant security exposures. Add a non-root user and switch to it before the CMD: RUN adduser --disabled-password appuser USER appuser 5. Use a .dockerignore File Just like .gitignore, a .dockerignore prevents files like .git, node_modules, .env, and local test data from being copied into the image, keeping it small and preventing accidental secret leakage. 6. Scan Your Images A 2024 study found that 87% of Docker images contain at least one high or critical vulnerability, making regular scanning non-negotiable. Tools like docker scout, trivy, and snyk integrate directly into CI/CD pipelines to catch vulnerabilities before they reach production. Sharing Images: Container Registries Once built, images need to be shared across teams, CI systems, and deployment environments. This is done through a container registry, essentially a web server for storing and distributing container images. Major options include: Docker Hub: the default public registry, free for public images Amazon Elastic Container Registry (ECR): tightly integrated with AWS ECS, EKS, and Lambda Google Artifact Registry: the recommended registry for GCP workloads (supersedes Container Registry) GitHub Container Registry (GHCR): native integration with GitHub Actions pipelines Azure Container Registry (ACR): native integration with AKS and Azure DevOps # Tag your image for a registry docker tag my-flask-app:1.0 ghcr.io/your-username/my-flask-app:1.0 # Push to the registry docker push ghcr.io/your-username/my-flask-app:1.0 # Pull it on another machine docker pull ghcr.io/your-username/my-flask-app:1.0 Private registries support access controls, vulnerability scanning, and image signing, all critical for production workflows. Container Orchestration Fundamentals Running a single container on a single machine is straightforward. Running hundreds of containers across dozens of servers, keeping them healthy, scaling them under load, routing traffic to them, and recovering from failures automatically. That is a completely different problem. This is what container orchestration solves. The Problem: Why Manual Management Fails at Scale Modern applications increasingly adopt microservices architectures, where an application is decomposed into small, specialized, independently deployable services, each running in its own container. A single e-commerce platform might have separate containers for authentication, product catalog, cart, payments, notifications, and search. At scale, managing these manually becomes operationally impossible. The core problems orchestration systems solve: Compute provisioning: determining which servers or VMs are available to run containers Scheduling: placing containers on the right servers based on available resources, affinity rules, and constraints Resource allocation: assigning CPU and memory limits to each container to prevent noisy-neighbor problems Availability: continuously health-checking containers and automatically replacing failed ones Scaling: adding or removing container instances in response to demand (horizontal scaling) Networking: giving containers stable network identities, enabling service-to-service communication, and routing external traffic in Storage: providing persistent storage for stateful containers (databases, message queues) that survives container restarts The Two Core Components of Every Orchestration System Every orchestration platform, regardless of vendor, is organized around the same two-tier model: The Control Plane (the brain) The Control Plane manages the overall state of the cluster and is responsible for deciding how the system should run. Whenever you make a request to the Kubernetes API, it interacts with the Control Plane, which then communicates the necessary adjustments to the Worker Nodes. The Worker Nodes (the muscle) Worker Nodes are the actual executors of workloads within a Kubernetes cluster. While the Control Plane handles management decisions and monitors the state of the cluster, Worker Nodes are responsible for the actual execution of the containers that make up the applications. The Orchestration Landscape: From Many Options to One Clear Standard Container orchestration has gone through a rapid consolidation over the past decade. Several systems competed for dominance: Docker Swarm: Docker's built-in clustering mode, simpler than Kubernetes but far less capable. Now largely abandoned for production use. Apache Mesos: A general-purpose cluster manager from Twitter and Airbnb. Powerful but complex. Twitter, one of its biggest champions, migrated away from it to Kubernetes. HashiCorp Nomad: A lightweight, multi-workload scheduler that handles containers, VMs, and non-containerized apps. Still actively maintained and used by organizations that want something simpler than Kubernetes, but a distant second in adoption. Kubernetes (K8s): Originally designed by Google based on lessons from its internal Borg system and open-sourced in 2014. Now the undisputed industry standard. Today, Kubernetes runs everywhere: on-premises, on every major cloud provider (EKS on AWS, GKE on GCP, AKS on Azure), and as distributions from Red Hat (OpenShift), Rancher, and others. The CNCF (Cloud Native Computing Foundation), which governs Kubernetes, reports that over 96% of organizations are either using or evaluating Kubernetes. A Closer Look at Kubernetes Architecture Since Kubernetes has become the default answer to container orchestration, it is worth understanding its architecture in more depth. The Control Plane Components The control plane is the "brain" of a Kubernetes cluster, responsible for managing its overall state. It consists of several core components: API Server (kube-apiserver): The central communication hub. It exposes the Kubernetes API, the primary interface for interacting with the cluster. All cluster operations transit through the API server. Scheduler (kube-scheduler): Determines where pods (the smallest deployable units in Kubernetes) run on worker nodes. It considers factors like resource requests, node constraints, and data locality to make efficient placement decisions. Controller Manager (kube-controller-manager): A collection of individual controllers that continuously monitor the cluster's state and take corrective actions to maintain the desired state. etcd: A distributed key-value store used by Kubernetes for persistent storage of all cluster data. This is where configuration data, state information, and metadata are stored, making etcd a critical component for data recovery and cluster state maintenance. The Worker Node Components Worker nodes run application workloads and include kubelet for communication with the control plane, kube-proxy for networking, and a container runtime (such as containerd or Docker) to manage containers. Kubelet — an agent that constantly communicates with the Control Plane, receiving instructions about the desired state of containers and monitoring the resources running on the node. Kube-proxy — manages network rules on each node, enabling pods to communicate with each other and with services outside the cluster. Container runtime — the engine that actually pulls and runs container images. Kubernetes supports containerd, CRI-O, and others. Docker as a runtime was deprecated in Kubernetes 1.24. The Fundamental Design Philosophy: Desired State What makes Kubernetes powerful is its declarative model. You describe what you want (three replicas of this container, always), and Kubernetes continuously works to make reality match that description. If a pod crashes, the Controller Manager notices and schedules a replacement. If a node goes down, its pods are rescheduled elsewhere. You do not issue commands like "restart this container" — you declare desired state and Kubernetes reconciles it continuously. # A simple Kubernetes Deployment — declare what you want apiVersion: apps/v1 kind: Deployment metadata: name: flask-app spec: replicas: 3 # Always keep 3 copies running selector: matchLabels: app: flask-app template: metadata: labels: app: flask-app spec: containers: - name: flask-app image: ghcr.io/your-username/my-flask-app:1.0 ports: - containerPort: 5000 Apply this with kubectl apply -f deployment.yaml and Kubernetes handles the rest — scheduling, health checking, and self-healing automatically. Putting It All Together The journey from code to production with containers looks like this: Write code → Define a Dockerfile → Build and tag the image → Push to a registry → Deploy to an orchestrated cluster → Orchestrator manages it from there This pipeline is repeatable, environment-agnostic, and automatable end to end through CI/CD tools like GitHub Actions, GitLab CI, or Jenkins. Every step after "push to registry" can be fully automated — meaning a code commit can trigger a production deployment with no manual steps and no "it works on my machine" surprises. Understanding containers and orchestration is not optional knowledge for modern developers and data engineers. It is the foundation on which virtually every cloud-native application, data pipeline, and AI workload is built today. Are you running containers in production? What orchestration setup are you using? Share in the comments below.

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More Startup & VC News