Docker Introduction

May 18, 2026
#devops #docker #containers #intermediate

“It works on my machine” is the problem Docker solves. Docker packages your application with everything it needs — code, runtime, libraries, system tools — into a portable container that runs identically everywhere: your laptop, your colleague’s laptop, staging, production.

What Docker Actually Is

Docker is a platform for building and running containers. A container is a lightweight, isolated environment that shares the host OS kernel but has its own filesystem, processes, and network.

Think of it like this:

  • Virtual machines — Each VM runs a full operating system. Heavy (GBs), slow to start (minutes).
  • Containers — Share the host OS kernel. Lightweight (MBs), start instantly (seconds).

Key Concepts

  • Image — A read-only template containing your app and its dependencies. Like a class in OOP.
  • Container — A running instance of an image. Like an object created from a class.
  • Dockerfile — Instructions for building an image.
  • Registry — A place to store and share images (Docker Hub, AWS ECR, GitHub Container Registry).

Installing Docker

Running Your First Container

# Run an nginx web server
docker run -d -p 8080:80 --name my-nginx nginx

# -d: run in background (detached)
# -p 8080:80: map host port 8080 to container port 80
# --name: give it a friendly name

Open http://localhost:8080 — you’ll see the nginx welcome page. That’s a full web server running in an isolated container.

# See running containers
docker ps

# View logs
docker logs my-nginx

# Stop the container
docker stop my-nginx

# Remove it
docker rm my-nginx

Building Your Own Image

Create a simple Node.js app and containerize it:

// app.js
const http = require("http");

const server = http.createServer((req, res) => {
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ message: "Hello from Docker!", time: new Date() }));
});

server.listen(3000, () => console.log("Server running on port 3000"));
# Dockerfile
FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --production

COPY . .

EXPOSE 3000

CMD ["node", "app.js"]

Build and run:

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

curl http://localhost:3000
# {"message":"Hello from Docker!","time":"2026-05-18T14:00:00.000Z"}

Dockerfile Instructions

Instruction Purpose
FROM Base image to build on
WORKDIR Set the working directory
COPY Copy files from host to image
RUN Execute a command during build
EXPOSE Document which port the app uses
CMD Default command when container starts
ENV Set environment variables
ARG Build-time variables

Layer caching

Docker caches each instruction as a layer. Order matters for build speed:

# Good — dependencies change less often than code
COPY package*.json ./
RUN npm ci --production
COPY . .

# Bad — any code change invalidates the npm install cache
COPY . .
RUN npm ci --production

Essential Commands

# Images
docker build -t name:tag .       # Build an image
docker images                    # List images
docker rmi image-name            # Remove an image

# Containers
docker run -d -p 8080:80 image   # Run a container
docker ps                        # List running containers
docker ps -a                     # List all containers (including stopped)
docker stop container-name       # Stop a container
docker rm container-name         # Remove a container
docker logs container-name       # View logs
docker exec -it container sh     # Open a shell inside a running container

# Cleanup
docker system prune              # Remove unused containers, images, networks

Environment Variables

Pass configuration without hardcoding:

docker run -d \
    -p 3000:3000 \
    -e DATABASE_URL="postgres://user:pass@host/db" \
    -e NODE_ENV="production" \
    my-app

Or use an env file:

# .env
DATABASE_URL=postgres://user:pass@host/db
NODE_ENV=production
docker run -d -p 3000:3000 --env-file .env my-app

Volumes (Persistent Data)

Containers are ephemeral — when they’re removed, their data is gone. Volumes persist data:

# Named volume (Docker manages the storage location)
docker run -d \
    -v postgres-data:/var/lib/postgresql/data \
    -e POSTGRES_PASSWORD=secret \
    postgres:16

# Bind mount (map a host directory into the container)
docker run -d \
    -v $(pwd)/src:/app/src \
    -p 3000:3000 \
    my-app

Bind mounts are useful for development — edit code on your host, see changes in the container.

Docker Compose

For multi-container applications, Docker Compose defines everything in one file:

# docker-compose.yml
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/myapp
    depends_on:
      - db

  db:
    image: postgres:16
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
      - POSTGRES_DB=myapp
    volumes:
      - postgres-data:/var/lib/postgresql/data

volumes:
  postgres-data:
# Start everything
docker compose up -d

# View logs
docker compose logs -f

# Stop everything
docker compose down

# Stop and remove volumes (destroys data)
docker compose down -v

.dockerignore

Like .gitignore — exclude files from the build context:

node_modules
.git
.env
*.md
dist
coverage

This makes builds faster and keeps secrets out of images.

Best Practices

  • Use specific base image tags (node:20-alpine, not node:latest) for reproducible builds
  • Use Alpine images when possible — they’re much smaller (5MB vs 900MB)
  • Don’t run as root — Add USER node or similar in your Dockerfile
  • One process per container — Don’t run your app and database in the same container
  • Use multi-stage builds for compiled languages to keep final images small
  • Never put secrets in images — Use environment variables or secret management tools

Multi-stage build example

# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Production stage
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
USER node
CMD ["node", "dist/index.js"]

What’s Next

With Docker basics down, explore Docker Compose for multi-service development environments, or learn about container orchestration with Kubernetes for production deployments at scale.