Getting Started with Docker – A Complete Beginner's Guide

Docker has transformed how software is developed, tested, and deployed. If you are new to the concept, this guide walks you through everything from the fundamentals to running your first containerized application.

The Problem Docker Solves

Before Docker, setting up a development environment was painful. You would install Node.js, PostgreSQL, Redis, and a dozen other dependencies on your machine. Your colleague on a Mac might have different versions. The production server ran a different Linux distribution. “It works on my machine” became the most repeated phrase in software teams.

Virtual machines helped by providing isolated environments, but they were heavy. Each VM runs a full operating system, consuming gigabytes of disk space and memory. Starting one takes minutes.

Docker takes a different approach. Instead of virtualizing hardware and running a full OS, it shares the host kernel and isolates only the application and its dependencies. The result is something much lighter: containers start in seconds, use minimal resources, and behave consistently everywhere.

Understanding the Core Concepts

There are four key concepts to understand before diving in:

Image: Think of an image as a snapshot or template. It contains the application code, the runtime, system libraries, and any other files the application needs. Images are read-only — you build them once and run them anywhere. They are composed of layers, where each instruction in the build process creates a new layer on top of the previous one. Layers are cached, so rebuilding an image after a small code change is fast.

Container: A container is a running instance of an image. You can start multiple containers from the same image, and each one runs in isolation. Containers have their own file system, networking, and process space. When you stop a container, all changes made inside it are lost unless you explicitly persist them to a volume.

Dockerfile: This is a text file containing instructions for building an image. Each line is a command that Docker executes in order. The most common instructions are FROM (specify the base image), COPY (copy files into the image), RUN (execute commands during build), and CMD (specify the default command when the container starts).

Docker Compose: Modern applications rarely consist of a single service. Your frontend might depend on an API server, which depends on a database, which might need a message queue. Docker Compose lets you define all these services in a single YAML file and manage them as a group. One command starts everything.

Installing Docker

Docker Desktop is available for Windows, macOS, and Linux. Download the installer from the official Docker website and follow the setup wizard. On Linux, you can also install Docker Engine directly through your package manager. After installation, verify it works by opening a terminal and running:

1
2
docker --version
docker run hello-world

The hello-world command pulls a tiny test image and runs it. If you see a welcome message, Docker is set up correctly.

Your First Dockerfile

Let us create a simple Node.js application and containerize it step by step. Start with a basic Express server:

1
2
3
4
5
6
7
8
9
10
11
12
// server.js
const express = require('express');
const app = express();
const port = 3000;

app.get('/', (req, res) => {
res.json({ message: 'Hello from Docker!' });
});

app.listen(port, () => {
console.log(`Server running on port ${port}`);
});

Next, create a package.json:

1
2
3
4
5
6
7
{
"name": "docker-demo",
"version": "1.0.0",
"main": "server.js",
"scripts": { "start": "node server.js" },
"dependencies": { "express": "^4.18.0" }
}

Now the Dockerfile:

1
2
3
4
5
6
7
8
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
USER node
CMD ["node", "server.js"]

Let me explain each instruction. FROM node:18-alpine starts from a lightweight Node.js image based on Alpine Linux, which is only about 5MB. WORKDIR /app creates and switches to /app inside the image. COPY package*.json ./ copies only the package files first — this is intentional. We then run npm ci --only=production to install dependencies. Because Docker caches each layer, copying package files separately means the install step is only re-run when dependencies change, not every time you edit server.js. COPY . . then copies the rest of the application code. EXPOSE 3000 documents that the container listens on port 3000. USER node switches from the default root user to a non-privileged user for security. CMD defines the default command.

Building and Running

Build the image from the directory containing your Dockerfile:

1
docker build -t my-express-app .

The -t flag tags the image with a name. The . tells Docker to use the current directory as the build context. Docker will execute each instruction in order, and you will see output for each step.

Run the container:

1
docker run -p 3000:3000 my-express-app

