What is Docker?
Docker is a containerization platform that lets developers package applications and their dependencies into portable containers. In this beginner-friendly Docker tutorial, we'll cover Docker containers, images, Dockerfile, Docker Hub, networking, volumes, and Docker Compose, with practical examples along the way.
Difference Between Docker Containers and Virtual Machines
Talking about isolating, you might think why not use a VM? It lets us run completely different OS on top of our host OS similar to what docker does. But unlike VMs which are huge and take lots of time to boot up, docker containers are very fast, lightweight, scalable, portable, and efficient.
Image below shows how docker works compared to VMs:

Docker containers aren’t immune to kernel-level vulnerabilities though, every container uses the same kernel so that is one bad thing. Also, hypervisor 1 works on top of hardware whereas hypervisor 2 and docker work on top of host OS.
Why use Docker? When VM exists
- No need for full OS installation: Docker containers share host OS kernel which reduces lots of CPU, memory, and storage loads resulting in much lower resource consumption.
- Isolated Environment: Docker containers run in an isolated environment, which makes applications run efficiently and secured in a shared hardware resource.
- Image Caching: Each instruction in Dockerfile is built in a separate layer, so when two or more containers use the same base image, docker reuses the layer which avoids duplication and saves disk space.
- More application: Since docker containers are lightweight, share host kernel, and also because containers use only necessary resources more containers can be built, which means more applications can run on a single machine.
- Ideal for microservices: Images are lightweight(uses layer caching and runs on host OS), scalable(scales up and down on demand), isolated(isolates each container), and easy to deploy(consistent everywhere and portable) which makes them ideal for microservices.
Below is an image of the docker architecture:

