Wednesday, January 21, 2026

Docker Images and Containers

Docker Images and Containers: A Complete Deep Dive | RJS Expert

Docker Images and Containers: A Complete Deep Dive

Written by: RJS Expert

This guide builds upon the Docker Introduction and explores the relationship between images and containers, how to build custom images, and optimize your Docker workflow.

Images vs Containers: The Core Relationship

When working with Docker, understanding the distinction between images and containers is fundamental to mastering containerization.

What Are Docker Images?

Images are templates, blueprints for containers.

An image contains:

  • The application code
  • The required tools to execute the code
  • All dependencies and libraries
  • Environment configuration
  • Setup instructions

Key Point: Images are read-only and shareable. You create them once and can use them to run multiple containers.

What Are Docker Containers?

Containers are running instances of images.

A container is:

  • The concrete running application
  • Based on an image
  • Isolated from other containers
  • Can be started, stopped, and removed
  • Has its own filesystem and network

Analogy: If an image is a class in programming, a container is an instance of that class. You can create multiple containers (instances) from a single image (class).

The Relationship

Images Containers
Templates/Blueprints Running instances
Read-only Read-write layer on top
Created once, reused many times Can create multiple containers from one image
Contains code and environment Executes the code

We run containers, which are based on images.

Working with Pre-Built Images

Before building custom images, let's understand how to use existing images from Docker Hub.

Docker Hub: The Image Registry

Docker Hub (hub.docker.com) hosts thousands of pre-built images:

  • Official images (Node.js, Python, Nginx, PostgreSQL, etc.)
  • Community-maintained images
  • Your own custom images

Pulling and Running an Image

Example: Running the official Node.js image

# Pull and run Node.js image
docker run node

# Run with interactive terminal
docker run -it node

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

Important: By default, containers are isolated. Even if a process inside the container exposes a port or interface, it's not automatically available to the host machine.

Understanding Container Isolation

When you run docker run node, the container starts and immediately exits because:

  • No interactive session is exposed
  • The container ran its default command and finished
  • Containers run in isolation from the host

To interact with the container, use the -it flag:

# -i = interactive (keep STDIN open)
# -t = tty (allocate pseudo-terminal)
docker run -it node

# Now you can run Node commands
> 1 + 1
2
> console.log("Hello from container!")
Hello from container!

Building Custom Images

Most real-world scenarios require building custom images with your application code.

The Dockerfile

A Dockerfile contains instructions for building an image. It's a plain text file (no extension) that Docker reads to create your custom image.

Sample Application Structure

my-node-app/
├── server.js           # Application code
├── package.json        # Dependencies
├── public/
│   └── styles.css      # Static files
└── Dockerfile          # Image instructions

Creating Your First Dockerfile

# Use Node.js as base image
FROM node

# Set working directory
WORKDIR /app

# Copy package.json first (optimization)
COPY package.json /app

# Install dependencies
RUN npm install

# Copy application code
COPY . /app

# Document exposed port
EXPOSE 80

# Command to run when container starts
CMD ["node", "server.js"]

Understanding Dockerfile Instructions

Instruction Purpose When Executed
FROM Specifies base image During image build
WORKDIR Sets working directory inside container During image build
COPY Copies files from host to image During image build
RUN Executes commands during build During image build
EXPOSE Documents which port container uses Documentation only
CMD Default command to run When container starts

Critical Difference: RUN vs CMD

  • RUN executes during image build (e.g., installing packages)
  • CMD executes when container starts (e.g., starting your application)

Building the Image

# Build image from Dockerfile in current directory
docker build .

# Output shows each step
Step 1/7 : FROM node
Step 2/7 : WORKDIR /app
Step 3/7 : COPY package.json /app
Step 4/7 : RUN npm install
Step 5/7 : COPY . /app
Step 6/7 : EXPOSE 80
Step 7/7 : CMD ["node", "server.js"]
Successfully built abc123def456

Running Your Custom Container

# Run container from image ID
docker run abc123def456

# Won't work yet! Need to publish ports...

Port Publishing: Exposing Container Ports

Even though we added EXPOSE 80 in the Dockerfile, the container port is still not accessible from the host.

Why EXPOSE Alone Isn't Enough

The EXPOSE instruction is documentation only. It tells users which port the container uses, but doesn't actually publish it.

Publishing Ports with -p Flag

# -p HOST_PORT:CONTAINER_PORT
docker run -p 3000:80 abc123def456

# Now accessible at localhost:3000
# Host port 3000 → Container port 80

Port Mapping Explained:

  • 3000 (left side) = Port on your host machine
  • 80 (right side) = Port inside the container
  • You can map to any available host port
  • Multiple containers can use the same container port (80) as long as host ports differ

Managing Containers

Essential Container Commands

# List running containers
docker ps

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

# Stop a container
docker stop CONTAINER_NAME

# Start a stopped container
docker start CONTAINER_NAME

# Remove a container
docker rm CONTAINER_NAME

# Remove an image
docker rmi IMAGE_ID

# View container logs
docker logs CONTAINER_NAME

Using Short IDs

You don't need to type the full container or image ID. Docker accepts unique prefixes:

# Full ID
docker run abc123def456

# Short ID (first few characters)
docker run abc

# If unique, even single character works
docker run a

Images Are Immutable: The Snapshot Concept

This is one of the most important concepts to understand about Docker images.

What Happens When You Change Code

Let's say you modify your source code after building an image:

// server.js - Original
<h1>My Course Goal</h1>

// server.js - Modified
<h1>My Course Goal!</h1>  // Added exclamation mark

If you restart the container, the change won't appear.

Why Code Changes Don't Appear

