Docker Tutorial for Beginners: Images, Containers and Your First Dockerfile | TeraSkill Academy
Curriculum Projects Trainer Pricing FAQ Blog Contact Join Waitlist

← All articles

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 docker command 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:

  • FROM sets the base image — node:20-alpine uses Alpine Linux specifically because it's a fraction of the size of the default Debian-based Node image
  • WORKDIR sets the working directory inside the container for every instruction after it
  • Copying package*.json and running npm ci before copying the rest of the code is deliberate — Docker caches each layer, so dependencies only get reinstalled when package.json actually changes, not on every code edit
  • CMD is 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, not node:latest) — latest silently 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 history on 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