Docker Basics — From Container Concepts to Production Deployment

The Problem Docker Solves

“It works on my machine!” — every developer has heard this at least once. The root cause is the difference between development and production environments (OS, library versions, configuration). Docker solves this by bundling applications and their runtime environments into isolated packages called containers.

Unlike virtual machines (VMs), containers share the OS kernel, making them much lighter.

FactorVirtual Machine (VM)Container (Docker)
Startup timeTens of seconds to minutesWithin seconds
MemoryGB scaleMB scale
Isolation levelFull OS isolationProcess-level isolation
Image sizeSeveral GBTens to hundreds of MB
PortabilityHypervisor dependentRuns identically anywhere

Three Core Concepts

To understand Docker, you only need to know three things: images, containers, and registries.

  • Image: A read-only template containing everything needed to run an app (code, runtime, libraries). Think of it as a blueprint.
  • Container: A running instance created from an image. It’s the actual building made from the blueprint. You can run multiple containers simultaneously.
  • Registry: A repository for storing and sharing images. Docker Hub is the most common example.

Essential Commands

Image Management

These commands download images from Docker Hub and manage them locally.

# Download an image (defaults to 'latest' if tag is omitted)
docker pull nginx:alpine

# List local images
docker images
# REPOSITORY   TAG       IMAGE ID       SIZE
# nginx        alpine    a2573b4e3f72   43MB

# Delete an image
docker rmi nginx:alpine

# View image details (layers, environment variables, etc.)
docker inspect nginx:alpine

The alpine tag refers to lightweight Alpine Linux-based images. Even for the same app, node:22-alpine (~50MB) is much smaller than node:22 (~350MB). Use alpine tags whenever possible in production.

Running and Managing Containers

These commands run images as containers and manage their state.

# Run in background (-d), assign a name, map ports (host:container)
docker run -d --name my-nginx -p 8080:80 nginx:alpine
# → Accessible at http://localhost:8080

# List running containers
docker ps
# CONTAINER ID   IMAGE          STATUS    PORTS                  NAMES
# a1b2c3d4e5f6   nginx:alpine   Up 5s     0.0.0.0:8080->80/tcp   my-nginx

# List all containers (including stopped ones)
docker ps -a

# Stop / restart / remove
docker stop my-nginx
docker start my-nginx
docker rm my-nginx          # Can only remove when stopped
docker rm -f my-nginx       # Force remove (even while running)

In -p 8080:80, the first number is the host port and the second is the container port. This is easy to mix up, so remember the order: host:container.

Debugging Commands

Used for inspecting container internals and viewing logs.

# Stream logs in real time
docker logs -f my-nginx

# View only the last 50 lines
docker logs --tail 50 my-nginx

# Access a shell inside the container
docker exec -it my-nginx sh
# → Run commands like ls, cat inside the container
# → Type exit to leave

# Monitor container resource usage
docker stats my-nginx
# CPU %   MEM USAGE   NET I/O   BLOCK I/O
# 0.00%   2.3MiB      1kB/0B    0B/0B

Writing a Dockerfile

A Dockerfile is the recipe for building an image. Here’s an explanation of each instruction using a Node.js app as an example.

# 1. Specify base image (prefer alpine)
FROM node:22-alpine

# 2. Set working directory
WORKDIR /app

# 3. Copy dependency files first → key to cache optimization
COPY package.json package-lock.json ./

# 4. Install dependencies (exclude devDependencies)
RUN npm ci --omit=dev

# 5. Copy source code (must come after step 3 to keep cache valid)
COPY . .

# 6. Document the port the container will use
EXPOSE 3000

# 7. Run command
CMD ["node", "server.js"]

The order of steps 3-5 is important. If package.json hasn’t changed, the npm ci layer is cached, significantly speeding up the build. If only the source code changes, only step 5 onward is re-executed.

Build and run:

# Build image (-t: assign tag, .: current directory)
docker build -t my-app:1.0 .

# Run the built image
docker run -d --name my-app -p 3000:3000 my-app:1.0

Use a .dockerignore file to exclude unnecessary files from the build context.

node_modules
.git
.env
dist
*.md

Docker Compose

A tool for defining and running multiple containers at once. Use it when managing related services together, like a web app + database.

# docker-compose.yml (or compose.yml)
services:
  web:
    build: .                          # Build using the Dockerfile in the current directory
    ports:
      - "3000:3000"                   # Host:container port mapping
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgres://user:secret@db:5432/mydb
    depends_on:
      - db                            # Specify dependency so db starts first
    restart: unless-stopped           # Auto-restart on abnormal exit

  db:
    image: postgres:16-alpine         # Use official image
    volumes:
      - db-data:/var/lib/postgresql/data   # Persist data
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=secret
      - POSTGRES_DB=mydb

volumes:
  db-data:                            # Named volume (persists even if container is removed)

Compose commands:

# Build + run all services in background
docker compose up -d --build

# Check service status
docker compose ps

# View logs (specific service only)
docker compose logs -f web

# Stop all services + remove containers
docker compose down

# Also remove volumes (includes DB data — use with caution!)
docker compose down -v

depends_on only guarantees startup order. It does not wait until the DB is “ready.” Implement DB connection retry logic in your app, or configure a healthcheck.

Practical Tips

ScenarioCommandDescription
Disk cleanupdocker system prune -aRemove all unused images/containers/networks
Build cache checkdocker builder pruneRemove build cache only
Environment variable filedocker run --env-file .envInject .env file variables into the container
Volume mountdocker run -v $(pwd):/appMount host directory to container (for development)
Multi-stage buildFROM ... AS builderExclude build tools from final image to reduce size

In production, use specific version tags (e.g., node:22.14-alpine) instead of latest. Since latest can change at any time, the same Dockerfile may produce different results.

Was this article helpful?