Understanding the Snapshot:

  1. When you run COPY . /app, Docker copies files at that moment
  2. The image stores a snapshot of your code
  3. Changes to source files after building don't affect the image
  4. The image is read-only and locked

Solution: Rebuild the Image

# Rebuild to pick up code changes
docker build .

# New image ID is generated
Successfully built xyz789abc012

# Run container with new image
docker run -p 3000:80 xyz789abc012

Key Takeaway: Images are templates that are finalized when built. To update the code in an image, you must rebuild it.

Layer-Based Architecture: Understanding Caching

Docker images use a layer-based architecture to optimize build performance.

How Layers Work

Each instruction in a Dockerfile creates a layer:

FROM node           # Layer 1
WORKDIR /app        # Layer 2
COPY package.json   # Layer 3
RUN npm install     # Layer 4
COPY . /app         # Layer 5
EXPOSE 80           # Layer 6 (metadata)
CMD ["node"...]     # Layer 7

Caching Behavior

How Docker Uses Cache:

  • Each layer result is cached
  • If nothing changed, Docker uses the cached layer
  • If a layer changes, that layer and all subsequent layers are rebuilt
  • Cache dramatically speeds up rebuilds

Rebuilding Without Code Changes

# First build - all layers executed
docker build .

# Second build (no changes) - uses cache
docker build .

Step 1/7 : FROM node
 ---> Using cache
Step 2/7 : WORKDIR /app
 ---> Using cache
Step 3/7 : COPY package.json
 ---> Using cache
Step 4/7 : RUN npm install
 ---> Using cache
...
Successfully built (almost instant!)

When Code Changes

# Modified server.js, rebuild
docker build .

Step 1/7 : FROM node
 ---> Using cache
Step 2/7 : WORKDIR /app
 ---> Using cache
Step 3/7 : COPY package.json
 ---> Using cache
Step 4/7 : RUN npm install
 ---> Using cache
Step 5/7 : COPY . /app
 ---> abc123def  # NEW - detects file change
Step 6/7 : EXPOSE 80
 ---> xyz456abc
Step 7/7 : CMD ["node", "server.js"]
 ---> hij789klm

Notice: Layers 1-4 used cache, but layer 5 onwards were rebuilt.

Optimizing Dockerfile Layer Order

The order of instructions matters significantly for build performance.

Unoptimized Dockerfile

FROM node
WORKDIR /app
COPY . /app              # Copies ALL files
RUN npm install          # Runs every time code changes
CMD ["node", "server.js"]

Problem: Any code change invalidates the COPY layer, which means npm install runs again unnecessarily.

Optimized Dockerfile

FROM node
WORKDIR /app
COPY package.json /app   # Copy dependencies first
RUN npm install          # Install dependencies
COPY . /app              # Copy source code last
CMD ["node", "server.js"]

Benefit: Source code changes don't trigger npm install again unless package.json changes.

Optimization Strategy:

  • Place stable instructions (rarely change) at the top
  • Place frequently changing instructions (code) at the bottom
  • Separate dependency installation from code copying
  • This maximizes cache utilization

Impact Example

Scenario Unoptimized Optimized
First build 60 seconds 60 seconds
Rebuild (code change) 55 seconds (npm install again) 5 seconds (cache used)
Rebuild (dependency change) 55 seconds 55 seconds

Complete Workflow Example

Let's put everything together with a complete workflow:

1. Create Application Files

// server.js
const express = require('express');
const app = express();

app.get('/', (req, res) => {
  res.send('<h1>Hello from Docker!</h1>');
});

app.listen(80, () => {
  console.log('Server running on port 80');
});
// package.json
{
  "name": "docker-demo",
  "version": "1.0.0",
  "dependencies": {
    "express": "^4.18.0"
  }
}

2. Create Optimized Dockerfile

FROM node
WORKDIR /app
COPY package.json /app
RUN npm install
COPY . /app
EXPOSE 80
CMD ["node", "server.js"]

3. Build and Run

# Build the image
docker build -t my-node-app .

# Run the container
docker run -p 3000:80 my-node-app

# Access at http://localhost:3000

4. Make Code Changes

// Update server.js
res.send('<h1>Hello from Docker - Updated!</h1>');

# Rebuild (fast - uses cache for npm install)
docker build -t my-node-app .

# Stop old container
docker stop CONTAINER_NAME

# Run new container
docker run -p 3000:80 my-node-app

Key Takeaways

Essential Concepts

  • Images are templates - Read-only blueprints containing code and environment
  • Containers are instances - Running applications based on images
  • Images are immutable - Must rebuild to incorporate changes
  • Layers enable caching - Each instruction creates a cached layer
  • Order matters - Place stable instructions first to maximize cache usage
  • EXPOSE is documentation - Use -p flag to actually publish ports
  • RUN vs CMD - RUN during build, CMD when container starts

Best Practices Summary

Dockerfile Optimization

  1. Copy dependency files (package.json) before copying source code
  2. Run dependency installation before copying full application
  3. Place frequently changing layers (code) at the bottom
  4. Use .dockerignore to exclude unnecessary files
  5. Combine multiple RUN commands to reduce layers
  6. Use specific base image versions (node:14) instead of :latest

Container Management

  • Name your containers with --name for easier management
  • Use -d flag to run containers in detached mode
  • Regularly clean up stopped containers with docker container prune
  • Remove unused images with docker image prune
  • Use docker logs to troubleshoot container issues

What's Next?

Now that you understand images and containers deeply, you can explore:

  • Data Persistence: Docker volumes and bind mounts
  • Networking: Container communication and networks
  • Multi-Container Apps: Docker Compose
  • Environment Variables: Configuration management
  • Multi-Stage Builds: Advanced image optimization
  • Container Orchestration: Kubernetes for production

No comments:

Post a Comment