You’ve probably heard the phrase: “It works on my machine.” Your local environment runs Node 16, PostgreSQL 12, and Redis 6. Production runs Node 18, PostgreSQL 14, and Redis 7. A teammate uses Windows. Another uses an M1 Mac. The frontend team spins up three separate services just to test an API change.
This is the problem Docker addresses. It’s not magic. It’s a practical solution to a real engineering problem: how do you ensure your application runs the same way everywhere?
What is Docker?
Docker is a containerization platform. That’s the technical definition, but it doesn’t explain what actually happens. Think of it differently: Docker packages your application and all its dependencies into a standardized unit that runs consistently across machines.
The key insight is that Docker doesn’t virtualize hardware as a virtual machine does. Instead, it isolates your application at the operating system level. Your container shares the host’s kernel but has its own filesystem, networking, and process space. This makes containers lightweight, fast to start, and resource-efficient.
Understanding Docker containers and images
Before you can use Docker effectively, you need to understand the relationship between two core concepts: images and containers.
What is a Docker image?
An image is a blueprint. It’s a read-only template that contains everything your application needs to run: your code, runtime, system libraries, dependencies, and environment variables. Images are built in layers. Each instruction in your Dockerfile creates a new layer on top of the previous one.
An image is a snapshot of your application at a specific point in time, and once built, it doesn’t change. This immutability is powerful because it ensures consistency across deployments.
What is a Docker container?
A container is a running instance of an image. When you run an image, Docker creates a container. The container is mutable, isolated, and ephemeral, meaning you can start, stop, or delete it at any time. Multiple containers can run from the same image simultaneously, each with its own isolated environment.

Here’s the key distinction: the image is the template. The container is the running process.
How layering works
Understanding layers is essential because they affect build speed, image size, and your workflow.
When you write a Dockerfile, each instruction creates a layer:
Each Dockerfile instruction creates a layer. In this example, there are seven:
- The base image (
node:18-alpine) - Set working directory
- Copy
package.json - Install dependencies
- Copy application code
- Expose port
- Set startup command
Docker caches each layer. If you run the build again and only your application code has changed, Docker skips layers 1-4 and rebuilds from layer 5 onward. This speeds up development significantly.

But there’s a tradeoff. More layers mean more control over caching, but also more complexity. Fewer layers mean faster builds but less granular control.
A common mistake is adding everything in one RUN instruction:
This bundles everything into a single layer. However, if you later add another package, Docker rebuilds the entire layer even though you only changed one line.
The optimal approach depends on your workflow. For development, more granular layers are useful. For production, sometimes a more compact image is better. There’s no universal rule. Understand the tradeoff and choose based on your needs.
What is Docker used for
Docker serves multiple purposes in modern development:
- Local development parity: Your development environment matches production. No more “works on my machine” problems.
- Team consistency: Everyone on the team runs the same environment. Onboarding becomes faster because a new developer just runs
docker-compose upinstead of manually installing multiple dependencies and services. - Microservices architecture: Each service runs in its own container. You can update, scale, and deploy services independently.
- CI/CD pipelines: Docker images are built once and deployed consistently across staging and production environments.
- Dependency isolation: Multiple projects with conflicting dependencies can coexist on the same machine. Each container has its own environment.
How to install Docker
Download Docker Desktop from the official Docker website. Docker Desktop includes the Docker engine, command-line interface, and Docker Compose.
Once installed, verify it works:
You should see the version number. That’s it. You’re ready to run containers.
How to use Docker
The basic workflow is straightforward:
- Write a Dockerfile
- Build an image
- Run a container
- (Optionally) push the image to a registry
Writing your first Dockerfile
Create a file named Dockerfile (no extension). For a complete reference of all Dockerfile instructions, check the Dockerfile reference:
Let’s break this down:
FROM node:18-alpinespecifies the base image. Alpine is a minimal Linux distribution that significantly reduces your final image size compared to standard distributions.WORKDIR /appsets the working directory inside the container.COPY package.json package-lock.json ./copies dependency files.RUN npm installinstalls dependencies.COPY . .copies your application code.EXPOSE 3000documents that the app listens on port 3000 (doesn’t actually publish the port).CMD ["node", "server.js"]specifies the startup command.
Building an image
This builds an image named my-app with tag 1.0 from the current directory. The -t flag specifies the name and tag. Tags let you version your images.
Docker reads the Dockerfile, executes each instruction, and creates layers. You’ll see output for each step. If any step fails, the build stops.
Running a container
This starts a container from your image. The -p flag maps port 3000 on your machine to port 3000 in the container. Your app should be accessible at localhost:3000.
To run in the background:
The -d flag runs the container in detached mode.
Common commands
Here are the commands you’ll use most frequently. For the complete list of all Docker commands and flags, refer to the Docker CLI reference:
Working with volumes and environment variables
Containers are ephemeral. When you stop a container, any data written to the container’s filesystem is lost. Volumes solve this.
A volume mounts a directory from your host machine into the container:
This is essential for development. You can mount your source code into the container and see changes instantly without rebuilding the image:
Environment variables configure your application at runtime without requiring a rebuild:
You can also use a .env file:
What is Docker Compose
Docker Compose is a tool for defining and running multi-container applications. Instead of running multiple docker run commands, you define your entire stack in a YAML file.
Here’s a practical example:
With this file, one command starts everything:
Docker Compose handles networking automatically. Services communicate by hostname, so your app container can connect to the database at postgres://db:5432, where db is the service name.

