DEV Community

Cover image for Docker for Beginners: A Practical Guide
Digital Unicon
Digital Unicon

Posted on

Docker for Beginners: A Practical Guide

You've just finished building a Node.js app. It runs perfectly on your laptop. You push it to your teammate's machine, and suddenly nothing works. Wrong node version. Missing a library. Different OS behaviours. You spend two hours debugging an environment issue instead of shipping features.

Or maybe you've landed a freelance client, deployed their app to a VPS, and spent a weekend untangling dependency hell because the server runs a different version of Python than your dev machine.

This is the problem Docker was built to solve.

In 2026, Docker isn't a nice-to-have skill — it's table stakes. CI/CD pipelines, cloud deployments, microservices, local dev environments — Docker is everywhere. And the good news? Getting started is much simpler than most people think.

This guide will take you from zero to confidently building and running your own containers. No fluff. No vague theory. Just practical Docker knowledge you can use today.


What Is Docker, Actually?

Docker is a platform that lets you package your application and all its dependencies into a single, portable unit called a container.

Think of it like a shipping container on a cargo ship. Before shipping containers existed, loading goods onto ships was chaotic — every item was different, every ship handled things differently. Shipping containers standardised everything. One universal format that works on any ship, any port, anywhere in the world.

Docker does the same for software. Your app, its runtime, its libraries, its config — all bundled together in a container that runs identically on your laptop, your teammate's Linux machine, a cloud server, or a CI/CD pipeline.

Docker was created by Solomon Hykes and released publicly in 2013. It sparked a revolution in how software gets built and shipped. In 2026, it's part of the standard DevOps toolkit alongside Kubernetes, GitHub Actions, and cloud platforms like AWS and GCP.


The Problem with Traditional Deployment

Before containers, deploying software looked like this:

  1. Write code on your machine
  2. SSH into a server and manually install dependencies
  3. Realize the server has Python 3.8 and your app needs 3.11
  4. Upgrade Python and break three other apps on the server
  5. Cry

This approach has several fundamental problems:

  • Environment drift: Dev, staging, and production environments slowly diverge
  • Dependency conflicts: Two apps needing different versions of the same library
  • Manual setup: Every new server requires hours of configuration
  • "Works on my machine" bugs: Differences in OS, filesystem, or installed tools cause subtle failures
  • Fragile deployments: One wrong apt-get install can take down a production server

Virtual machines tried to solve this, but they come with their own trade-offs.


Containers vs Virtual Machines

This is one of the most common points of confusion for beginners. Both VMs and containers provide isolation, but they work very differently under the hood.

Virtual Machines

A VM runs a full operating system on top of a hypervisor (like VirtualBox or VMware). Each VM has its own kernel, memory, and disc allocation — even if you just want to run a small Node.js app.

Containers

A container shares the host machine's OS kernel but isolates the application's filesystem, processes, and network. No full OS to boot. No gigabytes of overhead.

Side-by-Side Comparison

Feature Virtual Machine Container
Startup time Minutes Seconds (often milliseconds)
Size GBs MBs
OS overhead Full OS per VM Shares host kernel
Isolation Strong (hardware-level) Strong (process-level)
Portability Limited Excellent
Resource usage Heavy Lightweight
Use case Full OS isolation App/service isolation

Practical example: Spinning up a PostgreSQL VM might take 2 minutes and use 2 GB of disc. The official Postgres Docker image starts in under 5 seconds and uses around 350MB.

💡 Tip: Containers aren't a replacement for VMs — they solve different problems. In production, you'll often find containers running inside VMs (like on EC2 instances or GKE nodes).


Docker Architecture Explained

Understanding Docker's moving parts makes everything else click. Here's a quick tour:

Docker Engine

The core of Docker — a background service (daemon) that builds and runs containers. When you type docker, you're talking to the Docker Engine.

Images

An image is a read-only blueprint for a container. It contains your app's code, runtime, dependencies, and filesystem instructions. Images are built from a Dockerfile and stored locally or on a registry.

Think of an image as a recipe. The container is the meal you cook from it.

Containers

A container is a running instance of an image. You can run multiple containers from the same image simultaneously. Containers are isolated from each other and from the host — but you can control what they share.

Docker Hub

Docker Hub is the default public registry for Docker images — like GitHub but for containers. It hosts official images for Nginx, Postgres, Redis, Node.js, Python, and thousands more. You docker pull are from here and docker push your own images back.

Volumes

Containers are ephemeral — when they stop, their filesystem disappears. Volumes are how you persist data beyond a container's lifetime. Mount a volume into a container, and data written there survives restarts and rebuilds.

Networks

Docker creates virtual networks that let containers talk to each other securely. By default, containers are isolated — you explicitly connect them to networks when needed (critical for multi-container apps).


Installing Docker

Windows

Download Docker Desktop for Windows from docker.com. It requires WSL2 (Windows Subsystem for Linux). The installer will guide you through setup.

After installation, open PowerShell and run:

docker --version
Enter fullscreen mode Exit fullscreen mode

macOS

Download Docker Desktop for Mac from docker.com. Available for both Intel and Apple Silicon (M-series) chips.

