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
RUNexecutes during image build (e.g., installing packages)CMDexecutes 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:
- When you run
COPY . /app, Docker copies files at that moment - The image stores a snapshot of your code
- Changes to source files after building don't affect the image
- 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
-pflag to actually publish ports - RUN vs CMD - RUN during build, CMD when container starts
Best Practices Summary
Dockerfile Optimization
- Copy dependency files (package.json) before copying source code
- Run dependency installation before copying full application
- Place frequently changing layers (code) at the bottom
- Use
.dockerignoreto exclude unnecessary files - Combine multiple RUN commands to reduce layers
- Use specific base image versions (node:14) instead of :latest
Container Management
- Name your containers with
--namefor easier management - Use
-dflag to run containers in detached mode - Regularly clean up stopped containers with
docker container prune - Remove unused images with
docker image prune - Use
docker logsto 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