DEV Community

楊東霖
楊東霖

Posted on • Originally published at devtoolkit.cc

Docker for Beginners: From Zero to Containerization

Containers have transformed how software is built and deployed. Ten years ago, "it works on my machine" was an accepted excuse. Today, Docker eliminates that excuse entirely — your containerized app runs identically on a developer laptop, a CI server, and a production cluster.

If you've heard about Docker but haven't tried it yet, this guide is your starting point. We'll go from zero — what Docker is and why it exists — to running a real multi-container application with docker-compose. By the end, you'll have the foundation to containerize any project and understand what's happening under the hood.

What Is Docker and Why Does It Matter?

Docker is a platform for developing, shipping, and running applications inside containers. A container is an isolated, lightweight process that bundles your application with everything it needs to run: code, runtime, libraries, environment variables, and config files.

Containers vs. Virtual Machines

The most common question for Docker newcomers is: how are containers different from virtual machines? Both provide isolation, but they do it at different levels of the stack.

  • Virtual machines run a full guest operating system on top of a hypervisor. Each VM includes its own kernel, which means they're heavy (GBs of disk space) and slow to start (minutes).
  • Containers share the host operating system's kernel. They only package the application and its dependencies — not the OS itself. This makes containers small (MBs), fast to start (seconds or less), and far more efficient at resource usage.

A typical server might run 5–10 virtual machines. The same server can comfortably run 50–100 containers. This density advantage is a major reason Docker adoption exploded in the DevOps world.

The Core Problem Docker Solves

Before Docker, deploying a web app meant managing dependency versions across environments. Your app needs Python 3.11 — but the server has 3.9. Your app uses a specific version of a native library — but the sysadmin upgraded it for another service. Docker eliminates these conflicts by isolating each application's dependencies inside its own container. The container carries everything it needs. The host just runs containers.

Installing Docker

Docker Desktop is the easiest way to get started on Windows and macOS. It includes Docker Engine, Docker CLI, docker-compose, and a GUI.

  • Windows/Mac: Download Docker Desktop from docker.com and follow the installer. Make sure virtualization is enabled in your BIOS.
  • Linux: Use the official convenience script or your distribution's package manager. On Ubuntu: sudo apt install docker.io docker-compose-v2

Verify the installation:

docker --version
# Docker version 27.x.x

docker run hello-world
# Should print a success message confirming Docker is working
Enter fullscreen mode Exit fullscreen mode

Core Concepts

Before diving into commands, it helps to understand the four building blocks of Docker.

Images

A Docker image is a read-only template that describes what a container will contain. Think of it as a blueprint. Images are built in layers — each instruction in a Dockerfile adds a new layer on top of the previous ones.

Images are stored in registries. Docker Hub is the default public registry with thousands of official images for popular runtimes and databases: node, python, postgres, nginx, redis, and many more.

Containers

A container is a running instance of an image. You can run many containers from the same image simultaneously. Containers are ephemeral by default — when you stop and remove a container, any data written inside it is gone. Use volumes to persist data.

Volumes

Volumes are the mechanism for persisting data generated by containers. A volume exists outside the container's filesystem — it survives container restarts and deletions. Use volumes for databases, uploaded files, and any other stateful data.

Networks

Docker networks allow containers to communicate with each other. By default, containers on the same Docker network can reach each other by container name. This is how your web app container talks to your database container without exposing ports to the internet.

Essential Docker Commands

Working with Images

# Pull an image from Docker Hub
docker pull node:20-alpine

# List locally available images
docker images

# Remove an image
docker rmi node:20-alpine

# Remove all unused images
docker image prune

# Build an image from a Dockerfile in the current directory
docker build -t my-app:1.0 .

# Tag an existing image
docker tag my-app:1.0 my-app:latest
Enter fullscreen mode Exit fullscreen mode

Running Containers

# Run a container (downloads image if needed)
docker run nginx

# Run in the background (detached mode)
docker run -d nginx

# Run with a name
docker run -d --name my-nginx nginx

# Run and expose a port (host:container)
docker run -d -p 8080:80 nginx