docker --version
Enter fullscreen mode Exit fullscreen mode

Linux (Ubuntu/Debian)

# Update package index
sudo apt-get update

# Install prerequisites
sudo apt-get install ca-certificates curl gnupg

# Add Docker's official GPG key
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

# Add Docker repository
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# Install Docker Engine
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin

# Allow running docker without sudo
sudo usermod -aG docker $USER
Enter fullscreen mode Exit fullscreen mode

Verify the install:

docker run hello-world
Enter fullscreen mode Exit fullscreen mode

If you see "Hello from Docker!" — you're good to go.


Essential Docker Commands

Let's get hands-on. These are the commands you'll use every single day.

docker pull — Fetch an image

# Pull the latest official Nginx image from Docker Hub
docker pull nginx

# Pull a specific version
docker pull node:20-alpine
Enter fullscreen mode Exit fullscreen mode

docker images — List local images

docker images

# Output:
# REPOSITORY   TAG         IMAGE ID       CREATED        SIZE
# nginx        latest      a6bd71f48f68   2 weeks ago    187MB
# node         20-alpine   a7d6bde5e52a   3 weeks ago    131MB
Enter fullscreen mode Exit fullscreen mode

docker run — Start a container

# Run Nginx in the background, mapping port 8080 on host to 80 in container
docker run -d -p 8080:80 --name my-nginx nginx

# -d       = detached (background)
# -p       = port mapping (host:container)
# --name   = give it a friendly name
Enter fullscreen mode Exit fullscreen mode

Visit Nginx, and you'll see the Nginx welcome page. A web server, running in seconds, requires no installation.

docker ps — List running containers

docker ps

# Add -a to see stopped containers too
docker ps -a
Enter fullscreen mode Exit fullscreen mode

docker stop — Stop a container

docker stop my-nginx
Enter fullscreen mode Exit fullscreen mode

docker rm — Remove a container

docker rm my-nginx

# Stop and remove in one shot
docker rm -f my-nginx
Enter fullscreen mode Exit fullscreen mode

docker exec — Run a command inside a container

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

# Run a one-off command
docker exec my-nginx nginx -v
Enter fullscreen mode Exit fullscreen mode

docker logs — View container output

docker logs my-nginx

# Follow logs in real time (like tail -f)
docker logs -f my-nginx
Enter fullscreen mode Exit fullscreen mode

⚠️ Warning: Always use docker stop before docker rm. Forcefully removing a running container with docker rm -f skips graceful shutdown, which can corrupt data or leave ports in use.


Build Your First Docker Container

Let's build a real app. We'll containerise a simple Python Flask API.

Project Structure

my-flask-app/
├── app.py
├── requirements.txt
└── Dockerfile
Enter fullscreen mode Exit fullscreen mode

app.py

from flask import Flask, jsonify

app = Flask(__name__)

@app.route("/")
def home():
    return jsonify({"message": "Hello from Docker! 🐳", "status": "running"})

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)
Enter fullscreen mode Exit fullscreen mode

requirements.txt

flask==3.0.0
Enter fullscreen mode Exit fullscreen mode

Dockerfile

# Start from the official Python 3.11 slim image
FROM python:3.11-slim

# Set working directory inside the container
WORKDIR /app

# Copy requirements first (for layer caching — more on this below)
COPY requirements.txt .

# Install dependencies
RUN pip install --no-cache-dir -r requirements.txt

# Copy the rest of the application
COPY . .

# Tell Docker which port the app listens on
EXPOSE 5000

# The command to run when the container starts
CMD ["python", "app.py"]
Enter fullscreen mode Exit fullscreen mode

Build the Image

docker build -t my-flask-app .
Enter fullscreen mode Exit fullscreen mode

The -t flag tags the image with a name. The "``" means "use the current directory as build context".

Watch as Docker pulls the base image, installs your dependencies, and packages everything up.

Run the Container

`bash
docker run -d -p 5000:5000 --name flask-demo my-flask-app
`

Test It

`bash
curl http://localhost:5000

{"message": "Hello from Docker! 🐳", "status": "running"}

`

Open your browser at 'http://localhost:5000'. Your Flask app is running inside a container — same result on any machine, any OS, every time.


Understanding Dockerfiles

The Dockerfile is the recipe for your image. Let's break down each instruction:

Instruction What it does
FROM Sets the base image. Every Dockerfile starts here.
WORKDIR Sets the working directory for subsequent commands. Creates it if it doesn't exist.
COPY Copies files from your host machine into the image.
RUN Executes a command during the build phase (installs packages and compiles code).
EXPOSE Documents that port the container listens on. Doesn't actually publish the port — that's done with `-pat runtime '.
CMD Defines the default command to run when the container starts. Only one CMD per Dockerfile.

Why copy the requirements.txt before the rest of the code?

Docker builds images in layers. Each instruction creates a new layer. If a layer hasn't changed, Docker reuses the cached version. By copying requirements.txtfirst and runningpip install, your dependencies only reinstall when requirements.txt changes — not every time you change app.py. This can save minutes on each build.


Docker Compose: Managing Multi-Container Apps

Real apps rarely run alone. You've got a web server, a database, and maybe a cache. Running each container with separate docker run commands is painful. Docker Compose fixes this.

Why Compose Exists

Compose lets you define your entire application stack in a single YAML file and start everything with one command: docker-compose up.

Example: Flask App + PostgreSQL Database

docker-compose.yml

`yaml
version: "3.9"

