Docker Deep Dive
Images, containers, Dockerfiles, volumes, networking - everything you need to containerize your first app.
“Works on my machine is a DevOps joke until Docker makes it true.”
What you'll learn
Start with the image, not the meme
A Docker image is a layered filesystem plus a default command. That is the whole trick. Each instruction in a Dockerfile adds a layer. Layers that never change get cached. Layers you poke every commit get rebuilt. If npm install runs after you COPY the entire repo, you have built a very expensive way to reinstall lodash.
Order instructions from least to most frequently changing: base image, system packages, lockfiles, npm ci / pip install, then source. The cache is not magic. It is a hash of the instruction and the files it touches. Treat it like a budget.
Dockerfiles that survive a Friday deploy
Pin the base tag. node:20-alpine is a decision. node:latest is a coin flip you will lose during an incident. Multi-stage builds exist so the compiler is not invited to production. Stage one builds. Stage two copies the artifact into a thin runtime image and runs it as a non-root user.
A production Dockerfile that still contains npm run dev, a full node_modules from the build stage, and USER root is not 'simple'. It is unfinished. Add a HEALTHCHECK only if something actually queries it. An ignored health check is set dressing.
Volumes, mounts, and the data you thought was in the container
Containers are cattle until they hold a database file. Bind-mount source into a container for local dev. Use a named volume for anything you would be sad to lose when the container is recreated. Do not bind-mount over a path the image already populated unless you enjoy empty directories and mysterious permission errors.
Read the UID the process runs as. Alpine images often run as root until you say otherwise. Your laptop's 1000:1000 and the container's root will disagree about who owns node_modules. That disagreement shows up as EACCES, not as a helpful lecture.
Networking: localhost is a rumor
Inside a container, localhost is the container. Not your laptop. Not the sibling service. Compose gives you a DNS name per service. postgres:5432 works. localhost:5432 is how you spend an hour 'debugging the database'.
Publish ports with ports: only when a human or a host process needs them. Service-to-service traffic can stay on the Compose network. In production, the same idea becomes a ClusterIP. The instinct to publish everything to 0.0.0.0 is how internal admin UIs end up on the internet.
Compose is the right size until it is not
docker compose up is a complete orchestration story for one host and a handful of services. Define services, networks, volumes, and env files. Commit the Compose file. Do not commit .env if it holds secrets — and it will, the first week someone is in a hurry.
When you need rolling updates across hosts, self-healing, and a control plane, you have left Compose. That is a later lesson. Running Kubernetes on a laptop to front a single Node app is not ambition. It is a scheduling hobby.
A first app, without the folklore
Write the Dockerfile. Build with a tag you can find later (app:local, not latest as a lifestyle). Run it. docker logs and docker exec are how you confirm the process is the process you think it is. Then put Compose around it so the database is not a tribal ritual.
If it only works on your machine, you have packaged your laptop. If it works from a clean compose up on someone else's machine, you have a unit you can talk about in a pipeline.