The -p 3000:3000 flag maps port 3000 on your host to port 3000 in the container. Open http://localhost:3000 in your browser and you should see the JSON response.

Useful Commands

Here are the commands you will use most frequently:

  • docker ps — list running containers
  • docker ps -a — list all containers, including stopped ones
  • docker images — list all local images
  • docker stop <container-id> — stop a running container
  • docker rm <container-id> — remove a stopped container
  • docker rmi <image-id> — remove an image
  • docker logs <container-id> — view container logs
  • docker exec -it <container-id> sh — open a shell inside a running container
  • docker system prune — clean up unused containers, images, and networks

Volumes and Persistent Data

By default, data inside a container is ephemeral. When the container is removed, the data is gone. For data that needs to persist, use volumes:

1
docker run -v /host/path:/container/path my-app

This mounts a directory from your host machine into the container. Any files written to /container/path inside the container are actually stored on the host and survive container restarts and removals. For databases in development, this is essential.

Environment Variables

Pass configuration to containers through environment variables rather than hardcoding values:

1
docker run -e NODE_ENV=production -e DB_HOST=localhost my-app

This keeps your images generic and configurable for different environments. The twelve-factor app methodology recommends this approach for good reason.

Multi-Container Applications with Compose

Here is a realistic example using Docker Compose to run an application with a web server and a database:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
version: '3.8'
services:
web:
build: .
ports:
- '3000:3000'
environment:
- DB_HOST=db
- DB_PORT=5432
depends_on:
- db
db:
image: postgres:15-alpine
environment:
- POSTGRES_USER=app
- POSTGRES_PASSWORD=secret
- POSTGRES_DB=appdb
volumes:
- pgdata:/var/lib/postgresql/data

volumes:
pgdata:

Save this as docker-compose.yml and run docker compose up. Both services start together, networked automatically. The depends_on directive ensures the database starts before the web server. The named volume pgdata persists the database across restarts.

Best Practices

Use specific base image tags. node:18-alpine is better than node:latest. The latest tag changes without warning and can break your build unexpectedly.

Minimize layers. Chain related RUN commands: RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*. This keeps the image smaller and reduces the number of layers.

Use .dockerignore. Create a .dockerignore file to exclude node_modules, .git, and other unnecessary files from the build context. This speeds up builds and prevents accidentally copying secrets into the image.

Run as non-root. Always switch to a non-privileged user with the USER instruction. Running as root inside a container can be a security risk if the container is compromised.

Scan for vulnerabilities. Docker Desktop includes docker scan, which checks your images against known vulnerability databases. Run it before deploying to production.

Debugging Containers When Things Go Wrong

Here’s the thing about Docker — it works beautifully until it doesn’t. And when it doesn’t, the error messages can be frustratingly opaque. After years of banging my head against container issues, here’s what I actually reach for when something breaks.

Hop inside the container. docker exec -it <container> sh is your best friend. Don’t just inspect from the outside — get in there and poke around. Is the config file where you expected it? Are the environment variables actually set the way you think they are? Can you curl localhost:3000 from inside the container itself? I can’t tell you how many times I’ve spent thirty minutes debugging a network issue only to discover the app wasn’t even listening on the port I thought it was. Being inside the container’s filesystem cuts through all the abstraction layers and shows you what’s actually happening.

Check the logs first, always. docker logs --tail 100 <container> gives you the last hundred lines. The -f flag follows in real-time, like tail -f. If your container is crashing on startup and immediately exiting — a surprisingly common pattern — use docker logs <container> right after starting it. The startup output might flash by too fast to catch on screen, but it’ll be in the logs. I’ve debugged entire production incidents just by knowing to look there before touching anything else.

Watch out for zombie containers and dangling images. Stopped containers don’t get removed automatically, and you’d be shocked how fast they pile up during an active development session. Run docker ps -a every now and then — you’ll probably find dozens of stopped containers you forgot about, each eating a bit of disk space. A quick docker container prune cleans them up. Same goes for those <none>:<none> entries in docker images — they’re old layers from rebuilds, and docker image prune handles them.

