Docker for Developers: Containers, Images, and the Commands You Actually Use
What Docker containers actually are, how images and layers work, the Docker commands that matter in daily development, and how to write a Dockerfile that does not waste build time.
Containers vs virtual machines: the distinction that matters
A virtual machine emulates a complete computer, including a full operating system kernel. A container shares the host OS kernel and only isolates the filesystem, process tree, network interfaces, and a few other namespaces. The practical result: containers start in milliseconds instead of minutes, use megabytes of memory instead of gigabytes, and can run hundreds of instances on a machine that might host a handful of VMs.
Docker is not the only container runtime, but it standardised the developer experience for building and running containers to the point where "Docker" is often used interchangeably with "containers." The Open Container Initiative (OCI) now maintains the image and runtime specs, so Docker images run on Kubernetes, Podman, and other runtimes without changes.
Images and layers: how Docker avoids redundant downloads
A Docker image is a read-only stack of filesystem layers. Each instruction in a Dockerfile (FROM, RUN, COPY, etc.) creates a new layer. When you pull an image, Docker downloads only the layers you do not already have — because layers are shared across images. A Node.js application image and a Python image both built onubuntu:22.04 share the Ubuntu layer; Docker stores it once.
# Each line in a Dockerfile = one layer FROM node:20-alpine # base layer (Alpine Linux + Node 20) WORKDIR /app # creates /app directory in the layer COPY package*.json ./ # new layer: only package files RUN npm ci # new layer: node_modules installed COPY . . # new layer: application source EXPOSE 3000 CMD ["node", "server.js"] # Why COPY package*.json before COPY . . matters: # Docker caches layers. If package.json hasn't changed, # the npm ci layer is reused from cache — no re-download. # Without this split, every code change invalidates npm ci.
The commands that matter daily
# Build an image from Dockerfile in current directory docker build -t myapp:latest . docker build -t myapp:1.0 --no-cache . # ignore cache, rebuild all layers # Run a container docker run myapp:latest docker run -p 3000:3000 myapp:latest # map host:container port docker run -d myapp:latest # detached (background) docker run -it myapp:latest sh # interactive shell docker run --rm myapp:latest # auto-remove container when done docker run -e DATABASE_URL=postgres://... myapp:latest # inject env var # List running containers docker ps docker ps -a # all, including stopped # Logs docker logs <container-id> docker logs -f <container-id> # follow (tail -f equivalent) docker logs --tail 100 <container-id> # last 100 lines # Execute a command in a running container docker exec -it <container-id> sh docker exec <container-id> env # print env vars # Stop and remove docker stop <container-id> docker rm <container-id> docker rm -f <container-id> # force-stop and remove # Images docker images # list local images docker rmi myapp:latest # remove image docker pull node:20-alpine # pull without running
Docker Compose: running multi-container applications
Most applications need more than one container — an API server, a database, a cache, a background worker. Docker Compose defines all of these in a singledocker-compose.yml file and manages them as a unit.
# docker-compose.yml
services:
api:
build: . # build from local Dockerfile
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://user:pass@db/myapp
- REDIS_URL=redis://cache:6379
depends_on:
db:
condition: service_healthy # wait for db to be ready
volumes:
- .:/app # mount source for hot reload
- /app/node_modules # but keep container's node_modules
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: myapp
volumes:
- postgres-data:/var/lib/postgresql/data # persist data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user"]
interval: 5s
timeout: 5s
retries: 5
cache:
image: redis:7-alpine
volumes:
postgres-data:
# Commands
docker compose up # start everything, foreground
docker compose up -d # start in background
docker compose down # stop and remove containers
docker compose down -v # also remove volumes (wipe DB)
docker compose logs -f api # follow logs for the api service
docker compose exec api sh # shell into running api container
docker compose restart api # restart one serviceWriting efficient Dockerfiles
Slow builds and large images are the two most common Docker pain points. Both are avoidable with the right Dockerfile structure.
Order layers from least to most frequently changing. Base image first, then package manifests and dependency installation, then application source last. A code change only invalidates the last layers; dependency installation is cached.
Use multi-stage builds to keep production images small. A build stage can have compilers, dev tools, and full build dependencies. The final stage copies only the compiled artifacts, leaving build tools out of the production image.
# Multi-stage build for a Node.js app FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build # compile TypeScript, bundle, etc. FROM node:20-alpine AS production WORKDIR /app COPY package*.json ./ RUN npm ci --omit=dev # production deps only COPY --from=builder /app/dist ./dist # only compiled output USER node # never run as root EXPOSE 3000 CMD ["node", "dist/server.js"] # Result: production image has no TypeScript compiler, # no devDependencies, no source files — typically 60-80% smaller
Use a .dockerignore file. Like .gitignore, it prevents unnecessary files from being sent to the Docker build context — which speeds up builds and prevents accidentally including secrets or large test fixtures in the image.
# .dockerignore node_modules # container installs its own .git # not needed in image .env # never include secrets *.log dist # will be rebuilt inside container coverage .nyc_output README.md
Inspecting and debugging containers
# Inspect container details (ports, env, network, mounts) docker inspect <container-id> # Check resource usage docker stats # live resource usage for all containers docker stats <container-id> # single container # Diff: what changed from the base image docker diff <container-id> # Check image layers and their sizes docker history myapp:latest # Clean up unused resources (reclaim disk space) docker system prune # unused containers, networks, dangling images docker system prune -a # also remove unused images (not just dangling) docker system prune --volumes # also remove unused volumes (removes data!) # Check disk usage docker system df