Introduction
"It works on my machine" is the classic developer problem. Your app works locally but fails on a server, because the server has a different version of Node, a missing library, or a different operating system setup.
Docker solves this by packaging the app together with its runtime, libraries, and settings into a container — a sealed box that runs the same way wherever Docker runs. If it works in the container on your laptop, it works in the same container on the server.
Image vs container: the one distinction that matters
An image is the blueprint: a read-only template that says "start from this base, install these things, run this command." A container is a running instance of an image — the blueprint brought to life.
One image can produce many containers, just as one class in programming can produce many objects. You build an image once, then start, stop, and delete containers from it as needed. Deleting a container does not delete the image, so you can always start a fresh one.
A first Dockerfile
A Dockerfile is a short recipe for building an image. Here is one for a simple Node.js app:
# Start from the official Node.js image
FROM node:20
# Set the working folder inside the container
WORKDIR /app
# Copy package files first, so installs are cached
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy the rest of the app code
COPY . .
# Tell Docker which port the app uses
EXPOSE 3000
# The command that starts the app
CMD ["node", "index.js"]
Each line is one step, and Docker caches each step. That is why package*.json is copied before the rest of the code: if only your code changes, Docker reuses the cached install step instead of reinstalling everything.
Build the image and run a container from it:
docker build -t my-app .
docker run -p 3000:3000 my-app
The -p 3000:3000 maps port 3000 inside the container to port 3000 on your machine, so you can open the app in a browser as usual.
Why containers beat "just install it on the server"
- Consistency: the same image runs on a laptop, a test server, and production.
- Isolation: each container has its own files and processes, so two apps needing different versions of the same tool do not fight each other.
- Clean teardown: stop and delete a container, and its mess goes with it. Your machine stays clean.
Containers are not virtual machines. A virtual machine carries a whole guest operating system; a container shares the host's operating system kernel and only packages what the app needs on top. That is why containers start in seconds and use far less memory.
Common beginner mistakes
- Rebuilding the whole image after every small code change, instead of letting Docker's layer cache do its job.
- Forgetting the port mapping (-p), then wondering why the app is unreachable.
- Storing passwords or secret keys inside the image. Pass secrets at run time instead, never bake them into a Dockerfile that might be shared.
Takeaway
An image is the blueprint, a container is the running copy. Write a short Dockerfile, build once, and run the same container everywhere. That one habit removes an entire class of "but it worked locally" problems.
Originally published at https://practicaldevblog.blogspot.com/2026/10/docker-explained-containers-without.html
Top comments (0)