Why Docker Changed Development

Before Docker, the phrase "it works on my machine" was a constant source of frustration. Differences in OS, runtime versions, environment variables, and installed packages meant that code running locally often behaved differently in staging or production. Docker solves this by packaging your application and all its dependencies into a container — a portable, consistent environment that runs the same everywhere.

Core Concepts

Images

A Docker image is a read-only blueprint for a container. It includes the OS base layer, runtime, application code, and dependencies. Images are built from a Dockerfile and stored in registries like Docker Hub or GitHub Container Registry.

Containers

A container is a running instance of an image. Containers are isolated from each other and from the host system, but they share the host's OS kernel — making them far more lightweight than virtual machines.

Volumes

Volumes persist data beyond the lifetime of a container. Without a volume, any data written inside a container is lost when it stops. Use volumes for databases, uploaded files, or any stateful data.

Writing Your First Dockerfile

Here's a simple Dockerfile for a Node.js application:

# Use an official Node.js runtime as the base image
FROM node:20-alpine

# Set working directory
WORKDIR /app

# Copy dependency files and install
COPY package*.json ./
RUN npm install --production

# Copy the rest of the application
COPY . .

# Expose the port the app runs on
EXPOSE 3000

# Start the application
CMD ["node", "index.js"]

Build and run it with:

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

Docker Compose: Managing Multi-Container Apps

Most real applications involve more than one service — a web server, a database, maybe a cache. Docker Compose lets you define all your services in a single docker-compose.yml file and spin them all up with one command.

version: '3.8'
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/mydb
    depends_on:
      - db

  db:
    image: postgres:15
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: mydb
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

Start everything with docker compose up and tear it down with docker compose down.

Essential Docker Commands

CommandWhat It Does
docker build -t name .Build an image from a Dockerfile
docker run -p 8080:80 nameRun a container with port mapping
docker psList running containers
docker exec -it id shOpen a shell inside a running container
docker logs idView container logs
docker compose up -dStart all services in detached mode
docker system pruneClean up unused images and containers

Best Practices for Developer Workflows

  • Use multi-stage builds to keep production images lean
  • Always use a .dockerignore file to exclude node_modules, .git, and local configs
  • Pin your base image versions (e.g., node:20-alpine not node:latest) for reproducibility
  • Never hardcode secrets in a Dockerfile — use environment variables or secrets managers
  • Use named volumes for persistent data, bind mounts for local development hot-reloading

Docker is one of those tools that feels complex at first but quickly becomes second nature. Once you containerize your development environment, you'll wonder how you ever worked without it.