To stop everything:
Docker Compose is invaluable for local development. You get a realistic multi-service environment without manual setup.
Container networking
By default, containers can’t talk to each other unless you explicitly connect them. Docker handles this through networks.
When you use Docker Compose, it creates a network and connects all services to it automatically. That’s why your app can reach the database by service name.
If you’re running containers manually, create a network:
Now app can reach db by hostname.
Common patterns and gotchas
Understanding the basics of containers, images, and Compose gets you far, but real-world Docker usage requires awareness of common patterns and pitfalls that trip up developers.
Process management and signals
Your containerized application should run as PID 1 (the main process). PID 1 is a special process ID in Unix systems—it’s the first process that starts when the container launches. This matters for signal handling. If your app ignores SIGTERM (the termination signal Docker sends when stopping a container), Docker will terminate it after a timeout.
There are two ways to specify the startup command in a Dockerfile: the exec form and the shell form. The exec form runs your command directly without wrapping it in a shell, which allows it to become PID 1 and properly receive signals. The shell form, by contrast, launches a shell first, which becomes PID 1 instead, and your application becomes a child process that may miss signals.
Use the exec form of CMD:
Not the shell form:
The shell form wraps your process in a shell wrapper, so the wrapper becomes PID 1 and may not forward signals correctly.
Image size
Smaller images deploy faster and reduce the attack surface. Use Alpine-based images when possible. Node Alpine is 40MB compared to 900MB for the full Node image.
Be strategic about what you copy into the image. Don’t include node_modules if you’re going to install them anyway. Use .dockerignore:
Layer caching strategy
Order your Dockerfile instructions from least frequently changed to most frequently changed. Dependencies change less often than code:
If you reversed this order, Docker would rebuild npm install every time you changed your code.
Security basics
Don’t run containers as root if you can avoid it:
Don’t put secrets in your image. Use environment variables or secret management tools instead.
Beyond the basics
So far, you’ve learned to build and run containers on your local machine. But Docker’s real power shows when you start sharing images across teams and deploying to different environments. Let’s explore what comes after the fundamentals.
Pushing images to a registry
Once your image is built, you can push it to Docker Hub (the official registry) or a private registry:
Others can then run your image:

Production considerations
Docker containers are the foundation for Kubernetes and other orchestration platforms. In production, you typically don’t run containers manually. An orchestrator manages scaling, updating, and monitoring.
That is beyond the scope here, but understand that Docker is one layer of a larger orchestration system.
Debugging containers
Sometimes containers fail to start or behave unexpectedly. Common approaches:
- Check logs: Most problems show up in container logs:
- Open a shell: Examine the container’s filesystem directly:
- Inspect the image: Look at how the image was built:
- Run interactively: Start a container with an interactive shell to explore its filesystem instead of executing the default startup command:
If you’re stuck and need guidance,Play with Docker Training provides structured labs and tutorials, and theDocker Community Forums are active places to ask questions and get help.
Resources for a deeper dive
TheDocker official documentation should always be your primary resource for understanding Docker behavior and best practices. The official docs are thorough and well-maintained.
For readers who learn best by structured courses with guided projects, interactive learning platforms can accelerate your progress. Here are a few recommendations:
- Beginners Guide to Docker: A beginner-level course that walks through Docker fundamentals, containerization concepts, building and running containers, Docker Compose, and practical workflows. Includes hands-on exercises and real-world examples that directly build on what this guide covers.
- Docker for Developers: A practical course focused on Docker in real development workflows, covering image optimization, multi-container applications, networking, debugging, and integration with development tools. The next step is if you’re building with Docker and want to move beyond the basics into production-ready patterns.
What you now understand
You understand what Docker is. You know that a container is an isolated process and an image is a blueprint for that process. You grasp how layering works and why it matters for build performance.
You can write a Dockerfile, build an image, and run a container. You know how to mount volumes for persistent data and how to use environment variables for configuration. You understand what Docker Compose does and when to use it.
You’ve seen common patterns and pitfalls. You know the difference between the shell form and exec form of CMD, why image size matters, and how layer caching works.
This is a solid foundation. From here, you can explore Docker in depth. You can run more complex multi-container setups, optimize images for production, and integrate Docker into CI/CD pipelines.
But before you do any of that, you have the mental model. You understand the underlying concepts, not just the commands. That’s what matters.