Before diving in, a recommendation: if you prefer to learn by video, the resource that made Docker click for me was this 70-minute Docker tutorial by TechWorld with Nana. This article is my written explanation of the key concepts covered in that video.
What is Docker?
Docker makes developing and deploying applications easier by packaging not just code and dependencies, but also configuration, system tools, and the runtime environment into a standardised unit called a container. That container can then be shared and run consistently on any machine, regardless of what operating system or software is already installed on it.
Without Docker, setting up a development environment or deploying an application to a server involves a series of manual installation steps that can vary between operating systems, produce different results depending on what is already installed, and introduce human error at every stage. Docker removes most of that complexity.
Problems Docker solves in development
Imagine a team of developers working on an application that depends on several services: a web server, a database, and a caching layer like Redis. Every developer needs to install each service locally and use the same version as everyone else. The team writes documentation explaining how to set everything up. Installation steps differ depending on whether a developer is on macOS, Windows, or Linux. Every additional step increases complexity and the chance of something going wrong.
With Docker, each service is packaged into a container. Any developer can install and run that service with a single command, on any operating system, with no manual configuration. Docker also allows different versions of a service to run simultaneously without conflict, something that is difficult to achieve when installing software directly on your machine.
Problems Docker solves in deployment
Before Docker, deploying an application typically meant creating an artifact file and a set of installation instructions, then handing both to an operations team to set up on the server. Miscommunication, dependency conflicts, and differences between the development and production environments regularly caused problems that took time to diagnose and fix.
With Docker, the documentation and artifact are replaced by a Docker configuration file that encapsulates everything. The operations team runs a single Docker command to set up all the services on the server. If Docker has not been used on that server before, it needs to be installed first, but that is a one-time step. After that, deploying any Docker-based application follows the same straightforward process.
Docker vs virtual machines
An operating system has two layers: the kernel, which communicates with the hardware, and the application layer, where software runs.
A virtual machine (VM) virtualises both layers, creating a complete operating system including its own kernel. This makes VMs heavy: they require significant memory and take longer to start.
Docker virtualises the application layer only and uses the kernel of the host machine. This makes Docker containers much lighter and faster to start than virtual machines.
One consequence of this approach is that Docker was originally written for Linux, and Linux containers need to communicate with a Linux kernel. When developing on macOS or Windows, that kernel is not present. Docker Desktop solves this by providing the Linux kernel layer needed to run Linux containers on Mac or Windows. Docker Desktop is also available for Linux, where it adds a graphical interface and other useful tools on top of the core Docker engine.
Installing Docker
For Mac and Windows, the starting point is Docker Desktop. This is a single installer that includes:
- Docker Engine — the core technology for building and running containers
- Docker CLI — the command-line interface for interacting with Docker
- Docker Compose — a tool for defining and running multi-container applications using a configuration file
- Docker GUI — a graphical interface for managing containers without the command line
Docker images and containers
Two terms that are easy to confuse when starting with Docker are image and container.
A Docker image is an executable artifact that packages everything an application needs to run: the source code, services like Node.js and npm, environment variables, directories, and an OS layer. Think of it as a template.
A Docker container is a running instance of an image. When you download and run an image, you create a container. A single image can be used to run multiple containers simultaneously.
Docker registries and Docker Hub
Docker images are stored in and distributed from registries. The largest and most widely used registry is Docker Hub, which is the default registry Docker searches when you pull an image.
Major applications including PostgreSQL, MongoDB, NGINX, and WordPress have official images on Docker Hub, created in collaboration between the software authors, the Docker team, and security experts. You can search Docker Hub for any service you need.
When an application is updated, a new Docker image is published with a version tag. Best practice is to specify a version tag when pulling an image rather than using latest, which ensures your environment stays consistent and predictable.
Core Docker commands
Pulling and running images
To download an image from Docker Hub, use docker pull with the image name and version tag:
docker pull nginx:1.23
To run a container from an image, use docker run. You do not need to pull an image first — if Docker cannot find it locally it will pull it automatically:
docker run nginx:1.23
Running a container this way attaches the terminal to the container logs. To run it in the background instead, use the detached flag -d:
docker run -d nginx:1.23
To view the logs of a detached container:
docker logs {container-id}
Listing images and containers
docker images
Lists all images downloaded to your machine.
docker ps
Lists all currently running containers. Add the -a flag to include stopped containers:
docker ps -a
Starting and stopping containers
docker run always creates a new container. To stop and restart an existing container without creating a new one, use docker stop and docker start:
docker stop {container-ref}
docker start {container-ref}
Where container-ref is either the container ID or the container name. Container names are randomly generated unless you specify one using the --name flag:
docker run --name my-nginx -d nginx:1.23
Port binding
Containers run in an isolated Docker network. A service running inside a container on a given port is not accessible from your browser until you bind that container port to a port on your host machine.
Use the -p flag with the format host-port:container-port:
docker run -d -p 9000:80 nginx:1.23
This makes the NGINX server inside the container (which listens on port 80) accessible at localhost:9000 in your browser. Multiple containers can use the same container port since they are isolated from each other, but each must bind to a unique host port.
Private registries
Docker Hub also supports private repositories for images you do not want to make publicly available. Other private registries are provided by cloud platforms, including Amazon ECR, Google Container Registry, and Nexus.
It is worth understanding the distinction between a registry and a repository:
- A registry is the service that provides storage (e.g. Docker Hub, Amazon ECR)
- A repository is a collection of related images with the same name but different version tags, stored within a registry
Creating your own images with a Dockerfile
As well as using existing images from Docker Hub, you can create custom images for your own applications using a Dockerfile, a text file containing the instructions needed to build the image.
A simple Dockerfile for a Node.js application might look like this:
FROM node:19-alpine
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "app.js"]
Breaking this down:
- FROM — specifies a base image to build on. Here we use an Alpine Linux image with Node.js 19 already installed.
- WORKDIR — sets the working directory inside the container
- COPY — copies files from the host machine into the container
- RUN — executes a command during the image build process
- CMD — specifies the command to run when a container starts. Unlike RUN, this executes at runtime rather than build time.
To build the image from the Dockerfile, use docker build with a name, tag, and the path to the Dockerfile:
docker build -t my-app:1.0 .
The full stop at the end tells Docker to look for the Dockerfile in the current directory. The resulting image will appear in docker images and can be run like any other image.
Docker in the software development lifecycle
Docker fits into the full development and deployment workflow as follows:
- Development — developers pull images of any services they need (a database, a cache, a message queue) and run them locally as containers without installing anything directly on their machine.
- CI/CD — the application code and Dockerfile are pushed to a Git repository. A CI/CD pipeline picks this up, builds a Docker image, and pushes it to a private registry.
- Deployment — the operations team pulls the application image from the private registry and any dependency images from Docker Hub, then runs them on the server. The environment is identical to development.
Quick reference: common Docker commands
| Command | Description |
|---|---|
docker images | List all images on your local machine |
docker ps | List all running containers |
docker ps -a | List all containers including stopped ones |
docker pull nginx:1.23 | Pull a specific image version from Docker Hub |
docker run -d -p 9000:80 nginx:1.23 | Run a detached container with port binding |
docker run --name my-nginx -d nginx | Run a container with a custom name |
docker stop {container-ref} | Stop a running container |
docker start {container-ref} | Restart a stopped container |
docker logs {container-id} | View the logs of a container |
docker build -t my-app:1.0 . | Build an image from a Dockerfile in the current directory |