Skip to content
S sufi.my
Back to Blog

Article

ELI5: Docker Containers Explained

Understanding containers, images, and why they matter for deployment.

February 15, 2026 · 9 min read

What is Docker? Why Should You Care?

Imagine you’re shipping a cake. With traditional deployment, you’d send the recipe, ingredients, and hope the baker has the same oven, same flour quality, and same kitchen setup as you. It probably won’t taste the same.

Docker solves this by shipping the entire kitchen — the recipe, pre-measured ingredients, the exact oven settings, and everything else. When it arrives, you just turn it on. It works the same every single time.

That’s containerization in a nutshell.

Containers vs Virtual Machines: The Real Difference

People often confuse containers with VMs. Here’s the key difference:

Virtual Machine (VM):

  • Runs a full operating system
  • Needs 500 MB - 2 GB of RAM per instance
  • Slow to start (takes minutes)
  • Heavy isolation (good for untrusted code)
  • Takes a lot of disk space

Container:

  • Shares the host OS kernel
  • Needs 10-50 MB of RAM typically
  • Starts in milliseconds
  • Lighter isolation (good for your own code)
  • Minimal disk footprint

Think of it this way:

  • VMs are like shipping a house (with walls, foundation, kitchen)
  • Containers are like shipping a prefab living room (uses your house’s foundation, electricity, plumbing)
VMs:
┌─────────────────────┐
│  App               │
│  Guest OS          │
│  Hypervisor        │
└─────────────────────┘
  Heavy, isolated

Containers:
┌─────────┐ ┌─────────┐ ┌─────────┐
│ App     │ │ App     │ │ App     │
│ Libs    │ │ Libs    │ │ Libs    │
└─────────┴─┴─────────┴─┴─────────┘
   Shared Host OS Kernel
  Lightweight, fast

Core Concepts: Images and Containers

Image: A blueprint or snapshot. Like a recipe. It’s read-only, and it describes everything needed to run your application.

Container: A running instance of an image. Like baking from the recipe. Many containers can run from the same image.

An analogy:

  • Image = Class in programming
  • Container = Instance of that class

You can have one ubuntu:22.04 image and run 100 different containers from it, each with its own isolated filesystem and processes.

Dockerfile: Building Your Image

A Dockerfile is the recipe. It contains instructions for what goes into your image.

Here’s a simple example for a Node.js app:

# Start from an official Node.js image
FROM node:18-alpine

# Set the working directory inside the container
WORKDIR /app

# Copy your code into the container
COPY . .

# Install dependencies
RUN npm install

# Expose the port your app listens on
EXPOSE 3000

# The command to run when the container starts
CMD ["npm", "start"]

Let’s break this down:

  • FROM: Base image (the starting point)
  • WORKDIR: Directory where commands run (like cd /app)
  • COPY: Copy files from your machine into the container
  • RUN: Execute commands during build (installs, setup, etc.)
  • EXPOSE: Documents which ports the app uses (informational)
  • CMD: Default command when container starts

When you run docker build, Docker reads the Dockerfile and creates an image.

Real Example: A Python Flask App

FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .

RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 5000

CMD ["python", "app.py"]

Build it with:

docker build -t my-flask-app:1.0 .

Run it with:

docker run -p 5000:5000 my-flask-app:1.0

The -p 5000:5000 maps port 5000 on your machine to port 5000 in the container.

Docker Compose: Running Multiple Containers

Real applications need multiple services. Your Node.js app needs a database. Your backend needs a message queue. Running each with a separate docker run command gets messy fast.

docker-compose.yaml orchestrates multiple containers as a single unit:

version: '3.8'

services:
  web:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://db:5432/myapp
    depends_on:
      - db

  db:
    image: postgres:15
    environment:
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: myapp
    volumes:
      - db_data:/var/lib/postgresql/data

volumes:
  db_data:

Now run everything with:

docker-compose up

That’s it. One command starts your web service and database, connects them, and manages their lifecycle. The depends_on ensures the database starts first.

Why This Matters

Before Docker Compose:

docker run -d --name db postgres:15
docker run -d --name web -p 3000:3000 --link db my-app
# Hope you remember the right order and flags...

With Docker Compose:

docker-compose up

Much better.

Essential Docker Commands

# Build an image
docker build -t image-name:tag .

# Run a container
docker run -p 8000:8000 image-name:tag

# List running containers
docker ps

# List all containers (including stopped)
docker ps -a

# Stop a container
docker stop container-id

# Remove a container
docker rm container-id

# View logs
docker logs container-id

# Execute a command inside a running container
docker exec -it container-id bash

# Push to a registry (Docker Hub, etc.)
docker push username/image-name:tag

# Pull an image from a registry
docker pull username/image-name:tag

Real-World Example Session

# Build your app
docker build -t my-app:1.0 .

# Run it
docker run -d -p 8080:8000 --name running-app my-app:1.0

# Check it's running
docker ps

# See what it's printing
docker logs running-app

# Oops, need to debug. Get a shell
docker exec -it running-app bash

# When done
docker stop running-app

Volumes: Persistent Data