services:
web:
build: .
ports:
- "5000:5000"
environment:
- DATABASE_URL=postgresql://postgres:secret@db:5432/myapp
depends_on:
- db
volumes:
- .:/app

db:
image: postgres:16-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: postgres
POSTGRES_PASSWORD: secret
volumes:
- postgres_data:/var/lib/postgresql/data

volumes:
postgres_data:
`

Start everything:

`bash
docker compose up -d
`

Stop everything:

`bash
docker compose down
`

View all logs:

`bash
docker compose logs -f
`

Notice how the web service connects to db using the hostname db — Docker Compose automatically puts services on the same network and lets them discover each other by service name.

💡 Tip: Add docker-compose.yml to your project repo so every developer can spin up the full stack with one command. No more onboarding docs that say "install PostgreSQL 16, set these env vars..."


Common Beginner Mistakes

Learning Docker is fast. Making these mistakes is even faster. Save yourself the pain:

1. Bloated Images

Using 'base' as your base image for a Python app means pulling 200MB+ of OS utilities you don't need. Prefer slim or Alpine variants:

`dockerfile

❌ Too heavy

FROM python:3.11

✅ Much better

FROM python:3.11-slim
`

2. Forgetting``

Without a .dockerignorecopycommand, it copies everything — including node_modules files, `andbuild artefacts`. This bloats your image and can leak secrets.

Create a .dockerignore file:

`conf
.git
.env
node_modules
__pycache__
*.pyc
.DS_Store
`

3. Running as Root

By default, containers run as root. If your container is compromised, an attacker has root-level access. Add a non-root user:

`dockerfile
RUN adduser --disabled-password appuser
USER appuser
`

4. Baking Secrets into Images

`dockerfile

❌ NEVER do this

ENV DATABASE_PASSWORD=supersecretpassword123
`

Anyone who pulls your image can read this. Use environment variables at runtime (-e flag or .env file with Compose) or a secrets manager.

5. Not Using Volumes for Persistent Data

Stopped a database container and lost all your data? That's because you didn't mount a volume. Always use named volumes for databases and anything else that needs to persist.


Real-World Docker Use Cases

Local Development

Define your dev environment in Docker. Compose and every developer on the team gets the same stack. No more "works on my machine" issues. New joiner? git clone + docker compose up. Done.

CI/CD Pipelines

GitHub Actions, GitLab CI, CircleCI — all support Docker natively. Build your image in CI, run tests inside the container, push to Docker Hub or AWS ECR, and deploy. Reproducible builds from commit to production.

SaaS Products

Most SaaS companies containerise each service. User service in one container, billing in another, notifications in a third. They scale independently and deploy without affecting each other.

Microservices

Docker + Kubernetes is the dominant architecture for microservices in 2026. Each microservice lives in its own container with its own dependencies, lifecycle, and scaling rules.

Testing Environments

Spin up a fresh database container for each test run. No leftover state, no flaky tests caused by dirty data. Tear it down when done. This is a game-changer for integration testing.


Docker Best Practices for 2026

  • One process per container. Don't run your web server and database in the same container. Keep them separate and let Compose connect them.
  • Use specific image tags: FROM node:20.11.0-alpine not `FROM node:latest ''. Latest changes without warning and breaks your builds.
  • Layer your Dockerfile wisely. Put instructions that change least frequently at the top (base image, system packages) and most frequently at the bottom (your app code).
  • Scan images for vulnerabilities. Use docker scout (built into Docker Desktop) or tools like Trivy to catch security issues before deployment.
  • Keep images small. Use multi-stage builds for compiled languages to separate build tools from the final runtime image.
  • Use health checks. Tell Docker how to verify your app is actually healthy, not just running: `dockerfile HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost:5000/ || exit 1 `
  • Document your ports and volumes. EXPOSE and volume comments in your Compose file help teammates understand the architecture at a glance.

Wrapping Up

Let's recap what you've learned:

  • Docker solves the "works on my machine" problem by packaging your app and its environment together
  • Containers are lighter and faster than VMs because they share the host OS kernel
  • Docker images are blueprints; containers are running instances of those blueprints
  • Dockerfiles define how images are built, layer by layer
  • Docker Compose orchestrates multi-container apps with a single YAML file
  • Common mistakes like running as root, skipping .dockerignore, and baking in secrets are easy to avoid once you know about them

The best way to solidify this is to build something real. Take a project you've already built — a portfolio site, a REST API, a side project — and containerise it. Write the Dockerfile, run it locally, then try adding Docker Compose with a database.

You don't need to understand Kubernetes or Docker Swarm yet. Just get comfortable with these fundamentals, and you'll find Docker showing up everywhere in your work — making deployments cleaner, onboarding faster, and debugging much less of a nightmare.

Welcome to the container revolution. Now go ship something. 🐳

Top comments (0)