# Run with an environment variable
docker run -d -e NODE_ENV=production my-app

# Run and mount a volume
docker run -d -v /host/data:/app/data my-app

# Run an interactive shell inside a container
docker run -it ubuntu bash

# Run a one-off command and remove the container immediately
docker run --rm node:20 node --version
Enter fullscreen mode Exit fullscreen mode

Managing Containers

# List running containers
docker ps

# List all containers (including stopped)
docker ps -a

# Stop a container gracefully
docker stop my-nginx

# Kill a container immediately
docker kill my-nginx

# Start a stopped container
docker start my-nginx

# Restart a container
docker restart my-nginx

# Remove a stopped container
docker rm my-nginx

# Remove a running container (force)
docker rm -f my-nginx

# Remove all stopped containers
docker container prune
Enter fullscreen mode Exit fullscreen mode

Inspecting and Debugging

# View container logs
docker logs my-nginx

# Follow logs in real time
docker logs -f my-nginx

# Execute a command inside a running container
docker exec my-nginx ls /etc/nginx

# Open an interactive shell inside a running container
docker exec -it my-nginx bash

# Inspect container details (JSON output)
docker inspect my-nginx

# View resource usage (CPU, memory, network)
docker stats

# View running processes inside a container
docker top my-nginx
Enter fullscreen mode Exit fullscreen mode

Writing Your First Dockerfile

A Dockerfile is a text file with instructions for building a Docker image. Every line becomes a layer in the image. Let's build a simple Node.js web application.

Project Structure

my-app/
  Dockerfile
  package.json
  src/
    index.js
Enter fullscreen mode Exit fullscreen mode

A Simple Node.js Dockerfile

# Start from the official Node.js image (Alpine = minimal size)
FROM node:20-alpine

# Set the working directory inside the container
WORKDIR /app

# Copy dependency files first (layer caching optimization)
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production

# Copy the rest of the application code
COPY src/ ./src/

# Expose the port the app listens on
EXPOSE 3000

# The command to run when the container starts
CMD ["node", "src/index.js"]
Enter fullscreen mode Exit fullscreen mode

Key Dockerfile Instructions

  • FROM — Sets the base image. Every Dockerfile starts with this. Use official, minimal images (Alpine variants are smaller).
  • WORKDIR — Sets the working directory for subsequent commands. Creates the directory if it doesn't exist.
  • COPY — Copies files from your local machine into the image.
  • RUN — Executes a command during the build process. Each RUN creates a new layer.
  • EXPOSE — Documents which port the container listens on. Informational only — you still need -p when running.
  • CMD — The default command to run when a container starts. Can be overridden at runtime.
  • ENV — Sets environment variables available at runtime.
  • ARG — Build-time variables (not available at runtime).

Layer Caching: Why Order Matters

Docker caches each layer. If a layer hasn't changed since the last build, Docker reuses the cached version. This is why we copy package.json before the rest of the source code — the dependency installation layer only rebuilds when package.json changes, not every time you edit your code.

# Bad: copies all files first, so ANY change invalidates the npm install layer
COPY . .
RUN npm ci

# Good: only rebuilds npm install when dependencies change
COPY package*.json ./
RUN npm ci
COPY . .
Enter fullscreen mode Exit fullscreen mode

Building and Running Your Image

# Build the image
docker build -t my-app:1.0 .

# Run a container from it
docker run -d -p 3000:3000 --name my-app my-app:1.0

# Visit http://localhost:3000 in your browser
Enter fullscreen mode Exit fullscreen mode

Using .dockerignore

Create a .dockerignore file in the same directory as your Dockerfile to exclude files from the build context. This speeds up builds and prevents accidentally including sensitive files.

node_modules
.git
.env
*.log
dist
coverage
.DS_Store
Enter fullscreen mode Exit fullscreen mode

Introduction to Docker Compose

Real applications rarely run in a single container. Your web app needs a database, a cache, maybe a background worker. Docker Compose lets you define and manage multi-container applications with a single YAML file.

