You write an app. It works on your computer. You send it to a friend. It does not work on their computer. They have a different operating system, a different version of Python, or missing libraries.
This is the classic “it works on my machine” problem. Docker solves it.
What Is Docker?
Docker is a tool that packages your application and everything it needs into a container. A container includes your code, libraries, system tools, and settings. It runs the same way everywhere — on your laptop, your colleague’s laptop, or a server in the cloud.
Think of a container as a lightweight, isolated box. Your app runs inside this box. The box has everything the app needs. Nothing from the outside leaks in, and nothing from the inside leaks out.
Containers vs Virtual Machines
You might have heard of virtual machines (VMs). They also isolate applications. But containers and VMs work differently.
A virtual machine runs a full operating system on top of your existing OS. It needs its own kernel, its own file system, its own everything. This makes VMs heavy — they use a lot of memory and take minutes to start.
A container shares the host operating system kernel. It only packages the application and its dependencies. This makes containers lightweight — they use less memory and start in seconds.
Virtual Machine Container
┌──────────────────┐ ┌──────────────────┐
│ App A │ │ App A │
│ Libraries A │ │ Libraries A │
│ Guest OS │ └──────────────────┘
└──────────────────┘ ┌──────────────────┐
┌──────────────────┐ │ App B │
│ App B │ │ Libraries B │
│ Libraries B │ └──────────────────┘
│ Guest OS │ │
└──────────────────┘ ┌───────▼──────────┐
│ │ Container Engine │
┌────────▼─────────┐ │ (Docker) │
│ Hypervisor │ └──────────────────┘
└──────────────────┘ │
│ ┌───────▼──────────┐
┌────────▼─────────┐ │ Host OS │
│ Host OS │ └──────────────────┘
└──────────────────┘
Here is a quick comparison:
| Feature | Virtual Machine | Container |
|---|---|---|
| Start time | Minutes | Seconds |
| Size | Gigabytes | Megabytes |
| Performance | Slower (extra OS layer) | Near-native |
| Isolation | Full (own kernel) | Process-level (shared kernel) |
| Use case | Running different OS types | Running applications |
For most development tasks, containers are the better choice. They are fast, lightweight, and easy to share.
Installing Docker
Docker runs on macOS, Linux, and Windows. The easiest way to install it is with Docker Desktop.
macOS
- Go to docker.com/products/docker-desktop
- Download Docker Desktop for Mac
- Open the
.dmgfile and drag Docker to Applications - Open Docker from Applications
Linux
On Ubuntu or Debian:
# Update package list
sudo apt update
# Install Docker
sudo apt install docker.io
# Start Docker
sudo systemctl start docker
# Add your user to the docker group (so you don't need sudo)
sudo usermod -aG docker $USER
After adding yourself to the docker group, log out and log back in.
Windows
- Go to docker.com/products/docker-desktop
- Download Docker Desktop for Windows
- Run the installer
- Restart your computer if prompted
Verify the Installation
Open a terminal and run:
docker --version
You should see something like:
Docker version 27.5.1, build 9f9e405
If you see a version number, Docker is installed and ready.
Your First Container
Let’s run a container. Docker provides a test image called hello-world. Run this command:
docker run hello-world
You should see output like this:
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
e6590344b1a5: Pull complete
Digest: sha256:...
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working correctly.
...
What happened here? Let’s break it down:
- Docker looked for an image called
hello-worldon your computer - It did not find it, so it pulled (downloaded) it from Docker Hub
- Docker created a container from this image
- The container ran, printed a message, and stopped
That is the core Docker workflow: pull an image, create a container, run it.
Running an Interactive Container
The hello-world container prints a message and exits. Let’s try something more interesting. Let’s run Ubuntu inside a container:
docker run -it ubuntu bash
This starts a new container with Ubuntu Linux and opens a bash shell inside it. The -it flag means:
-i— interactive (keep the input stream open)-t— allocate a terminal (so you get a proper shell prompt)
You should see a prompt like:
root@a1b2c3d4e5f6:/#
You are now inside an Ubuntu container. Try some commands:
# Check the OS
cat /etc/os-release
# List files
ls /
# Install a package
apt update && apt install -y curl
# Exit the container
exit
When you type exit, the container stops. Everything you installed inside it is gone. Containers are temporary by default. We will learn how to keep data later in this series.
Key Concepts: Images, Containers, and Registries
These three terms come up all the time in Docker. Let’s define them clearly.
Image
An image is a template. It contains the operating system, your application, libraries, and configuration. You do not run an image directly. You create a container from it.
Think of an image like a recipe. The recipe itself is not food — but you use it to make food.
Container
A container is a running instance of an image. When you run docker run ubuntu, Docker creates a container from the ubuntu image. You can create many containers from the same image.
Each container is isolated. If you install something in container A, container B does not have it.
Registry
A registry is a storage place for images. The most popular registry is Docker Hub. When you run docker run ubuntu, Docker pulls the image from Docker Hub.
Registry (Docker Hub) Your Computer
┌──────────────────┐ ┌──────────────────────────────┐
│ ubuntu │ │ Image: ubuntu │
│ python │──► │ │
│ node │ │ Container 1 (from ubuntu) │
│ postgres │ │ Container 2 (from ubuntu) │
│ ... │ │ Container 3 (from python) │
└──────────────────┘ └──────────────────────────────┘
Managing Containers
List Running Containers
docker ps
This shows containers that are currently running. If you just ran hello-world, it already stopped, so docker ps shows nothing.
To see all containers (including stopped ones):
docker ps -a
Output looks like this:
CONTAINER ID IMAGE COMMAND STATUS NAMES
a1b2c3d4e5f6 ubuntu "bash" Exited (0) 5 min ago eager_turing
f7g8h9i0j1k2 hello-world "/hello" Exited (0) 10 min ago happy_lovelace
Each container has an ID, the image it was created from, its status, and a random name.
Stop a Running Container
If a container is running in the background, you can stop it:
docker stop a1b2c3d4e5f6
You can use the container ID or the container name:
docker stop eager_turing
Remove a Container
Stopped containers still take up disk space. To remove one:
docker rm a1b2c3d4e5f6
To remove all stopped containers at once:
docker container prune
Docker asks for confirmation. Type y to proceed.
Run and Auto-Remove
If you want a container to be removed automatically when it stops, use the --rm flag:
docker run --rm -it ubuntu bash
This is useful for quick experiments. When you exit the shell, the container is deleted.
Managing Images
List Downloaded Images
docker images
Output:
REPOSITORY TAG IMAGE ID SIZE
ubuntu latest 5a81c4b8502e 77.9MB
hello-world latest ee301c921b8a 9.14kB
Remove an Image
If you no longer need an image:
docker rmi hello-world
You must remove all containers using that image first. Or use the force flag:
docker rmi -f hello-world
Clean Up Everything
Over time, Docker accumulates unused images, stopped containers, and dangling networks. To clean up:
docker system prune
This removes stopped containers, unused networks, and dangling images. Add -a to also remove all unused images:
docker system prune -a
Running a Container with a Name
By default, Docker gives containers random names like eager_turing or happy_lovelace. You can choose your own name with the --name flag:
docker run -it --name my-ubuntu ubuntu bash
Names make it easier to manage containers:
docker stop my-ubuntu
docker start my-ubuntu
docker rm my-ubuntu
Without a name, you need to use the container ID (like a1b2c3d4e5f6) — harder to remember and type.
Environment Variables
Containers often need configuration. The -e flag passes environment variables:
docker run -e MY_NAME=Alex -e MY_APP=demo ubuntu bash -c "echo Hello $MY_NAME from $MY_APP"
This is how you pass database URLs, API keys, and other settings to containers. We will use environment variables heavily in the Docker Compose tutorial.
You can also pass multiple variables from a file:
docker run --env-file .env ubuntu bash
The .env file contains key-value pairs, one per line:
DATABASE_URL=postgres://localhost:5432/mydb
API_KEY=abc123
NODE_ENV=production
Running a Container with Port Mapping
Most real applications serve web traffic. Let’s run the Nginx web server:
docker run -d -p 8080:80 --name my-web nginx
Let’s break down the flags:
-d— run in the background (detached mode)-p 8080:80— map port 8080 on your computer to port 80 in the container--name my-web— give the container the name “my-web”nginx— use the Nginx image
Now open your browser and go to http://localhost:8080. You should see the Nginx welcome page.
To stop and remove this container:
docker stop my-web
docker rm my-web
Executing Commands in a Running Container
Sometimes you need to look inside a container that is already running. Use docker exec:
# Open a shell inside a running container
docker exec -it my-web bash
# Run a single command
docker exec my-web cat /etc/nginx/nginx.conf
# Check which processes are running
docker exec my-web ps aux
The -it flags work the same as with docker run — they give you an interactive terminal. This is useful for debugging issues inside a running container without stopping it.
Viewing Container Logs
Containers write their output to stdout and stderr. Docker captures this output and lets you view it:
# View all logs
docker logs my-web
# Follow logs in real-time (like tail -f)
docker logs -f my-web
# Show only the last 20 lines
docker logs --tail 20 my-web
Logs are essential for debugging. If a container starts and immediately stops, check the logs to see what went wrong.
Common Mistakes
1. Forgetting that containers are temporary. When a container stops, any files you created inside it are gone. If you need to keep data, you need volumes. We cover them in Docker Tutorial #4.
2. Not cleaning up old containers and images. After a few weeks of experimenting, you might have dozens of stopped containers and unused images taking up gigabytes of space. Run docker system prune regularly.
3. Using docker run when you meant docker start. The command docker run creates a new container every time. If you want to start a stopped container, use docker start <container>. Check docker ps -a to see existing containers.
What We Learned
In this tutorial, you learned:
- Docker packages applications into lightweight, portable containers
- Containers share the host OS kernel — they are faster and smaller than VMs
- An image is a template, a container is a running instance, and a registry stores images
docker runcreates and starts a containerdocker pslists running containersdocker stopanddocker rmmanage container lifecycledocker imageslists downloaded imagesdocker system prunecleans up unused resources
Related Articles
- Docker Cheat Sheet — quick reference for all Docker commands
What’s Next?
In the next tutorial, we will learn how to write a Dockerfile — a recipe for building your own custom images. Instead of using pre-built images like ubuntu or nginx, you will package your own application into a Docker image.