← Back

Docker, Explained

2026-01-09 15:34 · 👁 48165

#docker

Docker, Explained (for devs who care about speed, size, and safety)

Docker is “just containers”… until your builds get slow, your images get huge, your data disappears, and prod behaves differently than your laptop.

Here’s the practical stuff that actually matters when you’re building real systems.


1) Image layers & caching

Docker images are built in layers. Each instruction in your Dockerfile (FROM, RUN, COPY, etc.) creates a new layer. Docker caches each layer so future builds can reuse them.

Why cache “randomly” breaks

Docker invalidates the cache from the first changed layer onward. That means a tiny change can force expensive steps to rerun.

Classic example:

#### ❌ slow when code changes often
COPY . .
RUN npm ci
RUN npm run build

Every code change invalidates COPY . ., which forces npm ci to rerun.

Better: copy dependencies first

# ✅ faster builds
COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

Same idea applies to .NET:

COPY *.csproj ./
RUN dotnet restore

COPY . .
RUN dotnet publish -c Release -o /app

Rule of thumb: put the most stable stuff first (tooling/dependencies), and the most frequently changing stuff last (source code).


2) Multi-stage builds

Multi-stage builds let you compile in one image (big, with SDK/tools) and run in another (small, minimal runtime).

Why it matters

  • Smaller images → faster pulls and deploys
  • Less attack surface → fewer vulnerabilities
  • Cleaner production → no compilers, no package managers

Example (.NET):

# build stage
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -o /out

# runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:9.0
WORKDIR /app
COPY --from=build /out .
ENTRYPOINT ["dotnet", "MyApp.dll"]

3) Volumes vs bind mounts

Both map storage into containers, but they’re meant for different jobs.

Bind mount (dev workflow)

Maps a host path into the container:

  • Great for live-reload and local dev
  • Your code edits instantly reflect inside the container
  • Host permissions/path differences can bite across OSes

Example:

docker run -v $(pwd):/app myimage

Named volume (data persistence)

Managed by Docker (not tied to a specific host folder):

  • Best for databases and persistent data
  • Easy to back up/move
  • More portable and less fragile than bind mounts

Example:

docker run -v mydbdata:/var/lib/postgresql/data postgres

Rule of thumb:

  • Code: bind mount
  • Stateful data: volume

4) Networking: bridge, host, and container-to-container

Docker networking decides how containers talk to each other and to your machine.

Bridge (default)

  • Containers get an internal IP on a virtual network
  • You publish ports to reach them from the host: -p 8080:80

Container-to-container works via service/container name on the same network.

Host (Linux only, mostly)

  • Container shares the host network stack
  • No NAT, sometimes lower latency
  • Less isolation (use carefully)

Container-to-container communication

In Compose, this is where people get it wrong:

  • From your laptop: use localhost:<published-port>
  • From another container: use the service name + internal port

Example: API calling Redis in Compose:

  • API connects to redis:6379 (not localhost)

5) Resource limits (CPU / memory)

Without limits, one container can hog resources and create noisy neighbor problems.

What to control

  • Memory: prevent a runaway process from taking the host down
  • CPU: avoid one service starving everything else

Examples:

docker run --memory=512m --cpus=1.0 myimage

With Compose:

services:
  api:
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M

Tip: memory limits are often the most important in dev machines and multi-service stacks.


6) Docker Compose (local orchestration)

Compose is how you run multiple services locally without pain: app + db + cache + worker + queue.

A clean baseline:

services:
  api:
    build: .
    ports:
      - "8080:8080"
    environment:
      - ConnectionStrings__Db=Host=db;Port=5432;Database=app;Username=postgres;Password=postgres
    depends_on:
      - db

  db:
    image: postgres:16
    volumes:
      - pgdata:/var/lib/postgresql/data
    ports:
      - "5432:5432"

volumes:
  pgdata:

Compose gives you:

  • shared networking by default
  • predictable service names (db, api)
  • repeatable local environments

7) Secrets & env vars (don’t bake secrets)

A Docker image is meant to be shared and cached. If you put secrets in it, they will leak.

What not to do

  • Don’t ENV API_KEY=... in the Dockerfile
  • Don’t COPY .env into the image
  • Don’t commit secrets to git “just for dev”

What to do instead

  • Use env vars at runtime (docker run -e ...)
  • Use .env for local dev (Compose reads it)
  • Use Docker/Swarm/Kubernetes secrets for real environments
  • Use CI secret stores (GitHub Actions/Azure DevOps/etc.)

Rule: images should be environment-agnostic. Configuration happens at runtime.


8) Image security (minimal bases, scanning)

Most real-world Docker vulns come from:

  • bloated base images
  • outdated system packages
  • shipping build tools in production images

Practical security wins

  • Use minimal base images (distroless/alpine/lean variants where compatible)
  • Prefer pinned versions for repeatable builds
  • Remove package manager caches
  • Run as non-root when possible
  • Scan images in CI before deploy

Also: multi-stage builds help security by keeping compilers and tooling out of production.


The “fast, small, safe” checklist ✅

  • Cache-friendly Dockerfile (copy deps first)
  • Multi-stage builds (SDK image ≠ runtime image)
  • Bind mounts for code, volumes for data
  • Service-name networking in Compose (db, redis, etc.)
  • CPU/memory limits to avoid noisy neighbors
  • Secrets only at runtime
  • Minimal images + scanning before shipping

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please reload the page.