"It works on my machine" used to be a punchline. Docker made it a guarantee. By packaging your application with its entire runtime environment — OS libraries, language runtime, dependencies, config files — Docker ensures that the app runs identically on your laptop, your colleague's machine, a CI server, and production.

Containers vs Virtual Machines

Virtual machines run a complete operating system on top of a hypervisor. Each VM has its own kernel, its own filesystem, and its own memory space. A single VM might consume 2GB+ of RAM before you even start your app.

Containers share the host OS kernel. They isolate the application's filesystem, network, and processes using Linux namespaces and cgroups — but without the overhead of a full OS. A container typically starts in under a second and uses megabytes, not gigabytes.

Core Concepts

  • Image: A read-only template containing the application code, runtime, libraries, and dependencies. Think of it as a snapshot.
  • Container: A running instance of an image. You can start, stop, and destroy containers without affecting the underlying image.
  • Dockerfile: A text file with instructions for building an image, step by step.
  • Registry: A repository for storing and distributing images. Docker Hub is the default public registry.

Writing a Dockerfile

Node.js Dockerfile
# Use official Node.js LTS as base image
FROM node:20-alpine

# Set working directory inside container
WORKDIR /app

# Copy dependency manifests first (for layer caching)
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production

# Copy application source code
COPY . .

# Expose the application port
EXPOSE 3000

# Define the command to start the app
CMD ["node", "server.js"]

Essential Docker Commands

Most-Used Commands
# Build an image from a Dockerfile
docker build -t my-app:1.0 .

# Run a container from an image
docker run -d -p 3000:3000 --name my-app my-app:1.0

# List running containers
docker ps

# View container logs
docker logs my-app

# Stop and remove a container
docker stop my-app && docker rm my-app

# List local images
docker images

# Remove an image
docker rmi my-app:1.0

Docker Compose for Multi-Service Apps

Most web apps need more than one service — an application server, a database, maybe a cache or message queue. Docker Compose lets you define and run all these services together with a single YAML file.

docker-compose.yml
version: "3.9"
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/mydb
    depends_on:
      - db
      - redis

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: mydb
    volumes:
      - pgdata:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  pgdata:
Docker Compose Commands
# Start all services (detached)
docker compose up -d

# View logs for all services
docker compose logs -f

# Stop and remove all containers
docker compose down

# Rebuild images after code changes
docker compose up --build -d

Best Practices for Production

  • Use multi-stage builds to keep final images small. Build in one stage, copy only the output to a minimal runtime stage.
  • Use .dockerignore to exclude node_modules, .git, and other unnecessary files from the build context.
  • Pin base image versions — use node:20.11-alpine instead of node:latest for reproducible builds.
  • Run as non-root user inside containers for security.
  • Leverage layer caching — copy package.json and run npm install before copying source code so dependencies are cached.
  • Use health checks so orchestrators (Docker Swarm, Kubernetes) know when your app is ready.

Try Our Free Developer Tools

Minify, beautify, and format your code instantly in your browser.