HomeProjectsBlogContact

Docker for Beginners: What It Solves and How to Start

Docker sounds complicated until you see the problem it solves. Images, containers, Dockerfiles and Compose explained simply, with a working Node.js example you can adapt to any stack.

Docker for Beginners: What It Solves and How to Start

"It works on my machine" is the most famous sentence in software for a reason. Your laptop has Node 20, a teammate has Node 18, the server has something else, and a library behaves slightly differently on each. Docker exists to make that sentence obsolete.

This guide skips the theory overload and focuses on what you actually need to start.

The problem Docker solves

An app depends on more than its code: a language runtime, system libraries, environment variables, specific versions of everything. Docker packages all of that into a single unit that runs the same way on any machine with Docker installed.

You also get a bonus that's arguably even more useful day to day: you can run databases and services locally without installing them.

docker run -d --name pg -e POSTGRES_PASSWORD=secret -p 5432:5432 postgres:16

One command, and you have Postgres running. Delete the container when you're done and your system stays clean.

Four words you need to know

  • Image: a read-only template containing your app and everything it needs. Like a class.
  • Container: a running instance of an image. Like an object. You can run many containers from one image.
  • Dockerfile: the recipe for building an image.
  • Registry: where images are stored and shared. Docker Hub is the default; GitHub and cloud providers have their own.

Containers aren't virtual machines. They share the host's operating system kernel, so they start in seconds and use far less memory than a VM.

Your first Dockerfile

Here's a solid starting point for a Node.js app:

FROM node:20-alpine

WORKDIR /app

# Copy dependency files first, so this layer is cached
COPY package*.json ./
RUN npm ci --omit=dev

# Then copy the rest of the source
COPY . .

ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "server.js"]

Build and run it:

docker build -t my-app .
docker run -p 3000:3000 --env-file .env my-app

Visit http://localhost:3000. That's it: your app, in a container.

Why the order of lines matters

Docker builds images in layers, one per instruction, and caches each layer. If a layer hasn't changed, Docker reuses it.

That's why we copy package.json and install dependencies before copying the source code. Change a line of your app and Docker reuses the cached dependency layer, so the rebuild takes seconds instead of minutes. Copy everything first and every code change reinstalls all your packages.

Don't forget .dockerignore

Like .gitignore, but for the build context:

node_modules
.git
.env
dist
*.log

This keeps images small, builds fast, and stops your local .env and node_modules from sneaking into the image. Secrets should be passed at runtime, never baked into images.

Docker Compose: your whole stack in one file

Real apps have more than one piece. Compose lets you define and run them together:

# docker-compose.yml
services:
  api:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://postgres:secret@db:5432/app
    depends_on:
      - db

  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: app
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:
docker compose up

Notice the database host is db, the service name. Containers in the same Compose project can reach each other by name. The pgdata volume keeps database data around when containers are removed. Without a volume, data disappears with the container.

New teammate onboarding becomes: clone the repo, run docker compose up.

Commands you'll use constantly

Command What it does
docker ps List running containers
docker ps -a Include stopped ones
docker logs -f <name> Follow a container's logs
docker exec -it <name> sh Open a shell inside a container
docker compose up -d Start services in the background
docker compose down Stop and remove services (volumes kept)
docker images List local images
docker system prune Clean up unused data (read the prompt first)

Beginner mistakes to avoid

  • Using latest tags. node:latest changes under you. Pin versions like node:20-alpine for reproducible builds.
  • Huge images. Use slim or Alpine base images, .dockerignore, and multi-stage builds for compiled apps (build in one stage, copy only the output into a small final stage).
  • Baking secrets into images. Anyone with the image can read them. Use runtime environment variables. (Config best practices.)
  • Storing data inside containers. Containers are disposable. Use volumes or a managed database.
  • Binding to localhost inside the container. Your app must listen on 0.0.0.0, or it won't be reachable from outside the container.

Do you need Docker in production?

Not always. Platforms like Vercel and Netlify build your code without Docker. But a Dockerfile makes your app portable: it can run on container platforms, a VPS, Kubernetes or a free tier somewhere else when plans change. (Free hosting options.)

Even if you only ever use Docker for local databases, it's worth learning.

FAQ

What is the difference between Docker and a virtual machine?

A VM virtualises an entire computer with its own operating system. A container shares the host's kernel and isolates only the app and its dependencies, so it's much lighter and faster to start.

Is Docker hard to learn?

The basics, meaning running images, writing a simple Dockerfile and using Compose, take an afternoon. Advanced topics like networking, multi-stage builds and orchestration can come later, as you need them.

Should I use Docker for local development?

It's especially useful for running databases, caches and other services without installing them, and for matching production environments. Many developers run their app code directly and only containerise the services around it.

Keep reading

More from the workbench

All articles →