How to Use Docker for Beginners

11 min read Developer Guides

Table of Contents

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

  1. Download Docker Desktop from docker.com/products/docker-desktop.
  2. Run the installer and follow the prompts. Docker will enable WSL 2 (Windows Subsystem for Linux) if it is not already active.
  3. Restart your computer when prompted.
  4. Open a terminal and run docker --version to verify the installation.

macOS

  1. Download the installer for your chip architecture (Intel or Apple Silicon) from the Docker website.
  2. Drag Docker to the Applications folder and launch it.
  3. Grant the required permissions and wait for the Docker engine to start.
  4. Run docker --version in Terminal to confirm.

Linux

  1. 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
  2. Log out and back in so the group membership takes effect.
  3. Run docker --version to 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:

  • -d runs the container in detached (background) mode
  • -p 8080:80 maps port 8080 on your host to port 80 inside the container
  • --name my-nginx gives the container a human-readable name
  • nginx:latest specifies 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. Using npm ci ensures 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 ps to 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 docker group with sudo usermod -aG docker $USER and 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 your CMD or ENTRYPOINT runs a process that stays in the foreground.
  • Disk space running low: Docker images and stopped containers can accumulate quickly. Run docker system prune -a to remove all unused images, containers, networks, and build cache. Add the --volumes flag to also remove unused volumes.
  • Slow builds: Make sure your .dockerignore file excludes large directories like node_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 .dockerignore file to exclude node_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.

Related Articles