The port mapping trap. If you can’t reach your container, double-check the order in your -p flag. It’s HOST:CONTAINER, not the other way around. I still get this backwards occasionally and stare at a blank browser tab wondering why nothing is responding. Also, if the host port is already in use by another process, Docker won’t warn you — it’ll just silently fail to bind. Running docker port <container> will show you what actually got mapped.

Layer caching can fool you. Docker caches build layers aggressively, and 95% of the time that’s a blessing — it makes rebuilds lightning fast. But I’ve wasted entire afternoons chasing a bug only to realize Docker was serving a cached layer that didn’t include my latest fix. When something doesn’t make sense, docker build --no-cache -t my-app . and see if the problem disappears. If it does, your cache was stale and you just saved yourself hours of confusion.

An exit code of zero doesn’t mean success. A container that starts and immediately exits with code 0 looks healthy at a glance, but a web server shouldn’t exit at all — it should run until you stop it. If your container keeps exiting cleanly, check whether your CMD or ENTRYPOINT is actually starting a long-running process, or if it’s running a script that finishes and then exits.

None of this is textbook stuff. It’s the kind of thing you learn when something breaks at 11 PM and you’re the one who has to fix it before tomorrow’s demo. Docker’s learning curve isn’t particularly steep, but it has a surprisingly long tail of edge cases. The good news is that once you’ve hit enough of them, you develop an instinct for where to look. And honestly, that instinct is worth more than memorizing every CLI flag.

Docker vs Virtual Machines

Since both provide isolation, it is worth understanding the architectural difference. A virtual machine includes a full guest OS running on top of a hypervisor. Each VM needs its own kernel, system processes, and memory allocation — consuming gigabytes of disk and significant CPU overhead. Starting a VM typically takes 30 seconds to several minutes.

A container shares the host’s kernel and runs as an isolated process with its own file system, networking, and process namespace. Containers start in under a second, consume only the memory the application needs, and pack more densely on the same hardware. A server that runs 5 VMs might run 50 containers comfortably.

The trade-off is isolation strength. VMs provide stronger security isolation because they run a separate kernel. Containers share the kernel, so a kernel vulnerability could potentially affect other containers. For most application deployment scenarios, container-level isolation is sufficient, but this is why you sometimes see containers running inside VMs in high-security environments.

Container Networking Basics

By default, each container gets its own IP address on a private Docker network. Containers on the same network can reach each other by container name, thanks to Docker’s built-in DNS resolution. This is why the Compose example above uses DB_HOST=db — the web container resolves db to the database container’s IP address automatically.

Bridge networks (the default) isolate containers from the host network unless you explicitly publish ports. Host networking removes network isolation entirely — the container uses the host’s network stack directly, which can be useful for high-throughput applications. Overlay networks connect containers across multiple Docker hosts, forming the foundation of Docker Swarm.

For development, the default bridge network with published ports is all you need. As you move toward production, understanding overlay networks and service discovery becomes important.

Learning Path

Master Docker incrementally. Start with building and running single containers. Move on to Docker Compose for multi-service applications. Then explore multi-stage builds, which let you use one image for building and a smaller one for running. Understand networking and volumes in depth — they are the foundation of reliable production deployments. Finally, look into container orchestration with Docker Swarm or Kubernetes, which handle scheduling, scaling, and self-healing across clusters of machines.

Docker’s documentation is excellent, and the community is vast. Almost any problem you encounter has already been solved and documented. Start with the basics, practice regularly, and the rest follows naturally. The investment you make in learning Docker compounds — it is the foundation for almost every modern deployment workflow.


Getting Started with Docker – A Complete Beginner's Guide
https://toongs.org/blog/2025/12/25/01-docker-guide/
Author
Jain Chen
Posted on
December 25, 2025
Licensed under