All posts
dockerdevops

Docker: A Practical Guide for Full-Stack Developers

A practical guide to Docker — images, containers, Dockerfiles, and the patterns that keep 'works on my machine' from happening again.

SR

Suhail Roushan

August 6, 2026

·
4 min read
·
0 views

"Works on my machine" stopped being an acceptable excuse the moment Docker made it cheap to ship the machine along with the code.

Docker is a platform for packaging an application and everything it needs to run — code, runtime, system libraries, configuration — into a single portable image that runs identically across development, CI, and production. A container is a running instance of that image, isolated from the host system but far lighter weight than a full virtual machine, since containers share the host's kernel instead of virtualizing hardware.

Why Docker Matters (and When to Skip It)

Environment inconsistency is one of the most persistent sources of bugs that "only happen in production" — a different library version, a missing system dependency, an OS-level difference. Docker eliminates this category of problem by making the runtime environment part of the versioned, deployed artifact instead of an assumption about the host machine.

Skip Docker for very simple deployments where your target platform already fully manages the runtime for you (many serverless/PaaS platforms abstract this away entirely) and you don't need local environment parity badly enough to justify the added layer.

Getting Started with Docker

A minimal Dockerfile for a Node.js app:

FROM node:22-slim

WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci --omit=dev

COPY . .

EXPOSE 3000
CMD ["node", "server.js"]

Build and run:

docker build -t my-app .
docker run -p 3000:3000 my-app

Core Docker Concepts Every Developer Should Know

Layer caching makes rebuilds fast if your Dockerfile is ordered correctly. Each instruction creates a cached layer — copying package.json and installing dependencies before copying the rest of your source means dependency installation is only re-run when dependencies actually change, not on every code change:

# good: dependencies cached separately from source
COPY package.json package-lock.json ./
RUN npm ci
COPY . .

# bad: any source change invalidates the dependency install cache
COPY . .
RUN npm ci

Multi-stage builds keep production images small. Build artifacts in one stage, copy only what's needed into a minimal final image — this avoids shipping build tools, dev dependencies, and source maps into production:

FROM node:22 AS builder
WORKDIR /app
COPY . .
RUN npm ci && npm run build

FROM node:22-slim
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]

Volumes persist data beyond a container's lifecycle. Containers are ephemeral by design — anything written inside a container without a volume is lost when it's removed. Databases and other stateful services need explicit volume mounts:

docker run -v pgdata:/var/lib/postgresql/data postgres:16

Docker Compose orchestrates multi-container local setups (an app, a database, a cache) as a single declarative file, replacing a string of manual docker run commands with one docker compose up:

services:
  app:
    build: .
    ports: ["3000:3000"]
    depends_on: [db]
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: dev

Common Docker Mistakes and How to Fix Them

Mistake 1: running containers as root without reason. The default in many base images, this is an unnecessary security risk for production containers. Fix: create and switch to a non-root user in the Dockerfile unless there's a specific reason root access is needed.

Mistake 2: bloated images from not using multi-stage builds or slim base images. Shipping a full node image with build tools and dev dependencies into production wastes bandwidth, storage, and increases attack surface. Fix: use multi-stage builds and -slim/-alpine base images for the final production stage.

Mistake 3: not using a .dockerignore file. Without one, COPY . . includes node_modules, .git, and other files that bloat the build context and image unnecessarily. Fix: add a .dockerignore mirroring your .gitignore plus Docker-specific exclusions.

When Should You Use Docker Instead of a Native Runtime?

Use Docker when you need environment consistency across development/CI/production, are deploying to container-orchestration platforms (Kubernetes, ECS), or want isolated, reproducible local development environments for multi-service setups. Skip it for simple deployments to platforms that already abstract the runtime for you, where the added build/deploy step isn't buying you anything concrete.

Docker in Production

Scan images for vulnerabilities as part of your CI pipeline (docker scan, Trivy, or your registry's built-in scanning) — base image vulnerabilities are a real and often-overlooked attack surface. Also pin base image versions explicitly rather than using latest, since an unpinned tag can silently change the environment your app runs in between builds.

If your team still has "works on my machine" as a recurring conversation, that's the concrete signal Docker (or at minimum Docker Compose for local dev) is worth adopting now rather than later.

Related posts

Written by Suhail Roushan — Full-stack developer. More posts on AI, Next.js, and building products at suhailroushan.com/blog.

Get in touch