Page 1 of 2
Imagine you write an app on your laptop. It works perfectly. You hand it to a colleague, or push it to a server, and it breaks - a library is a different version, a config file is missing, something about "their machine" is just different from yours. This is the single most common source of wasted time in software, and it has a name: "it works on my machine."
Docker's whole purpose is to make that problem disappear. Instead of shipping just your code and hoping the destination machine has everything else it needs, you package your app together with everything it needs to run - the exact runtime, the exact libraries, the exact configuration - into one self-contained unit called a container. That container runs the same way on your laptop, your colleague's laptop, and a server in a data center, because it's not relying on whatever happens to already be installed there.
The analogy the whole industry uses: a shipping container. Before standardized shipping containers existed, loading a ship meant handling every crate, barrel, and sack differently. Once everything got packed into identical steel boxes, it didn't matter what was inside - any crane, any truck, any ship could move any container the same way. Docker does that for software. It doesn't matter what your app is written in or what it needs - once it's in a container, any machine that can run Docker can run it, the same way, every time.
Two words get used constantly in Docker, and mixing them up is the most common beginner confusion: image and container.
An image is a read-only template - a snapshot of everything your app needs: the code, the runtime, the libraries, the filesystem layout. Think of it as a blueprint, or a recipe. It doesn't do anything by itself; it just describes what should exist.
A container is a running instance made from that image - the actual thing that's alive and doing work. Think of the image as the recipe and the container as the meal you actually cooked from it. You can make many meals from the same recipe, and in the same way, you can run many containers from the same image, each one an independent, isolated process.
docker images # list the image "blueprints" you have downloaded/built
docker ps # list containers currently running (the "meals" currently being served)
docker ps -a # list ALL containers, including ones that have stopped
A Dockerfile is a plain text file containing step-by-step instructions for building an image. You write it once; Docker follows it every time you build.
FROM node:20-slim
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]
Reading it top to bottom:
cd /app before running any other command.That distinction - RUN happens at build time, CMD happens at container start time - trips up almost everyone at first. Keep it straight and a lot of Docker confusion disappears.
Once you have a Dockerfile, two commands take you from "just a text file" to "an actual running app":
docker build -t my-app .
# -t my-app gives the image a name you can refer to later
# . tells Docker "the Dockerfile and files to build from are in this folder"
docker run my-app
# starts a new container from the my-app image
A few commands you'll use constantly once something is running:
docker ps # what's currently running
docker stop <container-id> # stop a running container (gracefully)
docker rm <container-id> # remove a stopped container
docker rmi my-app # remove an image you no longer need
Containers are meant to be disposable. If something's wrong, the normal move isn't to fix a running container - it's to fix the Dockerfile and build a fresh one.
When something isn't working, two commands answer almost every "what's going on in there?" question:
docker logs <container-id> # see everything the app has printed out
docker logs -f <container-id> # keep watching new output live, as it happens
docker exec -it <container-id> /bin/sh # open an actual shell INSIDE the running container
docker exec is especially useful for poking around - checking whether a file actually got copied in, whether an environment variable is set the way you expect, or just confirming the container's filesystem looks the way you think it does.
By default, a container is isolated - nothing outside it can reach whatever's running inside, even if your app is happily listening on a port. You have to explicitly tell Docker to open a door between the outside world and the container.
docker run -p 8080:3000 my-app
# 8080:3000 means "traffic arriving on port 8080 of my machine gets forwarded to port 3000 inside the container"
The two numbers don't have to match, and mixing them up is a very common early mistake. The first number is what you'll actually type into your browser; the second number is whatever port your app is listening on inside the container.
Real scenario-based DevOps questions, hands-on practice, and clear explanations for every answer.