How to Use Docker: Containers for Beginners
Key Takeaways
- ▸Docker ranked #1 in the Stack Overflow 2025 Developer Survey — 71.1% of professional developers use it, a 17-point year-over-year increase.
- ▸A container is not a VM: it shares the host kernel, starts in milliseconds, and uses ~10x less memory than a VM running the same app.
- ▸Images are layered and immutable; containers are running instances. Understanding this distinction prevents 80% of beginner confusion.
- ▸Docker Compose is the standard for local multi-service development — one YAML file replaces a page of docker run commands.
- ▸The Docker container market reached $6.12 billion in 2025 and is projected to hit $16.32 billion by 2030 at 21.67% CAGR (MarketsandMarkets 2025).
The Biggest Misconception About Docker
"Docker is just a lightweight VM." This is the most common misconception, and it causes endless confusion about what Docker actually does and when to use it.
A virtual machine emulates hardware, boots a full operating system kernel, and consumes gigabytes of RAM before your application even starts. A Docker container does none of this. It is a process with an isolated filesystem, network, and process namespace — sharing the host OS kernel directly. The distinction is not academic:
| Property | Virtual Machine | Docker Container |
|---|---|---|
| Boot time | 30–60 seconds | ~50 milliseconds |
| Disk footprint | 5–20 GB per VM | 50–500 MB per image |
| RAM overhead | 512 MB – 2 GB (OS) | ~5 MB (shared kernel) |
| Kernel isolation | Complete (separate kernel) | Shared host kernel |
| Run multiple instances | Expensive (GB RAM each) | Cheap (MB RAM each) |
| Cross-OS support | Any OS on any host | Linux containers on Linux (Docker Desktop bridges on Mac/Windows) |
This performance gap explains why Docker hit #1 in the Stack Overflow 2025 Developer Survey — the first time Docker topped the tools category — with 71.1% professional developer adoption, up 17 points year-over-year. When your CI pipeline can spin up a fresh environment in 50ms instead of 60 seconds, iteration speed fundamentally changes.
Core Concepts: Images, Containers, and Layers
Docker's mental model has three building blocks. Confuse them and nothing makes sense. Understand them and everything clicks.
Images: The Blueprint
A Docker image is a read-only, layered filesystem. Each layer represents a set of filesystem changes — a new file, a deleted file, a modified permission. Layers are content-addressed by SHA256 hash and shared across images. If two images both derive from node:20-alpine, they share that base layer on disk and in memory. Docker Hub serves over 13 billion container image pulls per month, according to Docker's 2025 State of Application Development Report.
You define an image with a Dockerfile — a plain text file of instructions. Here is a production-ready Dockerfile for a Node.js application using multi-stage builds to keep the final image small:
# Stage 1: install dependencies (separate stage keeps build tools out of prod image)
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production
# Stage 2: production image — only runtime, no devDependencies or npm cache
FROM node:20-alpine AS runner
WORKDIR /app
# Run as non-root user — critical security practice
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
COPY --from=deps /app/node_modules ./node_modules
COPY --chown=appuser:appgroup . .
# EXPOSE documents the port — doesn't actually publish it
EXPOSE 3000
# CMD is the default; can be overridden at runtime
CMD ["node", "server.js"]Containers: The Running Instance
A container is a running process spawned from an image, with an additional writable layer on top. When you write files inside a container, they go into this writable layer — which disappears when the container is removed. This is why databases need volumes (covered later). Multiple containers can run from the same image simultaneously; they share the read-only image layers but each has its own isolated writable layer.
The Registry: Docker Hub and Beyond
Images are stored in registries. Docker Hub is the default public registry — it hosts official images for Postgres, Redis, Nginx, Node.js, and thousands more. Pull an image with docker pull, or reference it directly in your Dockerfile's FROM instruction. Production systems typically use private registries: AWS ECR, Google Artifact Registry, or GitHub Container Registry (ghcr.io), which keep your images private and co-located with your infrastructure.
Installing Docker and Running Your First Container
Docker Desktop (Mac and Windows) bundles the Docker daemon, CLI, and a lightweight Linux VM. On Linux, install Docker Engine directly — no VM needed because the kernel is native.
# Linux: install Docker Engine (Ubuntu/Debian)
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER # add yourself to the docker group
newgrp docker # activate without logging out
# Verify installation
docker version
docker info
# Run your first container — the classic Hello World
docker run hello-world
# Output: "Hello from Docker! This message shows that your installation
# appears to be working correctly."
# More useful: run an interactive Alpine Linux shell (30 MB image)
docker run -it --rm alpine sh
# -it = interactive TTY
# --rm = remove container after exit
# Now you are inside a container — try: cat /etc/os-releaseThe docker run command does four things in sequence: checks local image cache, pulls from Docker Hub if not cached, creates a new container from the image, and starts it. The entire sequence for hello-world completes in under a second on any modern machine.
Essential Docker Commands Every Developer Uses Daily
A handful of commands covers 90% of day-to-day Docker work. The rest exists for edge cases you will look up when needed.
# ─── IMAGES ──────────────────────────────────────────────────
docker images # list local images
docker pull nginx:1.27-alpine # pull specific version (ALWAYS pin versions in prod)
docker build -t myapp:1.0 . # build from Dockerfile in current directory
docker rmi myapp:1.0 # remove image
docker image prune # remove dangling (untagged) images
# ─── CONTAINERS ───────────────────────────────────────────────
docker ps # running containers
docker ps -a # all containers (including stopped)
docker run -d -p 8080:80 nginx # run nginx, detached, port 8080 → 80
docker stop <name_or_id> # graceful stop (SIGTERM)
docker kill <name_or_id> # immediate stop (SIGKILL)
docker rm <name_or_id> # remove stopped container
docker rm -f <name_or_id> # force remove running container
# ─── DEBUGGING ────────────────────────────────────────────────
docker logs <name_or_id> # stdout/stderr from container
docker logs -f <name_or_id> # follow logs (like tail -f)
docker exec -it <name> sh # open shell inside running container
docker inspect <name_or_id> # full JSON metadata (ports, mounts, env vars)
docker stats # live CPU/RAM/network usage per container
# ─── CLEANUP ──────────────────────────────────────────────────
docker system prune # remove stopped containers + dangling images
docker system prune -a # also remove unused images (reclaims GB of disk)The single most useful debugging command is docker exec -it <name> sh. It opens an interactive shell inside a running container so you can inspect the filesystem, check environment variables, and run commands as if you SSH'd in.
Ports, Volumes, and Environment Variables
Three mechanisms control how containers interact with the outside world.
Port Mapping: Exposing Container Ports
Containers have their own network namespace. To access a service inside a container from your host, you map a host port to a container port with -p host:container.
# Map host port 5432 to container port 5432 (PostgreSQL)
docker run -d --name mypostgres -p 5432:5432 -e POSTGRES_PASSWORD=secret -e POSTGRES_DB=myapp postgres:16-alpine
# Now connect from host: psql -h localhost -p 5432 -U postgres myapp
# Multiple ports: run a dev Node.js server on 3000 + debugger on 9229
docker run -d -p 3000:3000 -p 9229:9229 myapp:devVolumes: Persisting Data Across Container Restarts
Containers are ephemeral — their writable layer vanishes on removal. For persistent data (databases, uploads, logs), use volumes. Docker manages named volumes; bind mounts map a specific host path.
# Named volume — Docker manages it (preferred for databases)
docker run -d --name mypostgres -v pgdata:/var/lib/postgresql/data -e POSTGRES_PASSWORD=secret postgres:16-alpine
# pgdata is stored in /var/lib/docker/volumes/pgdata/
# Survives container removal — use docker volume rm pgdata to delete
# Bind mount — maps host directory to container path (great for dev hot-reload)
docker run -d -v $(pwd)/src:/app/src -p 3000:3000 myapp:dev
# Changes to ./src on your host are instantly visible inside the containerEnvironment Variables: Configuration Without Rebuilding
Never bake secrets into Docker images. Pass configuration via environment variables using -e flags or an --env-file pointing to a .env file. This is the twelve-factor app principle applied to containers: the same image runs in dev, staging, and production with different environment variables.
# Using --env-file (never commit .env to git)
docker run -d --env-file .env.production -p 3000:3000 myapp:latest
# .env.production contents:
# NODE_ENV=production
# DATABASE_URL=postgres://user:pass@db:5432/myapp
# SECRET_KEY=...
# View environment inside a running container
docker exec mycontainer envDocker Compose: Multi-Container Local Development
Running a single container is useful. Running a web server, database, cache, and message queue together — with networking, volumes, and startup order — is where Docker Compose shines. According to Docker's 2025 State of App Development Report, 64% of developers now use non-local primary development environments, and Docker Compose is the dominant tool for orchestrating those local environments.
Here is a production-representative docker-compose.yml for a Node.js API with PostgreSQL and Redis:
# docker-compose.yml
services:
api:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=development
- DATABASE_URL=postgres://postgres:secret@db:5432/myapp
- REDIS_URL=redis://cache:6379
volumes:
- ./src:/app/src # hot-reload: changes sync to container
depends_on:
db:
condition: service_healthy # wait for postgres to be ready
cache:
condition: service_started
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql # seed data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
cache:
image: redis:7-alpine
command: redis-server --save 60 1 --loglevel warning
volumes:
pgdata:
driver: local# Essential Compose commands
docker compose up # start all services (attached)
docker compose up -d # start detached (background)
docker compose down # stop and remove containers (volumes preserved)
docker compose down -v # also remove volumes (fresh slate)
docker compose logs -f api # follow logs for the 'api' service
docker compose exec api sh # shell into running 'api' container
docker compose ps # status of all services
docker compose build --no-cache # force rebuild all imagesNotice the healthcheck on the database service and condition: service_healthy on the API's dependency. Without this, the API container starts before PostgreSQL accepts connections, causing connection errors on the first few requests. This is the source of probably 30% of "Docker Compose doesn't work" complaints. For a complete deep-dive into local dev workflows, see our Docker Compose local development guide.
Building Smaller, Faster Images
Image size directly impacts pull time in CI, startup time in production, and your registry storage bill. Five techniques that have an outsized impact:
1. Use Alpine or Distroless Base Images
The official node:20 image is 1.1 GB. node:20-alpine is 68 MB. Google's Distroless images strip out the shell, package manager, and everything except the runtime — a Node.js Distroless image is ~40 MB and eliminates most shell-injection attack surfaces.
2. Multi-Stage Builds
Shown in the Dockerfile above: build in one stage, copy only artifacts to the final stage. A TypeScript project needs the compiler and all devDependencies to build; the production image needs only the compiled JavaScript and production dependencies. Multi-stage builds give you the tooling during build without the bloat at runtime.
3. Layer Cache Ordering
Docker rebuilds layers from the first changed instruction onward. Copy your package.json and run npm ci before copying source code. This way, the expensive dependency installation layer is cached and reused across builds unless dependencies actually change — typically cutting CI build time by 60–80%.
4. .dockerignore
# .dockerignore — exclude from build context
node_modules
.git
.env*
*.log
dist
coverage
.DS_StoreWithout .dockerignore, node_modules (often 300–500 MB) gets transferred to the Docker daemon on every build even if you later overwrite it in the container. This wastes time and risks bundling platform-specific native binaries into the image.
5. Pin Image Versions
FROM node:latest in production is a supply chain vulnerability waiting to happen. A new tag can introduce breaking changes, security patches that change behavior, or subtle OS differences. Pin to a specific version: FROM node:20.14.0-alpine3.20. Update deliberately, test, then advance the pin.
Container Networking: How Containers Talk to Each Other
Docker creates a virtual network by default. Containers on the same Docker network can reach each other by service name — which is how DATABASE_URL=postgres://postgres:secret@db:5432 works in Compose: db is the service name, resolved by Docker's internal DNS.
# Docker Compose creates a default bridge network named <project>_default
# All services on it can reach each other by service name
# Manual networking (without Compose)
docker network create mynet
docker run -d --name db --network mynet postgres:16-alpine
docker run -d --name api --network mynet -e DB_HOST=db myapp:latest
# 'api' can reach 'db' at hostname 'db' on port 5432
# Inspect network
docker network ls
docker network inspect mynet
# Host networking (Linux only) — container uses host network stack directly
# Best for high-performance scenarios or tools that need to see all host traffic
docker run --network host myappDocker in CI/CD Pipelines
Docker's reproducibility is the reason it dominates CI. A container is the same in development, CI, and production — no "works on my machine" class of bugs. Here is a GitHub Actions workflow that builds, tests, and pushes an image:
# .github/workflows/docker.yml
name: Build and Push Docker Image
on:
push:
branches: [main]
tags: ['v*']
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 # enables BuildKit and multi-platform builds
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha # GitHub Actions cache for layer reuse
cache-to: type=gha,mode=maxThe cache-from: type=gha line is the difference between a 5-minute and 45-second CI build. It stores Docker layer cache in GitHub Actions cache storage, so subsequent builds only rebuild changed layers. For a complete CI/CD pipeline guide, including testing stages and deployment strategies, see our dedicated article.
Docker Security: The Non-Negotiables
The Docker ecosystem has a security problem: too many tutorials run containers as root, expose the Docker socket, and pull unverified images. Per Snyk's 2025 Open Source Security Report, 84% of Docker images on Docker Hub contain at least one medium-severity vulnerability. Four practices eliminate the most critical exposure:
- 01
Run as non-root user
Add
USER appuserto your Dockerfile. A container running as root can escape to root on the host under certain conditions. This is the single highest-impact change for container security. - 02
Never mount the Docker socket
Mounting
-v /var/run/docker.sock:/var/run/docker.sockgives the container full Docker daemon access — equivalent to host root. Use Docker-in-Docker (DinD) with TLS or rootless Docker instead. - 03
Scan images before pushing
Run
docker scout cves myimage(Docker Scout, formerly Snyk integration) ortrivy image myimage(Trivy, open source) to surface CVEs in your base image and dependencies before they reach production. - 04
Use read-only filesystems where possible
Run containers with
--read-onlyto prevent malicious code from writing to the container filesystem. Mounttmpfsfor temporary files:--tmpfs /tmp.
For broader container best practices including resource limits, capability dropping, and seccomp profiles, see our Docker container best practices guide.
Frequently Asked Questions
What is the difference between a Docker image and a container?▾
Is Docker the same as a virtual machine?▾
What is Docker Compose and when should I use it?▾
What is a Dockerfile?▾
How do I persist data in Docker containers?▾
What is the difference between CMD and ENTRYPOINT?▾
How do I see running Docker containers and stop them?▾
Developer Tools for Docker Workflows
Docker configs use YAML and JSON extensively. BytePane's free tools help you validate and inspect them:
- JSON Formatter — validate and pretty-print Docker inspect output and config JSON
- YAML Validator — catch docker-compose.yml syntax errors before running docker compose up
- Base64 Encoder/Decoder — decode Docker registry credentials and secret values
Related Articles
Docker Compose for Local Dev
Complete guide to multi-service local environments with health checks and hot-reload.
Docker Container Best Practices
Security hardening, resource limits, and production-ready container configuration.
CI/CD Pipeline Guide
Build Docker-based CI/CD pipelines with GitHub Actions, testing, and deployment automation.
Environment Variables Guide
Manage secrets and config across dev, staging, and production with Docker.