Why Docker?
Docker is a tool that packages the application we develop along with the dependencies it needs. Why do we package it like this? Software needs dependent libraries, tools, settings, and runtimes. To run the software on another machine, we need the same versions of the dependencies as those used when we create it.
**Overall Concepts to Know while working with Docker
Dockerfile -> Docker image -> Container**
A Dockerfile is a saved, shareable instruction file. We use the Docker CLI to send this file to the Docker Daemon. The Daemon reads the instructions and builds a Docker Image (the packaged software/template). We can then use the CLI to tell the Daemon to run multiple Containers (the live applications) from that single Image.
If we need to start many containers and the containers have to communicate with each other for many practical uses. To define containers with details to start, stop, and manage the container lifecycle ( Orchestration ) and communicate with each other ( Networking ), we use a docker-compose file.
VM vs Docker
Each virtual machine displayed above acts like a completely different computer inside real computers.
Virtual machine architecture starts with the hypervisor, which takes part( abstracts and allocates, or we can call it partitions) of the machine's hardware and prepares virtual hardware. A guest OS runs inside each VM to run applications.
Unlike VMs, Docker containers don’t need a heavy Guest OS. Instead, they share the Host OS Kernel. By using Linux features like Namespaces, Docker creates an isolated environment (specific folders, networks, and files) where the process thinks it is running alone. Because Linux containers require a Linux kernel, running them on Windows or Mac requires Docker Desktop to spin up a single, lightweight Linux VM in the background, and all containers share that VM's kernel.
When we start a container, the Docker daemon starts a standard process but wraps it in two Linux features: Namespaces to isolate what the process can see, and cgroups to limit the CPU and RAM it can use. Because of this namespace isolation, the process has two identities: a standard PID assigned by the host OS, and an isolated PID (usually PID 1) inside the container's own namespace.
Commands
1. docker run –– name container1 ubuntu
The docker run ubuntu command tells Docker to run the latest version of the Ubuntu image. Docker first searches its local image cache on your machine. If the image is not found there, it reaches out to a Docker Registry (like Docker Hub), downloads the image to your cache, and then runs it as a container. If we didn’t mention its name by flag (–name < our name>), Docker assigns some random name to the container.
docker run –– name container1 -d ubuntu
If we start the container, the terminal is active, and logs are printed from the container. To use the same terminal for various uses, use the detach flag.
docker run –– name container1 ubuntu:4.0
We can specify a version instead of the latest version.
docker run –– name container1 -p 80:5000 ubuntu:4.0
We map host port 80 to the container’s app port 5000. From the outside world, we can communicate through the host port 80.
docker run –– name my-db -v /opt/datadir:/var/lib/mysql mysql
Containers are temporary and disposable. So if we store the DB inside the container, it is not recoverable. We can map the host folder to the container folder.
**
Container Folder = /var/lib/mysql
Host Folder = /opt/datadir**
docker run -e APP_COLOUR=blue kodekloud/simple-webapp
It is an environment variable, APP_COLOUR; we can use it in our container
docker ps
We can see all the active containers.
**
docker ps -a**
We can see all the containers, including inactive ones
docker start container1
Start the container
docker stop container1
Stops the container
docker rm container1
Deletes the container
docker images
Lists all the images
docker rmi nginx
Removes the image
docker pull ubuntu
Just pulls the image from Docker Hub, without running it.
docker exec -it container1 sh
It opens the container ubuntu, execute the command sh.
-i (Interactive): Keeps the input stream (STDIN) open so you can type.
-t (TTY): Allocates a fake terminal screen so it looks and acts like a normal command prompt. Together (-it), they give you an interactive shell.
docker inspect container1
Details about the container
docker logs container1
Prints out what the application has printed.
Example Dockerfile
FROM ubuntu:latest
# Update OS and install Python
RUN apt-get update && apt-get install -y python3 python3-pip
# Install the exact Flask version needed
RUN pip3 install Flask==2.0.1
# Set the folder and copy your laptop's code into the image
WORKDIR /opt
COPY . /opt
# The command to run when the container starts
CMD ["python3", "-m", "flask", "run", "--host=0.0.0.0"]
The Dockerfile instructs the Docker daemon to start from the Ubuntu base image, and within that environment, we use RUN to install packages. We use WORKDIR to tell Docker to create a folder at /opt/home and execute all subsequent commands from that directory. We then copy the code from our host directory directly into that workspace. Finally, when we start the container, it executes the command provided inside the CMD array.
CMD is a flexible suggestion; the user can override CMD, as explained in the image.
docker run my-app bash
Here, when running the container, by default it has to run python3 -m flask –host=0.0.0.0, but we override it with bash. When the container is started, it won’t start the Flask app; it will start bash. So we can use an ENTRYPOINT array, which prevents the command from being overridden. We can use a combination of them to our advantage.
# Lock in the main program
ENTRYPOINT ["python3", "app.py"]
# Provide a default argument
CMD ["--port=8080"]
In the above example, the runtime command is fixed as python3 app.py, but the port can be overridden.
Docker has a layered architecture; instructions that add or change files (RUN, COPY, ADD) create actual filesystem layers. Commands like ENTRYPOINT, CMD, or ENV just add lightweight metadata to the configuration, not physical layers
Layer 1 (Base Ubuntu Layer): The heavy foundation.
Layers 2 & 3 (apt and pip packages): The installed tools stacked on top.
Layer 4 (Source Code): Your actual application files.
The metadata telling Docker how to behave (Entrypoint): The final startup instructions.
Once the image is built, the underlying layers of the image are already created and read-only. The Docker daemon has a cache of every layer and can reuse it when rebuilding the image. For example, if you alter the source code ( 4th layer ), Docker won’t start rebuilding from scratch from Layer1. It will start from the 4th; it can recognise that the top 3 layers are unchanged, and it has the cache of the layers to reuse. So it will build layer 4 and the metadata step.
On top of this, there is a temporary, read-and-write container layer above all read-only stable image layers. It is like placing a glass board on top of the book; we can scribble anything on top of the glass board. It creates an illusion of changing the book, but in reality, the book is static. If the container is deleted, this layer’s memory is lost. This is the reason it is termed temporary. It uses Copy-on-Write to create an illusion of changing files in the container layer. Please research it separately if you want more detail, but this abstraction is enough to work with Docker.
To save permanently on the host file system, use volumes, which were discussed above.
docker run -d -v ./Downloads/db:/var/lib/mysql mysql
If we are building a simple API for an online store, we create two microservices: a database and a backend process containing the business logic. If a customer requests to see products, the backend queries the database and returns the result to the client. Without Docker, these processes use a socket address (IP + PORT) to establish a connection. In Docker, we use a network abstraction.
By default, the Docker daemon acts as a router/gateway. It assigns a dynamic IP to each container and places them in the default bridge network. These dynamic IPs can be used for communication, but they change every time a container restarts.
To overcome this, the Docker daemon has an embedded DNS that resolves container names into IPs, allowing containers to communicate using just their names. However, this DNS feature does not work on the default bridge network. To use it, we must create a user-defined network and place both of our containers inside it.
We can bind a port on the host machine to the process inside the container. This is called port binding. It opens a doorway so the external world can communicate with the container, allowing users to access the API or web server running inside it.
docker network create my-store-network
# Start the database and put it on the network
docker run -d --name my-database --network my-store-network mysql
# Start the web server and put it on the exact same network
docker run -d --name my-web-server --network my-store-network my-python-app
OLD WAY without Docker
# The web app connects to the database installed directly on your laptop
db_connection = mysql.connect(
host="localhost",
user="admin",
password="my-secret-pw",
port=3306
)
Docker way
# The web app connects to the separate database container
db_connection = mysql.connect(
host="my-database", # <--- Notice this change!
user="admin",
password="my-secret-pw",
port=3306
)
We have already discussed the volume above
#Create volume
docker volume create data_volume
# Use the volume
docker run -v data_volume:/var/lib/mysql mysql
Data_volume is a permanent folder on the host. /var/lib/mysql is a folder in the container.
There are two main ways to persist data in Docker. Volumes are entirely managed by the Docker daemon and stored in a hidden area on the host. Bind Mounts allow you to map a specific, known folder on your host machine directly into the container(-v /opt/datadir:/var/lib/mysql). With a bind mount, Docker doesn't copy or sync files; it simply allows the container to read and write directly to that exact host folder in real-time. The modern way to attach either of these in the CLI is by using the explicit --mount flag.
Docker compose
In the example image, we need 5 containers. A Python app for people to vote and update an in-memory Redis DB; from this in-memory DB, the .NET worker gets the data and updates the actual Postgres DB. The Postgres app was fetched to show the result in the Node.js result app. This single file helps to orchestrate multiple containers.
services:
vote:
image: dockersamples/examplevotingapp_vote
ports:
- "5000:80"
networks:
- front1
redis:
image: redis:alpine
networks:
- front1
- back1
worker:
image: dockersamples/examplevotingapp_worker
networks:
- back1
- back2
db:
image: postgres:15-alpine
environment:
- POSTGRES_PASSWORD=postgres
volumes:
- db-data:/var/lib/postgresql/data
networks:
- back2
- front2
result:
image: dockersamples/examplevotingapp_result
ports:
- "5001:80"
networks:
- front2
volumes:
db-data:
networks:
front1:
front2:
back1:
back2:
These are some commands for a Dockerfile
docker compose up -d
#Start all the containers without a live terminal
docker compose ps
#List of all containers
docker compose logs -f vote
#Logs of the container from the vote app
docker compose down
#Stop all the containers
docker compose up -d --build
#Rebuild all the containers
Docker Hub is a popular public container registry used widely for open-source images. For enterprise security, organizations usually host their own private registries (like AWS ECR or a self-hosted Docker Registry). We push our built images to these registries, and our servers pull those images to run them as containers.
docker login registry.mycompany.com
docker tag my-api:latest username/my-api:v1.0
docker push username/my-api:v1.0
docker pull username/my-api:v1.0



Top comments (0)