Build and Run a Docker Image
Now that you know what docker is and how it is better than hypervisors, let's try building one...
Prerequisite:
- Docker Desktop
- Python3 (to run/test python file locally)
How to build a Docker Image:
- Create simple App: First let's create a simple application using the Flask framework which will show “Hello world!“on a webpage.
# flaskapi.py
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello World!"
if __name__ == "__main__":
app.run(host="0.0.0.0")How to Write a Dockerfile:
Dockerfile will give instructions to the docker on how to create the docker image. To create a “requirements.txt” file and add Flask==2.3.0 inside it to install Flask.
FROM python:3.8-slim
WORKDIR /app
COPY requirements.txt /app
RUN pip install -r requirements.txt
COPY flaskapi.py /app
EXPOSE 5000
CMD ["python3", "flaskapi.py"]FROM python:3.8-slim will install a lightweight python in a docker container, WORKDIR /app will create a new folder called app where we will store our files in container. COPY copies requirements.txt file to the app folder, then runs the command pip install -r requirements.txt to install Flask. Expose port 5000 so we can use it from outside. At last, CMD ["python3", "flaskapi.py"] is used to run our app using python.
How to Build a Docker image
Run ‘docker build’ to run docker build -t flask-api-image . is used to build a docker image which creates an isolated environment to run our application. The -t flag is used to name your image and . represents that we are building the current directory.
How to Run a Docker Container
To run a docker container, we need to use this command docker run -d -p 5000:5000 flask-api-image the -d flag runs the container in detach mode(in the background), -p 5000:5000 maps port 5000 of the container with port 5000 of the local machine. flask-api-image is the image name we created before.
- To Verify Docker Container is running:
docker psthis command will list all the running containers. - Access the flask API: Go to 'localhost:5000' on your web browser, you should see your "Hello World!" webpage ready.
How to Stop or Remove Docker Container
First get container ID using the command docker ps then
To stop docker container: docker stop <CONTAINER_ID>
To remove container: docker rm <CONTAINER_ID>
To remove an image: docker rmi <IMAGE_NAME>
How to Push Docker Images to Docker Hub
Docker Hub is a container registry where developers can store, share, and pull Docker images, exactly how GitHub works.
Push Docker Image to DockerHub
- Create an account in DockerHub
- Assuming you have built an image in Docker in your local machine. We need to tag the image before we push it to DockerHub.
- Check the list of images that are present with the command:
docker imagesIt should look something like this: - Tag the image in this format:
<DOCKERHUB USERNAME>/<REPOSITORY NAME>:<TAG>. Check username from DockerHub, give a repository name for your image, and give a version name in tag(e.g. v1, v2, latest) - Tag your image using this command:
docker tag <IMAGE_NAME> <NEW_NAME>. For me its:docker tag soumik10/flask-api-image:latest - When you use
docker imagesit will show two images now, but don't worry it creates a reference, not a duplicate. That's why both have the same CONTAINER_ID. - Log in to your DockerHub using
docker login(web-based login) ordocker login -u <DOCKERHUB_USERNAME> -p <DOCKERHUB_PASSWORK>to login through CLI. - On successful login use
docker push <IMAGE_NAME>:<TAG>to push your image to DockerHub. It should look something like this: On visiting Repositories tab on your DockerHub you should see your recently pushed docker image live.
To update an image in DockerHub, you can perform the same steps as before. If you want to keep multiple versions of the image in the same repository, change the <TAG> value (e.g. v1, v2, 3.1,..) every time you push the image.
Pull Docker Image from DockerHub
Now let's try pulling our published image from DockerHub.
Use the command docker pull <IMAGE_NAME>:<TAG> to pull the image from DockerHub. In my case it's docker pull soumik10/flaskpi:latest`
TIP: docker system prune will clear cache, stop containers, network not used, and build cache.
Docker Image vs Docker Container
A Docker image is a read-only template containing the application, dependencies, libraries, and configuration needed to create a container. A container is a running instance of that image.
The process works something like this:
Image → Docker Run → Container
| Docker Image | Docker Container |
|---|---|
| Read-only template | Running instance |
| Used to create containers | Runs the application |
| Can be stored in registries | Has a lifecycle |
| Built using Dockerfile | Created from an image |
Docker Network Explained
Docker network, volumes, and docker-compose are mostly beneficial for multi-container applications.
How docker network work?
There are different types of networks that we use in docker.
Default Bridge Network:
If no network is specified, docker uses this as default. It allows containers on the same host to communicate with each other but isolated from other hosts. So the docker connection goes DOCKER->HOST->ROUTER
User-Defined Bridge Network:
User defined bridge is used to isolate applications in a network. It's more flexible to work with than the default bridge. The command to create one is:docker network create <NETWORK_NAME>.
Host Network:
The container works as a host network as if you are running your application in your favorite IDE(e.g. VS Code). The good part is you don't have to expose any ports for it, as it runs in your localhost. To run a container in the host network the command is:docker run -itd --network host --name <CONTAINER_NAME> nginx (you can change nginx with the image you want to run)
MACVLAN Network:
This works on another level, so far host network has been allowing docker containers to run in localhost but now with MACVLAN you can run docker as a separate machine connected to your router. Your container will have its own MAC address and IP address. Command to create one is:docker network create -d macvlan --subnet <ROUTER_SUBNETMASK> --gateway <ROUTER_IP> -o parent=<HOST_MAC> You can check the network list using the command:docker network ls You can use a container shell using the command: docker exec -it <CONTAINER_NAME> sh
Docker Volume Explained
Well, docker volume is really simple. A container has its own filesystem, but we shouldn't rely on it to store important data. If the container is removed, the data stored inside the container will be removed with it. This is where volumes come in.
A volume lets us store data outside the container's own filesystem, so the data can survive even when the container is removed and recreated. We can mount a host directory to a directory inside the container using the -v flag:
docker run -v /home/mount/data:/var/lib/mysql/data <IMAGE_NAME>
Here, /home/mount/data is the directory on our host machine and /var/lib/mysql/data is where we want that data to be available inside the container.
Docker can also create and manage the storage for us. Instead of providing a host path, we can use a named volume: docker run -v mysql-data:/var/lib/mysql/data <IMAGE_NAME>
Here, mysql-data is a Docker-managed volume. Docker will create it if it doesn't already exist, and the data will remain available even if we remove and recreate the container.
What is docker compose?
WWell, obviously it's annoying to write commands for deploying multiple containers and managing their networks, volumes, and ports every time. This is where Docker Compose comes in.
With Docker Compose, we can define one or more containers and their configuration in a single .yaml file. We can define the images, ports, volumes, networks, environment variables, and other settings in one place and start everything with a single command.
Here is an example of a simple compose.yaml file:
services:
nginx:
image: nginx
ports:
- "80:80"
back-end:
image: api-flask
volumes:
- ./back-end/.env:/api-flask/.env
expose:
- "5000"Now instead of running multiple docker run commands manually, we can start the services using:
docker compose up
And when we are done, we can stop and remove the Compose application using:
docker compose down
This becomes really useful when our application has multiple services, like a frontend, backend, database, and Redis container. We can define everything in one file and manage the whole application together.
Conclusion
Docker has become an important part of modern application development and deployment. From building an application inside a container to managing multiple services with Docker Compose, it makes the whole process more consistent and easier to manage. No more hassle of "works on my machine".
We can package our application with everything it needs, run it in an isolated environment, and deploy it across different environments without worrying about whether it behaves differently. Of course, there is a lot more to Docker than what we covered here, but this should give you a good starting point to understand containers, images, Dockerfiles, Docker Hub, networks, volumes, and Docker Compose.
Now it's time to start building and see how Docker fits into your own projects.
Sources:
Frequently Asked Questions
Can Docker containers communicate with each other?↓
Yes, Docker containers can communicate with each other using a Docker network. When multiple containers are connected to the same user-defined network, they can communicate with each other without exposing every port to the host machine.
For example, if your backend and PostgreSQL database are running in separate containers, the backend can connect to the database through the Docker network instead of exposing the database port publicly.
This becomes especially useful when you're running applications with multiple services.
How do I expose a Docker container to localhost?↓
You can expose a container's port to your host machine using the -p flag with docker run.
For example:
docker run -p 5000:5000 flask-api-image
Here, the first 5000 is the port on your local machine and the second 5000 is the port inside the container. You can then access the application through localhost:5000.
It's also worth noting that EXPOSE in a Dockerfile does not actually publish the port. It mainly documents which port the application is expected to listen on. You still need -p or -P when you want to publish a container port to the host.
What happens to Docker container data when the container is deleted?↓
Data stored only inside a container's writable layer can be lost when the container is removed. This is why Docker volumes are important when you're working with persistent data such as databases.
A volume stores the data outside the container's lifecycle, so removing and recreating the container doesn't necessarily mean losing the data.
For example, you wouldn't want your PostgreSQL data to disappear just because you recreated the database container. Using a Docker volume keeps that data persistent.
Why use Docker Compose instead of running Docker containers manually?↓
You can run containers manually with docker run, but things become annoying when your application has multiple services.
Imagine having a frontend, backend, PostgreSQL database, and Redis container. You would need to remember the correct images, ports, volumes, networks, and environment variables for each service.
Docker Compose lets you define all of this in a YAML file and manage the whole application together. You can describe your services, networks, volumes, and configuration in one place and start the stack with a single command.
This is why Docker Compose is especially useful for local development and multi-container applications.
