Learn what Docker is and why developers use it. Understand images vs containers, how Docker differs from a virtual machine, the basic workflow and its limits.
Docker packages an application together with everything it needs to run, the runtime, the libraries, the system tools, the configuration, into a single artifact that behaves the same wherever it runs.
That is the whole pitch, and it addresses a problem every team has had.
An application works on a developer's laptop and fails in staging. The cause is invariably an environment difference: a different Python version, a missing system library, a config file somewhere else, a locale setting nobody thought about.
The traditional fixes were documentation nobody kept current, configuration management that drifted, or a virtual machine image that was tens of gigabytes and slow to build.
Docker's answer is to ship the environment with the application. If it runs in the container on your laptop, it runs in the same container in production, because it is the same container.
The distinction people most often blur, and everything else depends on it.
Images are built in layers, each the result of an instruction in the Dockerfile. Layers are cached and shared. Ten images based on the same Ubuntu layer store that layer once. This is why images are far smaller in practice than their nominal sizes suggest, and why ordering Dockerfile instructions well makes rebuilds fast.
docker pull nginx:1.27 # fetch an image
docker run -d nginx:1.27 # create and start a container from it
docker run -d nginx:1.27 # and another, from the same imageA VM virtualises hardware and runs a full guest operating system. A container shares the host kernel and isolates only the process.
Two consequences worth knowing. Containers cannot run a different kernel. Linux containers on macOS or Windows actually run inside a small Linux VM that Docker Desktop manages, which is why file sharing between host and container is slower on those platforms. And because isolation is weaker than a VM's, running genuinely untrusted code deserves a stronger boundary.
A minimal Dockerfile:
The ordering there is deliberate and worth noticing early. Dependencies are copied and installed before the application code, so a change to your source does not invalidate the cached npm ci layer. Reversing those two lines makes every build reinstall everything, which is the single most common Dockerfile performance mistake.
That Dockerfile expects a few files next to it. None is Docker-specific, but the build fails without them, so here they are in full.
server.js, the application the container runs:
Node already listens on every interface when no host is given, so the explicit 0.0.0.0 changes nothing here. It is written out because the opposite mistake is so common: a server bound to localhost inside a container is reachable only from that container, and no -p mapping will ever reach it.
package.json, which is what COPY package*.json ./ picks up:
Run npm install once before your first build. npm ci installs strictly from package-lock.json and exits with an error if that file is missing, even for an app like this one with no dependencies at all. A single npm install generates the lockfile. Commit it, and every build from then on installs exactly the same versions.
Finally a .dockerignore, which keeps the host's node_modules out of the build context. Without it COPY . . copies your local install into the image, overwriting the one npm ci just made and shipping binaries built for the wrong platform:
Put those four files in one directory and the five commands at the top of this section run start to finish, with curl localhost:8080 returning the JSON greeting.
# 1. Write a Dockerfile describing the image
# 2. Build it
docker build -t myapp:1.0 .
# 3. Run a container
docker run -d -p 8080:3000 --name web myapp:1.0
# 4. Look at what is happening
docker ps
docker logs web
# 5. Clean up
docker stop web && docker rm webDocker Engine is the daemon that builds images and runs containers. The CLI talks to it over a socket, which is why docker commands work against a remote host just as easily as a local one.
A registry stores images. Docker Hub is the default; ECR, GCR, GitHub Container Registry and self-hosted registries all work the same way.
A volume persists data beyond a container's life. Anything written to a container's own filesystem disappears when it is removed, which surprises people exactly once.
A network lets containers reach each other. Containers on a user-defined network resolve each other by name, which is how multi-container applications are wired together.
This is the mental shift that makes Docker click.
A container is not a small server you log into and maintain. It is a process you replace. Configuration comes from environment variables, data lives in volumes or a database, and logs go to stdout, so that destroying and recreating the container loses nothing.
Treating containers as long-lived pets, SSHing in, installing packages, editing files, produces exactly the environment drift Docker exists to eliminate.
Worth stating, because it is frequently applied by default:
You run docker run three times against the same image. How many images and containers now exist?
A Dockerfile copies all source code before running npm ci. Every build reinstalls all dependencies even when only one source file changed. Why?
Find out why teams use Docker: consistent environments, faster deployment, easier scaling and microservices support, with real-world use cases.
Compare Docker containers and virtual machines on architecture, startup time, isolation and security, and learn when to choose one or use both together.
Choose between Docker Desktop and Docker Engine, check the requirements, verify the install with docker run hello-world, and fix the three most common failures.