Docker solves one problem so well that it took over: "it works on my machine".
It does that by packaging your application together with everything it needs — the runtime, the libraries, the system packages, the configuration — into one artefact that runs identically wherever Docker runs.
Almost every mistake beginners make traces back to a single missing concept. So the model comes first, and the commands after.
Images and containers
This is the idea. Get it and the rest follows.
An image is a read-only template. A recipe. It contains your application and its dependencies, and it does nothing on its own.
A container is a running instance of an image. You can start many containers from one image, and each is isolated from the others.
The analogy that works: an image is a class, a container is an object. Or: an image is an installer, a container is the installed, running program.
Why this matters immediately: containers are disposable. That is the point — you throw them away and start a fresh one. It is also why anything written inside a running container is destroyed when that container is removed, which is the single most alarming thing that happens to beginners. The fix is volumes, further down, and it is the thing to read before you put a database in one.
Is it worth learning?
Honest answer: it depends on what you deploy.
Worth it if:
- Your app needs specific versions of things — a particular Node, a particular Python, a database at a particular version.
- More than one person works on it, or you have ever said "works on my machine".
- You run several services that must talk to each other.
- You deploy to a VPS or a cloud platform and want the deploy to be reproducible.
Probably not worth it if:
- You deploy a WordPress site to shared hosting. Docker does not apply — you cannot run it there, and you do not need it.
- You have one static site on a managed platform.
That first exclusion is worth stating plainly, because it comes up constantly: shared hosting cannot run Docker. Containers need kernel-level features you do not have without root. This is one of the clearest reasons to move to a VPS — see shared vs VPS vs dedicated, and any unmanaged VPS with root will do, including ours.
The commands that matter
There are hundreds. You need about eight.
docker run -d -p 8080:80 --name web nginx # start a container in the background
docker ps # what is running
docker ps -a # including stopped ones
docker logs -f web # follow the logs — your main debugging tool
docker exec -it web sh # get a shell inside a running container
docker stop web && docker rm web # stop and remove
docker images # what images are on disk
docker system prune -a # reclaim space (careful: removes unused images)The one to internalise is -p 8080:80: host port : container port. Your app listens on 80 inside the container; you reach it on 8080 outside. Getting this backwards is the most common "why can I not connect" moment.
docker logs is where you will spend most of your debugging time. Learn it early.
Writing a Dockerfile
A Dockerfile describes how to build your image. Here is a realistic one for a Node app, with the two things that matter marked:
# 1. Build stage — has the toolchain
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# 2. Runtime stage — only what is needed to run
FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]Two things make this good rather than merely working:
Layer caching. Docker caches each instruction and reuses the cache while nothing above has changed. package*.json is copied and installed before the rest of the source, so editing a source file does not re-run npm ci. Copy everything first and every build reinstalls every dependency — this one ordering decision is often the difference between a 20-second build and a four-minute one.
Multi-stage builds. The first stage has the compilers and dev dependencies; the second copies only the built output. The shipped image is a fraction of the size, and it does not contain your build toolchain — which is a security improvement as well as a size one.
Also note USER node. Containers run as root by default, which is unnecessary and is a finding in every security audit.
.dockerignore
Create this file. Immediately, before your first build:
node_modules
.git
.env
*.log
distWithout it, COPY . . copies your entire node_modules, your git history and — this is the important one — your .env file with its secrets into the image. Anyone who pulls that image has your credentials. It is a common and genuinely serious mistake.
Volumes: where your data lives
Back to the disposability point.
Everything a container writes to its own filesystem disappears when the container is removed. For a stateless web app that is exactly what you want. For a database it is a disaster, and it is how people lose data on their first attempt.
A volume is storage that lives outside the container's lifecycle:
docker run -d \
-v pgdata:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=secret \
--name db postgres:17pgdata is a named volume Docker manages. Remove and recreate the container as often as you like; the data stays.
The rule: if losing it would matter, it goes in a volume. Databases, uploads, generated files.
⚠ A volume is not a backup. It lives on the same machine as the container. docker system prune --volumes will happily delete it. Back it up separately — see securing a Linux server.
Docker Compose
One container is easy. Real applications are three or four — app, database, cache, proxy. Compose describes them all in one file:
services:
app:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://app:secret@db:5432/app
depends_on:
- db
db:
image: postgres:17
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:Then docker compose up -d, and the whole stack starts.
The neat part is the networking: services reach each other by name. The app connects to db:5432 — not localhost, not an IP. Compose creates a network where each service's name resolves to its container. Getting this makes multi-service setups suddenly simple.
⚠ depends_on waits for the container to start, not for the database to be ready to accept connections. Your app must retry its first connection, or it will crash on startup roughly half the time.
Deploying to a VPS
The simplest deployment that is not embarrassing:
# On the VPS, once
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER # log out and back in
# Then, per deploy
git pull
docker compose build
docker compose up -dPut a reverse proxy in front — Caddy or Nginx — to terminate TLS and route to your containers. Caddy is the easier of the two because it obtains and renews certificates automatically.
Four things people skip and regret:
- Set restart policies.
restart: unless-stopped, or your app stays down after a reboot. - Do not put secrets in the Compose file if it is in git. Use a
.envfile that is gitignored, or your platform's secret store. - Cap the logs. Container logs grow without limit and fill the disk — a genuinely common cause of a mysteriously dead server.
- Harden the host. Docker is not a security boundary you should rely on alone, and it punches holes through
ufwby manipulating iptables directly — a published container port can be reachable even when your firewall says otherwise. Verify from outside.
The five mistakes everyone makes
- No
.dockerignore, so secrets andnode_modulesend up in the image. - Copying source before installing dependencies, so the cache never helps and every build is slow.
- Storing data without a volume, and losing it on the first container replacement.
- Running as root, because it is the default and nothing complains.
- Using
:latest. It means "whatever was newest when this was pulled", which is not reproducible and defeats the purpose. Pin versions:postgres:17, notpostgres:latest.
Debugging when a container will not start
The most common beginner experience is a container that exits immediately, and the instinct — run it again — never helps. The method that does:
docker ps -a # it is there, and "Exited (1)"
docker logs <name> # the actual reason, almost alwaysdocker logs on a stopped container still works, and the answer is usually in the last three lines. What you will typically find:
Exit code 1 — your application crashed. Missing environment variable, database not reachable, a syntax error. Read the log; it is an ordinary application error that happens to be inside a container.
Exit code 127 — command not found. Your CMD refers to something that does not exist in the image. Common when copying a Dockerfile between projects, or when the binary exists on your machine and not in the base image.
Exit code 137 — killed, out of memory. The container exceeded its limit, or the host ran out. Raise the limit or reduce what the process does.
It starts and exits 0 immediately — the container's main process finished. Containers live as long as their foreground process; a CMD that starts something in the background and returns will exit at once. Run the process in the foreground.
When the logs are not enough, get inside a working image and look:
docker run --rm -it --entrypoint sh myimage # bypass CMD, poke around
docker exec -it <running> sh # or inspect a live one--entrypoint sh is the one worth remembering. It starts the image without running your application, which lets you check whether the files you expected are actually where you expected them — and the answer is very often no, because of a COPY path or a .dockerignore entry.
Frequently asked questions
What is the difference between an image and a container? An image is a read-only template — the recipe. A container is a running instance of it. One image can produce many containers, and each container's own filesystem is discarded when it is removed.
Can I run Docker on shared hosting? No. Containers require kernel features that need root access, which shared hosting does not give you. You need a VPS or a dedicated server.
Why does my data disappear when I restart a container? Because container filesystems are ephemeral by design. Anything that must survive goes in a named volume, mounted at the path the application writes to.
Do I need Kubernetes as well? Almost certainly not. Kubernetes solves orchestration across many machines. For one server running a handful of services, Docker Compose is the right tool and Kubernetes is a large amount of complexity for no benefit.
Is Docker secure? Reasonably, with care. Do not run containers as root, do not use :latest, keep base images updated, and never bake secrets into an image. Note that Docker manipulates iptables directly and can expose published ports past a UFW rule — always verify from outside the machine.
How do I make my images smaller? Use multi-stage builds so the toolchain does not ship, start from an Alpine or slim base image, add a .dockerignore, and combine RUN steps that create and then delete files, since each layer is stored permanently even if a later layer removes the file.

.webp&w=128&q=75)
.webp&w=256&q=75)