What Is Docker? Containers Explained for Beginners
Learn what Docker is, why containers exist, how they differ from virtual machines, how to write a Dockerfile, how Docker Compose works, and why every development team uses Docker in 2026.
Docker is one of those tools that experienced developers consider essential and beginners find mysterious. The concept is simpler than it sounds, and understanding it makes a wide range of modern development practices immediately clearer: continuous integration, cloud deployments, microservices, and consistent team environments all depend on containers. This guide starts from zero and covers everything you need to understand and use Docker confidently.
What Is Docker?
Docker is a platform for building, shipping, and running applications inside containers. A container is a lightweight, isolated package that contains everything an application needs to run: the code, the runtime, the libraries, the configuration, and the operating system dependencies. Nothing more, and nothing less.
The key insight is this: a container runs the same way on every machine. It does not matter whether the machine is your laptop, a colleague’s workstation, a CI/CD server, or a production cloud instance running in a data centre on a different continent. The container always contains the same environment, so the application always behaves the same way.
A Docker container is not just code. It is code plus every single thing the code needs to run, packaged together so it behaves identically on any machine that runs it.
The Problem Docker Solves
Before Docker, deploying applications was unreliable because of environment differences between machines. Code worked on a developer’s laptop but failed on the staging server because of different operating system versions, different library versions, different runtime configurations, or subtly different environment variable setups.
The classic developer phrase was “it works on my machine,” followed by hours of debugging to understand why production behaved differently. Docker eliminates this entire class of problems by packaging the entire environment along with the code.
- Works on your laptop, breaks in production
- Different Node.js version on each machine
- Library conflicts between projects
- Hours of setup for new team members
- Production config differs from development
- Staging environment drifts over time
- Same container runs on every machine
- Runtime version locked in the Dockerfile
- Isolated containers: no conflicts between projects
- New developers: clone, run, working in minutes
- Same image in development and production
- Infrastructure is version-controlled and reproducible
Containers vs Virtual Machines
Containers are often compared to virtual machines, but they are fundamentally different in how they work. Understanding the difference explains why containers are so much faster and lighter than VMs, and why they have become the standard deployment unit for modern applications.
A VM emulates an entire computer, including its own full operating system kernel, on top of a hypervisor. Running five VMs means running five complete operating systems simultaneously, each using significant memory and CPU.
A container shares the host operating system’s kernel. It contains only the application, its dependencies, and the libraries it needs. Not a full operating system. Not a hypervisor. Just the essentials.
The practical result: containers start in under a second instead of minutes, consume a fraction of the memory, and you can run hundreds on the same hardware that would support only a handful of VMs. This efficiency is what makes container-based deployments and microservices architectures economically viable at scale.
The Four Core Docker Concepts
Docker has four core concepts you need to understand before anything else makes sense. These four terms appear in every piece of Docker documentation, every tutorial, and every conversation about containers:
Writing a Dockerfile
A Dockerfile is a plain text file that contains instructions for building a Docker image. Each instruction adds a layer to the image. Docker caches each layer: if the instruction has not changed since the last build, Docker reuses the cached layer instead of re-executing it. Understanding layer caching explains why the order of instructions in a Dockerfile matters for build performance.
Here is a complete, production-quality Dockerfile for a Node.js application with explanations for each instruction:
Dockerfile Instructions Reference
Every Dockerfile uses the same set of instructions. Understanding what each instruction does and when to use it is everything you need to write Dockerfiles for any application:
Docker caches each layer and reuses it if the instruction has not changed. Copy package.json and run npm install before copying your application code. This way, the dependency installation layer is only re-run when your dependencies change, not every time you change a source file. Changing the order so COPY . . comes before RUN npm install means every code change triggers a full reinstall of all dependencies, dramatically slowing builds.
Essential Docker Commands
These are the commands you will use every day when working with Docker. Each one is worth understanding rather than just copying from examples:
| Command | What It Does | Common Options |
|---|---|---|
| docker build -t name . | Build an image from the Dockerfile in the current directory and tag it with a name | -t name:tag to specify version, –no-cache to rebuild all layers |
| docker run -p 3000:3000 name | Create and start a container from an image. The -p flag maps host port to container port | -d for detached (background), -e KEY=val for env vars, –name to name the container |
| docker ps | List all currently running containers with their IDs, names, ports, and status | -a to show stopped containers too |
| docker stop id | Gracefully stop a running container by sending SIGTERM, then SIGKILL after timeout | Use container ID or name. docker kill sends SIGKILL immediately. |
| docker logs id | Print the stdout and stderr output of a container | -f to follow (tail) live output, –tail 50 for last 50 lines |
| docker exec -it id sh | Open an interactive shell inside a running container for debugging | Use bash instead of sh for bash shell if available |
| docker images | List all images stored locally with their names, tags, and sizes | -a to include intermediate layers |
| docker pull postgres:15 | Download an image from Docker Hub without running it | Specify the tag after the colon. latest is the default if no tag is given. |
| docker rm id | Remove a stopped container. Running containers must be stopped first. | -f to force remove a running container |
| docker rmi image | Remove a locally stored image. The image must not be used by any container. | docker image prune removes all unused images at once |
| docker system prune | Remove all stopped containers, unused images, unused networks, and build cache in one command | -a to also remove images not referenced by any container |
When debugging a container that is not behaving as expected, run docker exec -it container-id sh to open a shell inside the running container. From there, run env to see all environment variables the container has, cat /etc/hosts to see network configuration, or any other diagnostic command. You can also inspect the JSON-formatted container configuration with docker inspect container-id. Paste the output into the JSON Formatter to read the full configuration clearly.
Docker Compose: Running Multiple Containers Together
Most real applications need more than one service: a web server, a database, a cache, a message queue, a background worker. Running each as a separate container and manually connecting them with network flags is tedious and error-prone. Docker Compose solves this by letting you define an entire multi-container application in a single YAML configuration file and start everything with one command.
Docker Compose is included with Docker Desktop. On Linux, it is available as the docker compose plugin. The configuration file is called docker-compose.yml and is committed to version control alongside the application code.
A Complete Docker Compose Example
Here is a docker-compose.yml for a web application with a PostgreSQL database and a Redis cache. This covers the most common multi-service development setup:
The depends_on key tells Docker Compose to start the listed services before the dependent service. In the example above, the web service waits for the db and cache services to start first. Note that depends_on waits for the container to start, not for the service inside it to be ready. For databases, you may need a health check or a retry mechanism in your application to handle the brief window between the container starting and PostgreSQL accepting connections.
Why Development Teams Use Docker
Docker has become the standard for application delivery not because it is fashionable but because it solves real, expensive problems. Here are the specific benefits that make it worth learning for every developer:
7-Step Guide to Getting Started With Docker
If you have never used Docker before, follow this sequence. Each step builds on the previous one and takes you from a clean machine to a fully containerised application with a database:
- Install Docker Desktop. Download Docker Desktop from docker.com for your operating system. Docker Desktop includes the Docker Engine, Docker CLI, Docker Compose, and a GUI for managing containers and images. On macOS and Windows, it runs a lightweight Linux VM in the background to host containers. After installing, verify with docker –version and docker compose version in your terminal.
- Run your first container from Docker Hub. Before writing any Dockerfile, run a pre-built container to understand what a container is: docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=secret postgres:15. This downloads the official PostgreSQL 15 image, starts a container, and maps port 5432. You now have a running database with no installation other than Docker itself.
- Write a Dockerfile for your own application. Create a Dockerfile in the root of your project. Start with the official base image for your language (node:20-alpine, python:3.12-slim, etc.), set the WORKDIR, copy and install dependencies first, then copy the application code. Test the build with docker build -t my-app . and fix any errors before proceeding.
- Run your application container and confirm it works. Run docker run -p 3000:3000 my-app and verify the application is accessible at localhost:3000. Check the output with docker logs if it does not start correctly. Inspect any JSON configuration output by pasting it into the JSON Formatter to read it clearly.
- Add a docker-compose.yml to connect your application to its database. Create a docker-compose.yml that defines both your web service and a database service. Use depends_on to set the startup order. Use named volumes to persist database data across container restarts. Run docker compose up and confirm both services start and communicate.
- Add a .dockerignore file. Create a .dockerignore file in the project root listing files and directories that should not be copied into the image. Always exclude node_modules, .env, .git, test files, and documentation. This keeps images small and prevents sensitive files from being baked into the image. The format is identical to .gitignore.
- Push your image to a registry for team use. Tag your image with docker tag my-app registry/my-app:1.0.0 and push it with docker push registry/my-app:1.0.0. Your team can now pull and run the image without building it. This is the foundation of every CI/CD pipeline: build the image once, push to a registry, pull and deploy the same image to every environment. Use the Text Diff Checker to compare docker-compose.yml files between environments when debugging configuration differences.
Frequently Asked Questions About Docker
No. Docker Desktop runs on macOS and Windows. On those platforms it creates a lightweight Linux virtual machine in the background to run containers, because containers use Linux kernel features. From your perspective, everything works through the same Docker CLI and Docker Desktop GUI regardless of your host operating system. On Linux, Docker Engine runs natively without any VM layer, which gives slightly better performance. For development purposes, Docker Desktop on macOS or Windows works well and is the standard setup for most developers.
Docker Desktop is free for personal use, education, and small businesses under 250 employees and 10 million USD in annual revenue. Larger enterprises require a paid Docker subscription. The Docker Engine on Linux, the Docker CLI, and the Docker Compose plugin are all open source and completely free with no size restrictions. Docker Hub’s public image hosting is free for public images, with rate limits on unauthenticated pulls. For teams with larger scale or privacy requirements, private registries like GitHub Container Registry, AWS ECR, and Google Artifact Registry are alternatives.
Docker creates and runs individual containers on a single machine. Kubernetes orchestrates many containers across many machines, handling automatic scaling, load balancing, self-healing, rolling updates, and service discovery. Think of Docker as the tool that builds and runs containers, and Kubernetes as the system that manages thousands of those containers across a cluster of servers. Docker is where every developer starts. Kubernetes is where large production systems run, typically managed by a cloud provider (Google GKE, AWS EKS, Azure AKS). You do not need Kubernetes to use Docker: most small and medium applications deploy with Docker Compose on a single server or use a managed container platform like AWS ECS or Google Cloud Run.
Both define what runs when a container starts, but they behave differently when arguments are passed to docker run. CMD specifies a default command that can be completely replaced by arguments passed to docker run. ENTRYPOINT specifies a command that always runs, and any arguments from docker run are appended to it rather than replacing it. A common pattern is to use ENTRYPOINT for the executable and CMD for default arguments: ENTRYPOINT [“node”] and CMD [“server.js”] means docker run my-app runs node server.js, but docker run my-app debug.js runs node debug.js. For most application containers, CMD alone is sufficient.
There are two main mechanisms: volumes and bind mounts. A named volume is managed by Docker and persists independently of any container. It is the right choice for database data and any persistent application storage. A bind mount maps a directory from the host machine into the container. Bind mounts are used in development to mount your source code into the container so code changes are reflected immediately without rebuilding the image. In production, volumes are preferred because they are managed by Docker and are portable. You can also share data between containers using a shared named volume referenced by multiple services in docker-compose.yml.
A multi-stage build uses multiple FROM instructions in a single Dockerfile, with each stage building on or copying from the previous one. The most common use case is separating the build environment from the runtime environment. For a Go application, the first stage installs the full Go compiler and builds the binary. The second stage starts from a minimal base image and copies only the compiled binary from the first stage. The final image contains only the binary, not the compiler, build tools, or source code, making it dramatically smaller and more secure. Multi-stage builds are the standard for compiled languages and frontend JavaScript applications where the build toolchain is much larger than the runtime artefact.
Tools that work alongside your Docker workflow
Inspect JSON from docker inspect, compare docker-compose files between environments, convert configuration formats, and more. All free, all in your browser, no login required.
Start With One Container. The Rest Follows.
Docker solves a real problem that every development team has experienced: environment differences that cause bugs, inconsistent setups that slow down onboarding, and the gap between what works locally and what works in production. Once you understand the four core concepts (image, container, Dockerfile, registry) and how Docker Compose connects multiple services together, the entire ecosystem of modern DevOps tooling starts to make sense.
The best way to learn Docker is to containerise something you are already working on. Write a Dockerfile for your current application, get it building and running, then add a docker-compose.yml that brings up your database alongside it. The 7-step guide above gives you a concrete sequence that works for any stack. The concepts transfer directly from a Node.js app to Python to Go to Ruby.
When you run docker inspect container-id and want to read the JSON output clearly, paste it into the JSON Formatter. When you are debugging differences between your development and production docker-compose.yml configurations, paste both into the Text Diff Checker to see exactly what differs. Both tools are free and work directly in your browser.