DEV Community

Alex Chen
Alex Chen

Posted on

Docker Deep Dive: Beyond docker run (2026)

Docker Deep Dive: Beyond docker run (2026)

You know docker run and docker build. But Docker can do so much more. Here's what you need to know to use it effectively in production.

Understanding How Containers Actually Work

┌─────────────────────────────────────────────────────┐
│                    Your Container                   │
│  ┌─────────────────────────────────────────────┐   │
│  │           Application Layer                  │   │
│  │         (Your Node.js app, Python, etc.)     │   │
│  ├─────────────────────────────────────────────┤   │
│  │           Container Layer (Writable)          │   │
│  │    Changes made at runtime go here           │   │
│  │    (Deleted when container is removed!)      │   │
│  ├─────────────────────────────────────────────┤   │
│  │           Image Layer (Read-Only)             │   │
│  │    FROM node:18-alpine                       │   │
│  │    RUN npm ci --only=production              │   │
│  │    COPY . .                                  │   │
│  │    Each command = a layer (cached!)          │   │
│  └─────────────────────────────────────────────┘   │
│                                                     │
│  Shared with host: Kernel, Network (via bridge),    │
│                    File System (via volumes)        │
└─────────────────────────────────────────────────────┘

Key insight: Images are read-only. Containers add a thin writable layer on top.
When you remove the container, ALL changes in that writable layer are GONE.
That's why VOLUMES are essential — they persist data outside the container.
Enter fullscreen mode Exit fullscreen mode

Dockerfile Best Practices

# === The Perfect Node.js Dockerfile ===
# 1. Use specific version tags (not "latest"!)
# 2. Multi-stage builds (smaller images)
# 3. Leverage layer caching (order matters!)

# Stage 1: Build dependencies
FROM node:18-alpine AS builder

WORKDIR /app

# Install dependencies FIRST (changes least often → best cache hit)
COPY package.json package-lock.json ./
RUN npm ci --only=production && npm cache clean --force

# Stage 2: Production image (minimal)
FROM node:18-alpine AS production

# Security: Run as non-root user!
RUN addgroup -g 1001 -S appgroup && \
    adduser -u 1001 -S appuser -G appgroup

USER appuser
WORKDIR /app

# Copy only built artifacts from builder stage
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
COPY . .

# Health check (so orchestrators know if your app is alive):
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

EXPOSE 3000

# Signal handling (graceful shutdown!):
STOPSIGNAL SIGTERM

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

# === Why This Matters ===
# Image size comparison:
# - Naive Dockerfile: ~1GB (includes devDependencies, source code, etc.)
# - Multi-stage above:  ~150MB (only production deps + compiled code)
# Smaller images = faster pulls, faster deploys, smaller attack surface!
Enter fullscreen mode Exit fullscreen mode

Essential Dockerfile Patterns

# === Pattern 1: Cache Busting ===
# Problem: COPY . . copies EVERY file → invalidates cache on ANY code change
# Solution: Copy dependency files separately:

COPY package.json package-lock.json ./
RUN npm ci          # Only re-runs when package files change!

COPY . .
RUN npm run build   # Only re-runs when source code changes!

# For even finer control:
COPY src/ ./src/     # Only re-runs when src/ changes
# Order from LEAST frequently changed to MOST frequently changed!

# === Pattern 2: Multi-Architecture Builds ===
FROM --platform=linux/amd64 node:18-alpine AS build-amd64
FROM --platform=linux/arm64 node:18-alpine AS build-arm64
# Buildx handles this automatically:
# docker buildx build --platform linux/amd64,linux/arm64 -t myapp .

# === Pattern 3: Secrets Management (Build Time) ===
# Don't embed secrets in image layers!
RUN --mount=type=secret,id=NPM_TOKEN,target=/root/.npmrc \
  npm ci --registry=https://registry.example.com
# Usage: docker build --secret id=NPM_TOKEN,src=.npm_token_file .

# === Pattern 4: Non-Root User (Security!) ===
# NEVER run as root in production containers!
# If attacker exploits a vulnerability in your app,
# they get root access inside the container → easier privilege escalation

# In your base image or Dockerfile:
RUN groupadd -r myuser && useradd -r -g myuser myuser
USER myuser
# Now your app runs with limited permissions

# But you might need root during BUILD:
USER root
RUN apt-get update && apt-get install -y python3 make g++
USER myuser  # Switch back to non-root for runtime!
Enter fullscreen mode Exit fullscreen mode

Volume & Network Deep Dive

