Docker Tutorial for Beginners: Images, Containers and Your First Dockerfile
Docker shows up in almost every DevOps roadmap for a reason: once your application runs in a container, "it works on my machine" mostly stops being a problem. This is a hands-on introduction — by the end you'll have built and run a real image, not just read about the concept.
What Docker actually is
Docker packages an application together with everything it needs to run — code, runtime, system libraries, dependencies — into a single, portable unit called an image. When you run that image, you get a container: an isolated, running instance of it. The same image behaves identically on your laptop, a teammate's machine, and a production server, because the environment travels with it instead of being assumed.
It's worth being precise about the two terms, since they get used interchangeably in casual conversation but mean different things:
- Image — a read-only template (think: a class in programming)
- Container — a running instance of that image (think: an object created from that class)
Docker architecture, briefly
Four pieces matter when you're starting out:
- Docker Client — the
dockercommand you type - Docker Daemon — the background process that actually builds images and runs containers
- Images — the templates, built from a
Dockerfile - Registry — where images are stored and shared (Docker Hub is the public default; teams often use a private one like Amazon ECR)
Writing your first Dockerfile
A Dockerfile is a plain-text script of instructions for building an image. Here's a minimal one for a Node.js app:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
A few things worth understanding line by line:
FROMsets the base image —node:20-alpineuses Alpine Linux specifically because it's a fraction of the size of the default Debian-based Node imageWORKDIRsets the working directory inside the container for every instruction after it- Copying
package*.jsonand runningnpm cibefore copying the rest of the code is deliberate — Docker caches each layer, so dependencies only get reinstalled whenpackage.jsonactually changes, not on every code edit CMDis the command that runs when the container starts
Commands you'll use daily
# Build an image from the Dockerfile in the current directory
docker build -t myapp:v1 .
# Run a container from that image, mapping port 3000
docker run -p 3000:3000 myapp:v1
# List running containers
docker ps
# See logs from a running container
docker logs <container_id>
# Stop a container
docker stop <container_id>
# List images on your machine
docker images
# Remove an image
docker rmi myapp:v1
Common mistakes
Not using a .dockerignore file. Without one, COPY . . happily copies node_modules/, .git/, and local .env files into your image — bloating it and sometimes leaking secrets. Add a .dockerignore the same way you'd use .gitignore.
Running everything as root. By default, containers run as root unless told otherwise. For anything beyond local experimentation, add a non-root user in the Dockerfile and switch to it before CMD runs.
One giant image instead of a multi-stage build. If your app needs a build step (compiling TypeScript, bundling assets), doing that and running the app in the same final image drags along build tools you don't need at runtime. A multi-stage build compiles in one stage and copies only the final output into a clean, minimal final image — often cutting image size dramatically.
Production considerations
- Scan images for vulnerabilities before deploying — a tool like Trivy checks both the base image and your dependencies for known CVEs
- Pin versions (
node:20-alpine, notnode:latest) —latestsilently changes over time and can break a working deployment for no reason you touched - Keep images small — smaller images pull faster, start faster, and have a smaller attack surface
- Never bake secrets into an image — environment variables or a secrets manager at runtime, not hardcoded in the Dockerfile or copied-in config files (a
docker historyon a careless image can reveal exactly what you tried to hide)
Where this fits in the bigger picture
Docker is step 5 in our DevOps roadmap — after Linux, Git, cloud fundamentals and CI/CD with Jenkins, and right before Kubernetes, which is what most teams reach for once they're running more containers than they can manage by hand. Once you're comfortable building and running containers, Kubernetes Services explained is a natural next read — it picks up exactly where this leaves off.
Want to learn this hands-on, live?
Join Waitlist