By default, when a container stops, its filesystem is deleted. For databases and important files, you need volumes — persistent storage outside the container.

There are three types:

# Create a volume
docker volume create my-data

# Use it
docker run -v my-data:/data myimage

Everything written to /data inside the container is stored in my-data on your machine.

2. Bind Mounts (Good for development)

docker run -v /Users/you/code:/app myimage

Your local /Users/you/code folder is mounted at /app inside the container. Changes sync both ways.

3. Anonymous Volumes

docker run -v /data myimage

Docker creates a temporary volume that’s deleted when the container stops.

In docker-compose:

services:
  db:
    image: postgres:15
    volumes:
      - db_data:/var/lib/postgresql/data  # Named volume
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql  # Bind mount

volumes:
  db_data:

Networking Basics

Containers are isolated. They can’t talk to each other or the outside world by default.

Port Mapping

docker run -p 8080:3000 myapp

Maps port 8080 on your machine to port 3000 in the container. External traffic reaches the container.

Container-to-Container Communication

With docker-compose, services can reach each other by name:

services:
  web:
    image: myapp
    environment:
      DATABASE_HOST: db  # Can use 'db' as hostname!

  db:
    image: postgres:15

The web container can connect to postgres://db:5432 because Docker’s internal DNS resolves db to the database container.

Without docker-compose, use custom networks:

docker network create mynet
docker run --network mynet --name db postgres:15
docker run --network mynet -e DB_HOST=db myapp

Common Docker Pitfalls

1. Large Image Size

Problem: Your image is 1 GB.

Solution: Use smaller base images.

# Bad
FROM ubuntu:22.04

# Good
FROM python:3.11-slim

# Even better for production
FROM python:3.11-alpine

Alpine is tiny (5-40 MB) vs Ubuntu (200+ MB).

2. Layers and Build Cache

Docker builds images in layers. Each instruction creates a new layer.

FROM node:18
COPY . /app          # Layer 1
RUN npm install      # Layer 2 (takes 2 minutes)
CMD ["npm", "start"] # Layer 3

If you change one file and rebuild, Docker re-uses layers 1 and 3 but rebuilds layer 2 (slow).

Solution: Order instructions by change frequency (least-changing first).

FROM node:18
COPY package.json /app/     # Changes rarely
RUN npm install             # Cached most times
COPY . /app                 # Changes frequently
CMD ["npm", "start"]

3. Running as Root

By default, containers run as root. Bad idea.

FROM node:18
RUN useradd -m app
USER app  # Switch to non-root user
COPY . /app
CMD ["npm", "start"]

4. Not Using .dockerignore

Without it, COPY . . copies everything — node_modules, .git, build artifacts, secrets. Huge image.

# .dockerignore
node_modules/
.git/
.env
dist/
build/

5. Forgetting to Clean Up in RUN

# Bad: 2GB image
RUN apt-get update && apt-get install -y gcc

# Good: Cleans up apt cache
RUN apt-get update && apt-get install -y gcc && rm -rf /var/lib/apt/lists/*

6. Not Using Multi-Stage Builds

Build artifacts can be huge. Don’t ship them.

# Stage 1: Build
FROM node:18 as builder
COPY . /app
RUN npm install && npm run build

# Stage 2: Runtime (only what's needed)
FROM node:18-alpine
COPY --from=builder /app/dist /app
CMD ["npm", "start"]

The final image only has the compiled output, not build tools.

What Happens When You Run a Container

Understanding the process helps with debugging:

  1. Check for the image locally — If not found, pull from registry
  2. Create a new container — Docker allocates a unique ID and filesystem
  3. Set up networking — Assign an IP, handle port mappings
  4. Mount volumes — Connect storage
  5. Set up environment — Apply environment variables
  6. Start the process — Run the CMD
  7. Stream output — Show stdout/stderr

When you docker stop, the process gets SIGTERM (polite shutdown). If it doesn’t exit in 10 seconds, SIGKILL (forced).

Docker in Practice: A Real Stack

Here’s a realistic docker-compose setup for a web app:

version: '3.8'

services:
  web:
    build: .
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: production
      DATABASE_URL: postgres://app:password@db:5432/myapp
      REDIS_URL: redis://cache:6379
    depends_on:
      - db
      - cache
    volumes:
      - ./logs:/app/logs
    restart: unless-stopped

  db:
    image: postgres:15
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: password
      POSTGRES_DB: myapp
    volumes:
      - db_data:/var/lib/postgresql/data
    restart: unless-stopped

  cache:
    image: redis:7-alpine
    restart: unless-stopped

volumes:
  db_data:

Run with docker-compose up -d and everything starts. The web app can reach the database at postgres://db:5432 and Redis at redis://cache:6379.

Summary

Docker containers solve the “works on my machine” problem by packaging your entire environment. Key takeaways:

  • Images are blueprints; containers are running instances
  • Dockerfiles describe what goes into an image
  • docker-compose orchestrates multi-container applications
  • Volumes provide persistent storage
  • Networks allow container communication
  • Minimize image size, use multi-stage builds, avoid root user

With Docker, you ship your application once and it runs the same everywhere — dev, staging, production. That’s the power.