The official hands-on tutorial. After this lesson, work through it to build and run your first container. docs.docker.com →
"It works on my machine" is the most expensive sentence in software. Your machine has Node 20, your colleague has Node 18, staging has Node 16. Dependencies differ. Environment variables differ. The OS differs. Docker eliminates all of this.
Think of a Docker image like a package.json + node_modules + the Node.js runtime + the OS, all frozen into a single snapshot. A container is a running instance of that snapshot. Anyone who has the image runs your app identically — on any machine, on any cloud.
# Pull an image from Docker Hub
docker pull node:20-alpine
# List local images
docker images
# Build an image from a Dockerfile in current dir
docker build -t myapp:1.0 .
# Tag for a registry
docker tag myapp:1.0 username/myapp:1.0
# Push to Docker Hub
docker push username/myapp:1.0
# Remove an image
docker rmi myapp:1.0
# Run a container (interactive, auto-remove on exit)
docker run -it --rm node:20 node
# Run your app (detached, port mapped)
docker run -d -p 3000:3000 --name myapp myapp:1.0
# List running containers
docker ps
# List all containers (including stopped)
docker ps -a
# View logs
docker logs myapp
docker logs -f myapp # Follow (live)
# Execute a command inside a running container
docker exec -it myapp sh
# Stop / start / remove
docker stop myapp
docker start myapp
docker rm myapp
docker rm -f myapp # Force remove running container
# Remove all stopped containers, unused images, volumes
docker system prune -a
# See disk usage
docker system df
Every line in a Dockerfile creates a layer. Layers are cached. If you only change your app code, Docker reuses all the layers above it (OS, runtime, dependencies) and only rebuilds what changed.
Put the things that change least at the TOP of your Dockerfile, and the things that change most at the BOTTOM. Your app code changes every commit — COPY . . should be near the end. Dependencies change rarely — RUN npm install should come before copying source code.
Containers are isolated. To reach a container from your host machine, you map a host port to a container port:
# -p HOST_PORT:CONTAINER_PORT
docker run -p 3000:3000 myapp # localhost:3000 → container:3000
docker run -p 8080:3000 myapp # localhost:8080 → container:3000
docker run -p 3000:3000 -p 3001:3001 myapp # multiple ports