Docker Explained: Images, Containers, Dockerfiles, Docker Hub, and Docker Compose
Why do we need Docker?
Imagine you’re a chef who has created the perfect cake recipe. You want your friends to bake the same cake, but each friend has a different kitchen, equipment, and ingredients.
As a result, the cake may not turn out exactly the way you intended.
Software developers face a very similar problem. An application that works perfectly on one machine may fail on another because of differences in:
- Operating systems
- Installed software
- Dependency versions
- Environment configurations
This often leads to the infamous phrase:
“It works on my machine.”
Docker solves this problem by packaging an application together with all of its dependencies and configurations into a portable unit called a container.
Instead of sharing only the code, developers can share the entire environment required to run that code.
Understanding Docker Through an Apartment Analogy
To understand Docker better, let’s compare Virtual Machines and Containers.
Virtual Machines: Think of Virtual Machines as separate apartments.
Each apartment has:
- Its own kitchen
- Its own living room
- Its own utilities
This provides strong isolation but consumes a lot of resources.
Containers: Containers are more like rooms in a shared apartment.
Each room is isolated, but everyone shares the same kitchen (the host operating system).
This makes containers:
- Lightweight
- Faster to start
- More resource-efficient
What is a Docker Image?
An image is like a snapshot of a virtual operating system (OS) that contains all the necessary files and configurations to run an application.
It includes:
- Application code
- Runtime
- Libraries
- Dependencies
- Configuration files
Think of an image as a recipe that can be used repeatedly to create identical containers.
💡Key Point: An image is read-only and serves as a Blueprint.
What is a Docker Container?
A container is a running instance of an image. It encapsulates the application and its dependencies, isolating it from the host system.
A container contains:
- The application
- Required dependencies
- Runtime environment
Because containers are isolated, they behave consistently across different systems.
💡Key Point: Container is a Running Application
Common Docker Commands
Starting and Stopping Containers
docker start <container_id>
docker stop <container_id>These commands start and stop existing containers.
Accessing a Running Container
docker exec -it <container_id> shExplanation:
execexecutes a command inside a running container.-ikeeps STDIN open.-tallocates a terminal.shOpens a shell session.
This allows you to inspect and interact with the container.
Understanding Port Mapping
Port mapping allows accessing services running inside a Docker container from the host system.
Get Atul Kumar’s stories in your inbox
Join Medium for free to get updates from this writer.
Example
docker run -it -p 8000:9000 my_web_appWhat Happens?
- Port 8000 on your machine
- Maps to port 9000 inside the container
When you visit:
http://localhost:8000Docker forwards the request to port 9000 inside the container.
Dockerfile: The Recipe for Your Container
Picture a Dockerfile as a recipe for building your container. Each instruction adds a new “ingredient” or step to the recipe, creating a layer in the final image. These layers act like building blocks, with subsequent layers depending on the ones below.
Docker Layers
Each Dockerfile instruction creates a separate layer.
Layer 5 Application Code
Layer 4 npm install
Layer 3 Copy Files
Layer 2 Install Node.js
Layer 1 Ubuntu Base ImageThese layers are stacked together to form the final image.
Benefits
- Faster builds
- Efficient storage
- Layer caching
When one layer changes, Docker rebuilds only that layer and the layers above it.
Dockerfile Example for a Node.js Application
#This instruction pulls the latest Ubuntu base image from the Docker Hub registry and creates a new layer.
FROM ubuntu:
RUN apt-get update:
RUN apt-get install -y curl:
RUN curl -sL https://deb.nodesource.com/setup_18.x | bash -:
RUN apt-get upgrade -y:
RUN apt-get install -y nodejs:
#Copies the files from the host machine to the current working directory inside the container, creating a new layer.
COPY package.json package.json:
COPY package-lock.json package-lock.json:
COPY main.js main.js:
#Installs the project dependencies defined in package.json inside the container.
RUN npm install:
#Specifies the command to run when the container starts.
ENTRYPOINT [ "node", "main.js" ]:Efficient Docker Caching
Think of caching as remembering parts of your recipe you’ve already made. By placing stable instructions (like installing system packages) earlier in your Dockerfile, Docker can “remember” them and skip rebuilding those steps if they haven’t changed. This makes building your container image much faster, like not having to re-chop vegetables every time you cook the same dish.
Efficient caching in Docker involves structuring your Dockerfile to prioritize stable, less frequently changing instructions first. By doing so, Docker can reuse cached layers for unchanged instructions during subsequent builds, minimizing redundant execution and speeding up the build process.
Ordering Instructions:
- Prioritize less frequently changing instructions before those that change more often.
- Commands like package installations (
apt-get install) should come before copying application files or runningnpm install. - In the provided Dockerfile, instructions like
FROM,RUN apt-get update, andRUN apt-get installare less likely to change frequently and benefit from caching.
Leveraging Cache:
- Docker caches each instruction’s result as a separate layer.
- Properly ordered instructions allow Docker to reuse cached layers for unchanged instructions, avoiding redundant execution during subsequent builds.
Here’s an optimized version of the Dockerfile we made earlier:
FROM ubuntu
# Install system dependencies
RUN apt-get update \
&& apt-get install -y \
curl \
nodejs \
&& curl -sL https://deb.nodesource.com/setup_18.x | bash - \
&& apt-get upgrade -y \
&& rm -rf /var/lib/apt/lists/*
# Copy application files
COPY package.json package-lock.json main.js /app/
# Install application dependencies
RUN npm install
# Set entry point
ENTRYPOINT [ "node", "/app/main.js" ]Publishing Image to Docker Hub
Docker Hub is a cloud registry used to store and share Docker images.
Creating a Repository on Docker Hub
- Go to the Docker Hub website and log in to your account.
- Click on the “Create Repository” button.
- Choose a name for your repository and configure any other settings as needed.
- Click on the “Create” button to create the repository.
Building an Image with Username/Reponame
- First, ensure that you’re logged in to Docker Hub on your local machine using the
docker logincommand:
docker login2. Then, navigate to the directory containing your Dockerfile and build your image using the docker build command:
#Replace username with your Docker Hub username and reponame with the name of the repository
docker build -t username/reponame.Pushing Image to Docker Hub
- Once the image is built, you can push it to Docker Hub using the docker push command:
# Again, replace username with your Docker Hub username and reponame with the name of the repository.
docker push username/reponameDocker Compose
Docker Compose simplifies the process of managing multiple Docker applications by allowing you to define all the necessary components in a single YAML file. This file typically includes information such as which Docker images to use for each service, the networking configuration between containers, and any volumes required for data persistence.
For example, let’s say you have a web application that consists of a frontend service using a Node.js image and a backend service using a Python image. With Docker Compose, you can specify these services along with their dependencies, environment variables, and any other configurations in a docker-compose.yml file. Then, with a single command (docker-compose up), Docker Compose will create and start all the necessary containers based on your specifications.
Example Docker Compose File
version: '3'services: frontend:
build: ./frontend ports:
- "3000:3000" depends_on:
- backend environment:
- NODE_ENV=production
- API_URL=http://backend:5000 backend:
image: postgres:latest ports:
- "5432:5432" environment:
- POSTGRES_USER=myuser
- POSTGRES_PASSWORD=mypassword
- POSTGRES_DB=mydatabase
Running Docker Compose
Start all services:
docker compose upRun in detached mode:
docker compose up -dStop all services:
docker compose downWith a single command, Docker Compose creates networks, starts containers, and manages communication between services.
📝Key Takeaways
✅ Docker eliminates the “works on my machine” problem.
✅ Images act as templates for containers.
✅ Containers provide isolated and consistent environments.
✅ Dockerfiles define how images are built.
✅ Layer caching makes image builds faster.
✅ Docker Hub allows sharing images worldwide.
✅ Docker Compose simplifies multi-container applications.
Docker has become one of the most important tools in modern software development. Whether you’re building web applications, deploying microservices, or working with AI projects, learning Docker is a skill that will save countless hours and make your applications more portable, scalable, and reliable.
If you found this article helpful, consider following me for more such content
Connect with me on LinkedIn: https://www.linkedin.com/in/atulkumar8/










