Docker has fundamentally changed the way developers build, ship, and run applications. Instead of wrestling with “it works on my machine” problems, Docker lets you package your app and all its dependencies into a lightweight, portable container that runs identically everywhere—on your laptop, a colleague’s workstation, or a cloud server.
This guide is written for complete beginners. By the end, you will understand Docker’s core concepts, know the essential commands, and be able to containerize a real application using a Dockerfile and Docker Compose.
What Is Docker and Why Should You Use It?
Docker is a platform that uses containerization to run applications in isolated environments called containers. Think of a container as a lightweight virtual machine, but instead of emulating an entire operating system, it shares the host’s OS kernel and only packages the libraries and files the application needs.
Key Benefits
- Consistency: Your app runs the same way in development, testing, and production.
- Isolation: Each container is sandboxed, so dependencies never conflict with other projects on your system.
- Portability: A Docker image built on macOS runs on Linux servers without modification.
- Speed: Containers start in seconds, unlike virtual machines that take minutes to boot.
- Reproducibility: Dockerfiles serve as executable documentation of your entire environment.
Installing Docker Desktop
Docker Desktop is the easiest way to get started. It bundles the Docker Engine, CLI tools, Docker Compose, and a graphical dashboard.
Windows
- Download Docker Desktop from
docker.com/products/docker-desktop. - Run the installer and follow the prompts. Docker will enable WSL 2 (Windows Subsystem for Linux) if it is not already active.
- Restart your computer when prompted.
- Open a terminal and run
docker --versionto verify the installation.
macOS
- Download the installer for your chip architecture (Intel or Apple Silicon) from the Docker website.
- Drag Docker to the Applications folder and launch it.
- Grant the required permissions and wait for the Docker engine to start.
- Run
docker --versionin Terminal to confirm.
Linux
- Follow the official installation instructions for your distribution. For Ubuntu:
sudo apt update sudo apt install docker.io docker-compose-v2 sudo systemctl enable --now docker sudo usermod -aG docker $USER - Log out and back in so the group membership takes effect.
- Run
docker --versionto verify.
Core Concepts You Need to Know
Images
A Docker image is a read-only template that contains your application code, runtime, libraries, and configuration files. Images are built from a Dockerfile and are stored locally or in a registry like Docker Hub. You can think of an image as a blueprint or a snapshot of your application at a point in time.
Containers
A container is a running instance of an image. You can start, stop, restart, and delete containers without affecting the underlying image. Multiple containers can be created from the same image, each running independently.
Volumes
Volumes are the mechanism Docker uses to persist data. By default, data written inside a container is lost when the container is removed. Volumes let you store data on the host file system so it survives container restarts and removals. They are essential for databases, file uploads, and logs.
Docker Hub
Docker Hub is the default public registry where developers publish and share images. Official images for popular software—Node.js, Python, PostgreSQL, Redis, Nginx—are maintained and regularly updated here.
Essential Docker Commands
Here are the commands you will use most often, explained with practical examples.
Pulling an Image
docker pull nginx:latest
Downloads the latest Nginx image from Docker Hub to your local machine.
Running a Container
docker run -d -p 8080:80 --name my-nginx nginx:latest
This command:
-druns the container in detached (background) mode-p 8080:80maps port 8080 on your host to port 80 inside the container--name my-nginxgives the container a human-readable namenginx:latestspecifies the image to use
Open http://localhost:8080 in your browser and you will see the Nginx welcome page.
Listing Containers
docker ps # Show running containers
docker ps -a # Show all containers, including stopped ones
Stopping and Removing Containers
docker stop my-nginx # Gracefully stop the container
docker rm my-nginx # Remove the stopped container
Viewing Logs
docker logs my-nginx # Show all logs
docker logs -f my-nginx # Follow logs in real time
Executing Commands Inside a Container
docker exec -it my-nginx bash
This opens an interactive shell inside the running container, which is invaluable for debugging.
Removing Images
docker images # List all local images
docker rmi nginx:latest # Remove a specific image
Writing Your First Dockerfile
A Dockerfile is a plain text file that contains instructions for building a Docker image. Each instruction creates a layer in the image, and Docker caches these layers to speed up subsequent builds.
Here is a Dockerfile for a simple Node.js application:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
Line-by-Line Explanation
FROM node:20-alpine– Start from the official Node.js 20 image based on Alpine Linux, which is small and secure.WORKDIR /app– Set the working directory inside the container.COPY package*.json ./– Copy dependency manifests first to leverage Docker’s layer caching.RUN npm ci --only=production– Install dependencies. Usingnpm ciensures a clean, reproducible install.COPY . .– Copy the rest of the application source code.EXPOSE 3000– Document that the app listens on port 3000.CMD ["node", "server.js"]– Define the default command to run when the container starts.
Building and Running the Image
docker build -t my-node-app .
docker run -d -p 3000:3000 my-node-app
The -t flag tags the image with a name. The . tells Docker to use the current directory as the build context.
Using Volumes for Persistent Data
Without volumes, any data written inside a container disappears when the container is removed. Here is how to create and use a volume:
# Create a named volume
docker volume create app-data
# Run a container with the volume mounted
docker run -d -v app-data:/var/lib/data --name my-app my-node-app
# Inspect the volume
docker volume inspect app-data
You can also use bind mounts to map a folder on your host directly into the container. This is especially useful during development so that code changes appear instantly:
docker run -d -v $(pwd)/src:/app/src -p 3000:3000 my-node-app
Docker Compose: Managing Multi-Container Applications
Real-world applications rarely consist of a single container. A typical web app might need a Node.js server, a PostgreSQL database, and a Redis cache. Docker Compose lets you define and run multi-container setups using a single YAML file.
Example docker-compose.yml
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://user:password@db:5432/mydb
- REDIS_URL=redis://cache:6379
depends_on:
- db
- cache
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: password
POSTGRES_DB: mydb
volumes:
- pgdata:/var/lib/postgresql/data
cache:
image: redis:7-alpine
volumes:
pgdata:
Essential Compose Commands
# Start all services in the background
docker compose up -d
# View logs for all services
docker compose logs -f
# Stop and remove all containers, networks, and volumes
docker compose down
# Rebuild images after code changes
docker compose up -d --build
With a single docker compose up command, Docker pulls the necessary images, creates a private network so the services can communicate by name, and starts everything in the correct order based on depends_on.
Practical Example: Containerizing a Python Flask App
Let’s put everything together with a quick Python example.
app.py
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello from Docker!"
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
requirements.txt
flask==3.1.*
Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["python", "app.py"]
Build and Run
docker build -t flask-app .
docker run -d -p 5000:5000 flask-app
Visit http://localhost:5000 and you will see “Hello from Docker!” served from inside the container.
Common Troubleshooting Tips
Even experienced developers run into Docker issues. Here are solutions to the problems beginners encounter most often:
- Port already in use: If you see “port is already allocated,” another process (or container) is using that port. Run
docker psto find the conflicting container and stop it, or map to a different host port (e.g.,-p 8081:80). - Permission denied on Linux: If Docker commands fail with “permission denied,” make sure you added your user to the
dockergroup withsudo usermod -aG docker $USERand then logged out and back in. - Container exits immediately: This usually means the main process crashed. Check the logs with
docker logs <container-name>to see the error output. Ensure yourCMDorENTRYPOINTruns a process that stays in the foreground. - Disk space running low: Docker images and stopped containers can accumulate quickly. Run
docker system prune -ato remove all unused images, containers, networks, and build cache. Add the--volumesflag to also remove unused volumes. - Slow builds: Make sure your
.dockerignorefile excludes large directories likenode_modules,.git, and build artifacts. This reduces the build context size dramatically.
Best Practices for Beginners
- Use official images as base images. They are regularly updated with security patches.
- Keep images small. Prefer Alpine or slim variants to reduce download times and attack surface.
- Leverage layer caching. Copy dependency files before source code so that dependencies are only reinstalled when they change.
- Never store secrets in images. Use environment variables or Docker secrets instead of hardcoding credentials.
- Use .dockerignore. Create a
.dockerignorefile to excludenode_modules,.git, and other unnecessary files from the build context. - Tag images meaningfully. Instead of relying on
latest, use version numbers or commit hashes so you can roll back reliably. - Use multi-stage builds for production images. A multi-stage Dockerfile uses one stage to build the application and a second, minimal stage to run it, resulting in much smaller final images.
- One process per container. Each container should run a single responsibility. Use Docker Compose to coordinate multiple services rather than running several processes inside one container.
Conclusion
Docker eliminates environment inconsistencies and makes your applications portable and reproducible. Start by installing Docker Desktop, experiment with pulling and running existing images, then write your own Dockerfile. Once you are comfortable, use Docker Compose to orchestrate multi-container applications.
The learning curve is gentle once you grasp the relationship between images and containers. Practice with small projects first, and before long, Docker will become an indispensable part of your development workflow.