# === Volumes: Persistent Data ===
# Named volume (managed by Docker):
docker run -v mydata:/app/data postgres
# Data survives container removal. Docker manages storage location.

# Bind mount (host path mapped into container):
docker run -v $(pwd)/logs:/app/logs myapp
# Direct mapping to host filesystem. Good for development.

# Read-only bind mount (security!):
docker run -v $(pwd)/config:/app/config:ro myapp
# Container can READ config but can't MODIFY it.

# tmpfs (in-memory, super fast, lost on stop):
docker run --tmpfs /tmp/cache myapp
# Great for temporary files, session data, caches.

# Volume vs Bind Mount:
#
# Named Volume:            Bind Mount:
# ✅ Docker managed          ✅ Full control of location
# ✅ Works on any OS        ✅ Can edit from host
# ✅ Backup tools built-in   ❌ Path must exist on host
# ❌ Harder to find files    ❌ Host-specific paths
# Best for: Databases       Best for: Dev, configs, logs

# === Networks: Container Communication ===
# Bridge network (default):
docker network create mynet
docker run --network mynet db
docker run --network mynet app
# Containers communicate by SERVICE NAME as hostname!
# From app container: pg.connect({ host: 'db' }) ← 'db' is the container name!

# Host network (skip Docker networking):
docker run --network host myapp
# App listens directly on host's network stack.
# Simpler but less isolation. Good for high-performance needs.

# None network (completely isolated):
docker run --network none myapp
# No network access at all. Maximum security for batch jobs.

# Port publishing (-p):
docker run -p 3000:3000 myapp       # Map host 3000 → container 3000
docker run -p 8080:3000 myapp       # Map host 8080 → container 3000
docker run -p 0.0.0.0:3000:3000     # Bind to all interfaces (default)
docker run -p 127.0.0.1:3000:3000    # Bind to localhost only (more secure!)
Enter fullscreen mode Exit fullscreen mode

Production Operations

# === Resource Limits ===
# Prevent one container from eating all resources:
docker run -d \
  --memory="512m" \          # Max 512MB RAM
  --cpus="1.0" \             # Max 1 CPU core
  --memory-swap="768m" \     # RAM + swap limit
  --restart=unless-stopped \  # Auto-restart on crash
  --read-only \               # Read-only filesystem (except volumes!)
  --tmpfs /tmp:rw,noexec,nosuid,size=50m \  # Writable temp dir
  --security-opt=no-new-privileges:true \    # Drop capabilities
  --cap-drop=ALL \            # Remove ALL Linux capabilities
  --cap-add=NET_BIND_SERVICE \ # Add back ONLY what you need
  myapp

# === Monitoring Inside Containers ===
# Resource usage:
docker stats                          # Live CPU/MEM/NET for all containers
docker stats --no-stream mycontainer   # One snapshot

# Inspect container details:
docker inspect mycontainer             # Everything about the container (JSON)
docker inspect --format='{{.State.Pid}}' mycontainer  # Just the PID
docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' mycontainer  # IP address

# Exec into running container:
docker exec -it mycontainer sh         # Alpine-based (sh)
docker exec -it mycontainer bash       # Debian/Ubuntu-based (bash)
docker exec mycontainer cat /proc/1/status  # Check process info
docker exec mycontainer df -h          # Disk usage INSIDE container

# === Logging ===
# View logs:
docker logs mycontainer               # All logs
docker logs -f mycontainer            # Follow (live tail)
docker logs --since 2026-06-01 mycontainer  # Since date
docker logs --tail 100 mycontainer    # Last 100 lines

# Log drivers (configure per container or daemon-wide):
# json-file (default): JSON format, easy to parse
# syslog: Send to system syslog
# journald: systemd journal
# fluentd: Send to FluentD for aggregation
# none: Disable logging (for security-sensitive containers)

# Configure logging driver:
docker run --log-driver=json-file --log-opt max-size=10m --log-opt max-file=3 myapp

# === Cleanup & Maintenance ===
# See disk usage:
docker system df                      # Overview of images, containers, volumes, cache

# Prune unused items:
docker system prune -a                # Remove stopped containers + unused images
docker volume prune                  # Remove unused volumes
docker builder prune                 # Remove build cache

# Find large objects:
docker system df -v                   # Detailed breakdown (find space hogs!)

# Image cleanup:
docker images --format "{{.Repository}}:{{.Tag}} {{.Size}}" | sort -k2 -h
# List images by size, biggest last
Enter fullscreen mode Exit fullscreen mode

What's your most-used Docker command? What Docker concept took you longest to understand?

Follow @armorbreak for more practical developer guides.

Top comments (0)