A Complete docker-compose.yml Example

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: production
      DATABASE_URL: postgres://user:password@db:5432/myapp
      REDIS_URL: redis://cache:6379
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
      POSTGRES_DB: myapp
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  cache:
    image: redis:7-alpine
    restart: unless-stopped

volumes:
  postgres_data:
Enter fullscreen mode Exit fullscreen mode

Essential Docker Compose Commands

# Start all services (build images if needed)
docker compose up

# Start in detached mode (background)
docker compose up -d

# Build or rebuild images
docker compose build

# Stop all services
docker compose down

# Stop and remove volumes (WARNING: deletes data)
docker compose down -v

# View logs for all services
docker compose logs

# Follow logs for a specific service
docker compose logs -f app

# Run a one-off command in a service container
docker compose exec app sh

# Scale a service to multiple instances
docker compose up -d --scale app=3

# View running services
docker compose ps
Enter fullscreen mode Exit fullscreen mode

Docker Best Practices for Beginners

  • Use specific image tags. Never use latest in production — it makes builds unpredictable. Pin to a specific version like node:20.11-alpine3.19.

  • Run as a non-root user. By default, containers run as root. Add a user in your Dockerfile for better security:

RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
Enter fullscreen mode Exit fullscreen mode
  • Use multi-stage builds for production. Separate the build environment from the runtime image to dramatically reduce image size:
FROM node:20 AS builder
WORKDIR /app
COPY . .
RUN npm ci && npm run build

FROM node:20-alpine AS runtime
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]
Enter fullscreen mode Exit fullscreen mode
  • Never bake secrets into images. Use environment variables, Docker secrets, or a secrets management tool like Vault. Secrets in image layers are permanent and can be extracted.

  • Keep images small. Start from Alpine or Distroless base images, remove build tools after use, and combine RUN commands with && to reduce layers.

  • One process per container. Each container should do one thing. Don't run Nginx and your app and a cron job in the same container. Use separate containers and orchestrate with Compose.

Common Patterns and Workflows

Development vs. Production Compose Files

Use multiple Compose files to override settings for different environments:

# docker-compose.yml (base)
# docker-compose.override.yml (dev overrides, auto-applied)
# docker-compose.prod.yml (production overrides)

# Run with production overrides
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Enter fullscreen mode Exit fullscreen mode

Watching for Code Changes in Development

# Mount your source code as a volume for live reload
services:
  app:
    build: .
    volumes:
      - ./src:/app/src
    command: npm run dev  # Use a dev server that watches for changes
Enter fullscreen mode Exit fullscreen mode

Viewing and Cleaning Up Everything

# See what's taking up disk space
docker system df

# Remove everything unused (images, containers, networks, build cache)
docker system prune -a
Enter fullscreen mode Exit fullscreen mode

What Comes After Docker?

Once you're comfortable with Docker, the natural next step is container orchestration for production — running containers across multiple servers with automatic scaling and healing. The two main options are:

  • Docker Swarm — Docker's built-in orchestration. Simpler than Kubernetes, good for teams that want to stay in the Docker ecosystem. Run docker swarm init to get started.
  • Kubernetes — The industry-standard orchestration platform. More complex but far more powerful. Essential for large-scale, production-grade deployments. See our Docker Compose vs Kubernetes guide to understand when to make the leap.

Summary

You've covered the fundamentals of Docker: what containers are and why they exist, how to pull and run images, how to write a Dockerfile, and how to orchestrate multi-container apps with docker-compose. These skills will serve you on every project from a simple side project to enterprise deployments.

The best way to cement this knowledge is to containerize something you already have — a personal project, a script you run regularly, or a local development environment. The first containerization is the hardest; by the fifth, it feels effortless.

Ready to explore more developer tools? Visit devplaybook.cc/tools for our free suite of developer utilities, including JSON formatters, regex testers, and more.

Free Developer Tools

If you found this article helpful, check out DevToolkit — 40+ free browser-based developer tools with no signup required.

Popular tools: JSON Formatter · Regex Tester · JWT Decoder · Base64 Encoder

🛒 Get the DevToolkit Starter Kit on Gumroad — source code, deployment guide, and customization templates.

Top comments (0)