Beginner? — Docker packages your app with its dependencies into a container so it runs the same everywhere: laptop, CI, and production. This complete guide takes you from what a container is to your first build, Dockerfile, and Docker Compose stack — no prior container experience needed. To create files without memorizing syntax, try our Dockerfile Generator and Docker Compose Generator.
- What it is: container platform — packages app + libs into an isolated, portable filesystem that shares the host kernel (MBs, ms to start, vs VMs' GBs/minutes).
- Mental model: Dockerfile →
docker build→ Image (read-only template, layered) →docker run→ Container (running instance) → Registry (Docker Hub) to share; clientdockertalks to daemondockerd. - 80% loop:
docker build -t myapp .→docker run -d -p 3000:3000 myapp→docker ps / logs -f / exec -it→docker compose up -dfor multi-service stacks. - Two files you need:
Dockerfile(FROM, WORKDIR, COPY, RUN, EXPOSE, CMD) andcompose.yml(services, ports, volumes, networks). Use our Dockerfile Generator to scaffold the first and Docker Compose Generator for the second. - Safety: never COPY
.env, add.dockerignore(node_modules, .git, .env), pin tags (node:20-alpinenotlatest), run as non-root, volumes for data you must keep.
What Is Docker and Why Containers?
Before Docker, "works on my machine" was the norm: your laptop had Node 20, your teammate 18, prod had 16 — same code, different results. Docker fixes that by packaging the app with its runtime, libraries, and system deps into a container that runs identically anywhere Docker runs.
A container is not a lightweight virtual machine, though it's often compared to one. A VM virtualizes hardware and boots a full Guest OS per app (GBs, seconds–minutes to start). A container virtualizes the OS — it shares the host kernel via Linux primitives (namespaces for isolation, cgroups for resource limits) and adds only its filesystem layers on top. Result: MBs, milliseconds to start, many containers per host, isolated yet efficient.
What you gain as a beginner: portability — build once, run on laptop, CI, and prod from the same image; isolation — per-container deps, no conflict when one service needs Python 3.11 and another 3.12; speed — docker run in ms, not VM boot minutes; and distribution — push to a registry like Docker Hub and others pull the same bits. Docs: Docker Get Started and Docker concepts define containers vs images clearly.
Virtual Machines vs Containers — When to Use Which
VMs still matter for strong isolation (running Windows on macOS, untrusted workloads needing hardware isolation) or a different kernel. For most web apps, APIs, and microservices, containers win: same isolation for the app layer with far less overhead. The architecture docs at Docker overview explain the daemon/client/registry picture we cover next.
How Docker Works — The Mental Model in One Diagram
Three parts matter:
- Docker Client — the
dockerCLI you type. It sends REST calls. - Docker Daemon (dockerd) — the background service that builds images and runs containers.
- Registry — a server storing images by
name:tag(Docker Hub, GHCR).docker pull node:20-alpinedownloads,docker push myuser/myapp:1.0uploads.
Two objects you'll create constantly:
- Image — a read-only template built from layered filesystem diffs. Think "class". Created by
docker buildfrom a Dockerfile. - Container — a runnable instance of an image plus a thin writable layer and isolated namespaces. Think "object". Created by
docker run(orcompose upmakes several at once). - Volume / Network — persistent data and virtual networks containers share.
The flow: write a Dockerfile → docker build -t myapp . → image layers cached locally → docker run myapp → container → optionally docker tag myapp myuser/myapp:1.0 && docker push → others docker pull. Reference: Docker Engine and What is an image / What is a container.
Install Docker in 3 Minutes (Windows / macOS / Linux)
Docker Desktop bundles daemon, CLI, Compose, and a GUI for all OSes. Install:
- Windows/macOS: Install Docker Desktop — download, install, start Desktop, confirm systray icon turns green. It includes WSL 2 backend on Windows.
- Linux: Install Docker Engine (or Desktop). Add your user to
dockergroup:sudo usermod -aG docker $USERthen re-login sodocker psneeds nosudo.
Verify: docker --version && docker compose version and docker run hello-world. If Cannot connect to the Docker daemon, Docker Desktop is not running — start it first; on Linux, sudo systemctl start docker. Full verify steps: Docker Desktop docs.
Your First Container — hello-world to Nginx to Your Own App
Three commands prove Docker works before you write any file:
docker run hello-world
# → Hello from Docker! (pulls image → creates container → prints → exits)
docker run -d -p 8080:80 --name web nginx:alpine
docker ps # → web running
# open http://localhost:8080 → "Welcome to nginx!"
docker logs web -f # follow logs, Ctrl+C to detach
docker exec -it web sh # shell inside, type exit to leave
docker stop web && docker rm web
What docker run actually does in order: (1) pull image if not cached, (2) create container from image layers plus a writable thin layer, (3) create isolated network namespace and FS, (4) execute CMD. Flags: -d detach, -p host:container publish port, --name human handle for logs/exec/stop. Docs: docker run reference and Running containers.
The 8 Commands That Cover 80%
docker ps/docker ps -a— running vs all.docker logs <name> -f --tail 100— stream logs.docker exec -it <name> sh— shell in container (bashif image has it).docker build -t myapp .— build image from Dockerfile in current dir.docker run -d -p 3000:3000 --name myapp myapp— create + start.docker stop / rm <name>— stop then remove;docker rm -fdoes both.docker images / rmi— list / delete images.docker compose up -d / logs -f / down— multi-container life-cycle.
Dockerfile — The Recipe That Builds Your Image
A Dockerfile is a text recipe, one instruction per layer. Minimal Node.js example that we'll dissect:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
- FROM — base image.
alpinevariant is small (≈50 MB vs 1 GB for full distro). Pinnode:20-alpine, notnode:latestwhich can surprise you. - WORKDIR /app — sets
cd+mkdirfor following instructions. - COPY package*.json ./ — host files → image. Order matters (next).
- RUN npm ci --only=production — executed at build time to create a layer with deps.
- COPY . . — copy app source after deps so cache hits.
- EXPOSE 3000 — documents the port (doesn't publish;
-pstill needed). - CMD ["npm","start"] — default command at
docker run.
Build → run:
docker build -t myapp:1.0 .
docker run -d -p 3000:3000 --name myapp myapp:1.0
docker logs myapp -f
Build cache trick: copying package*.json + RUN npm ci before COPY . . means a code-only change doesn't invalidate deps layer — rebuilds drop from minutes to seconds. Reverse the order and every keystroke busts cache. Use our Dockerfile Generator to scaffold this correctly for Node, Python, Go, or Java without memorizing syntax — it orders layers, exposes the right port, and sets a non-root USER. Deep specs: Dockerfile overview and Best practices.
Python Example — Same Pattern, Different Runtime
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000"]
Only runtime and install command changed; the layer-order principle is identical. For multi-stage (smaller final image), docs: Multi-stage builds.
Docker Compose — One YAML for App + DB + Cache
Real apps are not one container — they are web + postgres + redis. Without Compose you'd write many docker network create and long docker run --net ... -v ... -p ... commands. Compose collapses them to one file and one command.
# compose.yml at project root
services:
web:
build: .
ports: ["3000:3000"]
depends_on: [db, redis]
env_file: [.env]
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: example
volumes: [pgdata:/var/lib/postgresql/data]
redis:
image: redis:7-alpine
volumes:
pgdata:
docker compose up -d # build (if needed) + create network + start 3 containers
docker compose logs -f # follow all
docker compose down # stop + remove network (keep volumes)
docker compose down -v # also delete pgdata (use with care)
Services discover each other by name: your web code connects to postgres://db:5432 and redis://redis:6379 with no IP wrangling — Compose creates a default bridge network. depends_on orders startup, but the app should still retry DB connects (DB may not be ready instantly). Generate this file without YAML errors using our Docker Compose Generator — pick services, ports, volumes, and it emits valid compose.yml. Spec: Compose docs and Compose file reference.
Volumes, Networks, and .dockerignore — Data That Matters
Containers are ephemeral by design — docker rm web deletes its writable layer. Data you must keep must live outside:
- Named volume —
pgdata:/var/lib/postgresql/datamanaged by Docker; survives container recreates.docker volume lslists it. Back up withdocker run --rm -v pgdata:/data -v $(pwd):/backup alpine tar czf /backup/pgdata.tgz /data. - Bind mount —
-v ./app:/appmaps host directory for live-reload during dev (npm run devinside container reflects host edits).
Networking: bridge (default for run), host (no isolation), and Compose's auto-created network. Rarely needed as a beginner beyond -p and Compose service names; full: Networking overview.
Every repo needs a .dockerignore next to the Dockerfile (like .gitignore) — it tells docker build what never to send to the daemon context:
node_modules
.git
.env
dist
*.log
coverage
Without it, COPY . . ships node_modules/ (heavy, host OS specific) and .env secrets into the image — both slow builds and security risks. Docs: .dockerignore.
5 Beginner Safety Rules
- Never COPY .env into an image — pass secrets via
env_file: [.env]in compose or--env-fileforrun, and add.envto.dockerignore. - Don't run as root — add
RUN addgroup -S appgroup && adduser -S appuser -G appgroup && chown -R appuser /app USER appuser(alpine) orUSER nodefor official node images. - Pin tags —
node:20-alpine,postgres:16, notlatest(latest moves). - Least-privileged ports —
-p 127.0.0.1:3000:3000when you don't need external access. - Scan images —
docker scout quickview(Desktop) surfaces CVEs before you push.
Beginner Troubleshooting — Top 4 Fixes
| Symptom | Fix |
|---|---|
| port is already allocated | docker ps → docker stop <offender> or -p 3001:3000 to remap |
| Cannot connect to the Docker daemon | Start Docker Desktop; Linux: sudo systemctl start docker |
| permission denied | sudo usermod -aG docker $USER then re-login |
| build very slow / no space | docker system df → docker system prune + fix .dockerignore and layer order |
Practice Lab — 10 Minutes From Empty Folder to Stack
mkdir docker-lab && cd docker-lab
# app
echo 'console.log("hello docker")' > app.js
echo '{ "name":"lab","scripts":{"start":"node app.js"}}' > package.json
# Dockerfile (or use our Dockerfile Generator at Toolwasp)
printf 'FROM node:20-alpine
WORKDIR /app
COPY package.json ./
RUN npm install
COPY . .
CMD ["npm","start"]
' > Dockerfile
printf 'node_modules
.git
.env
' > .dockerignore
docker build -t lab:1.0 .
docker run --rm lab:1.0 # → hello docker
# compose (or use our Compose Generator at Toolwasp)
printf 'services:
app:
build: .
ports: ["3000:3000"]
' > compose.yml
docker compose up -d && docker compose logs -f
docker compose down
What you just practiced: bake → ship → run loop, plus .dockerignore and compose up/down. Repeat with the compose generator to add Postgres+Redis and see service discovery (app → db) without manual networking. Interactive follow-ups: Docker workshop (101).
How to Keep Learning — Next Steps
Once build/run/ps/logs/exec and compose up/down feel natural, deepen:
- Images: Best practices (multi-stage, USER, cache), then Multi-stage builds to shrink images.
- Compose: Compose file — healthchecks, depends_on with
condition: service_healthy. - Registry:
docker tag lab:1.0 myuser/lab:1.0 && docker push myuser/lab:1.0— thendocker pullanywhere. - Tooling: explore our full Docker tools collection — Dockerfile and Compose generators plus utilities for env, cron, and image inspection — so you generate correct files instead of debugging YAML indentation.
Docker rewards small images (alpine base, correct COPY order, .dockerignore) and one concern per container. Do those two habits and Compose will carry you remarkably far before you need Kubernetes.
Layers, Cache, and Multi-Stage — Why Small Images Matter
Every Dockerfile instruction creates a read-only layer; docker build stacks them and hashes each for cache. Change one layer, and Docker rebuilds that layer and all after it — but reuses unchanged earlier layers. That's why ordering COPY package*.json + RUN npm ci before COPY . . makes code-only edits fast: deps layer hits cache and only the last copy rebuilds.
Inspect layers yourself: docker history myapp:1.0 shows each instruction, size, and creation time. docker inspect myapp:1.0 shows config, env, and entrypoint. This is how you spot bloat — e.g., copying node_modules from the host adds 300 MB that should never be in an image when the image already npm ci's inside.
For production, shrink further with multi-stage builds — build in one image with compilers, copy only the artifact to a minimal final image:
# Stage 1: build
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build # e.g., produces /app/dist
# Stage 2: run — only dist + production deps
FROM node:20-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY package*.json ./
RUN npm ci --only=production
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]
Result: final image has no source, no devDependencies, no build tools — often 60–80% smaller. Docs at Multi-stage builds. Beginners can defer this until images feel large, but understanding it explains why alpine and pinned tags matter.
Running Containers Well — Logs, Limits, and Health
Once a container runs, three habits keep it observable:
- Logs —
docker logs myapp --tail 100 -ffollows stdout/stderr. Add--timestampswhen debugging ordering issues. For Compose,docker compose logs -f webtails one service. If logs fill disk, set rotation in daemon:"log-opts": {"max-size":"10m","max-file":"3"}. - Resource limits — containers share the host, so cap runaway:
docker run --memory 512m --cpus 1 myapp. In Compose:deploy.resources.limits: {memory: 512M, cpus: '1.0'}(or legacymem_limit). Check usage withdocker stats. - Healthchecks — declare how Docker knows your app is up:
HEALTHCHECK CMD curl -f http://localhost:3000/health || exit 1in Dockerfile, or in Compose. Thendocker psshows(healthy)vs(unhealthy)instead of justUp.
Also, treat containers as cattle, not pets: if a container behaves oddly, docker rm -f myapp && docker run ... recreates it cleanly from the image. That's the benefit of immutable images + volumes for only the data you truly persist. Reference: docker run limits and Compose healthcheck.
From localhost to Registry — Sharing Your Image
The same image you built locally can run anywhere once pushed to a registry:
docker tag lab:1.0 docker.io/myuser/lab:1.0
docker login # once, with Docker Hub PAT
docker push myuser/lab:1.0
# on any other machine:
docker pull myuser/lab:1.0
docker run -d -p 3000:3000 myuser/lab:1.0
Tag is just an alias (name:tag), not a copy. Use semantic tags: 1.0, 1.0.3, latest only as an extra alias, not the sole tag — otherwise docker pull is non-deterministic. Private registries (GHCR, ECR, GCR) work identically; only the registry hostname changes. Guide: Docker Hub repos.
Security for Beginners — Minimal, Honest Checklist
Beginners don't need hardening guides yet, but avoid the most common pitfalls that ship secrets or bloat images:
- Never copy secrets at build.
COPY .env .bakes the value into a layer anyone with the image candocker historyordocker save | strings. Useenv_file:in Compose ordocker run --env-file .envat runtime, and list.envin both.gitignoreand.dockerignore. - Run as non-root. Official
node:20-alpineandpython:3.11-slimimages provide anode/ non-root user. AddUSER nodeafterCOPY+RUN chownso a container breakout doesn't equal host root. Reference: USER best practice. - Prefer distroless or alpine for final stage. A
node:20-alpinefinal stage is ~150 MB vs ~1 GB for fullnode:20. Smaller surface, fewer CVEs, faster pushes. - Scan before you ship.
docker scout cves myapp:1.0(Desktop) or Hub's vulnerability scan flags known CVEs in base layers so youdocker pullpatched tags (node:20-alpinemoves with patches when you pin minor, notlatest).
When you're ready, the Docker security docs cover roots, capabilities, and read-only filesystems (read_only: true in Compose) — but the four items above prevent most beginner incidents.
Frequently Asked Questions
What is Docker in simple terms?
Docker is a platform that builds and runs containers — lightweight, isolated packages of app + dependencies that share the host kernel and run the same anywhere Docker is installed.
What's the difference between an image and a container?
An image is a read-only layered template (like a class), built from a Dockerfile. A container is a running instance of an image (like an object) with a writable layer and isolated filesystem/network. Many containers can run from one image.
Is Docker a virtual machine?
No. VMs virtualize hardware and boot a full Guest OS per app (GBs, slow). Containers share the host kernel via namespaces/cgroups (MBs, ms start). Need a different OS kernel or hardware isolation → use a VM; need app isolation → use containers.
Do I need Docker Desktop?
Desktop is the easiest on Windows/macOS (includes daemon, CLI, Compose, GUI). On Linux you can run Engine directly, but Desktop still adds a friendly dashboard and extensions. See Desktop docs.
What is Docker Compose?
A tool that defines multi-container apps (web, DB, cache, volumes, networks) in one compose.yml and manages them with docker compose up -d / logs -f / down. Without it you'd need many docker run flags.
What is .dockerignore and why does it matter?
A file listing paths never to send to the Docker daemon during docker build (e.g., node_modules, .git, .env). It makes builds faster, smaller, and avoids copying secrets into images. See .dockerignore docs.