dissected.io
Lesson 8 of 23
Intermediateintermediatecontainersdockerimageslayers11 min read

Containers and Docker Deep Dive

Containers are not lightweight VMs. This is the most common misconception, and it leads to wrong mental models about security, performance, and isolation. A VM runs a full guest operating system on top of a hypervisor, with its own kernel. A container shares the host kernel and uses two Linux kernel features to create isolation: namespaces (which partition what a process can see — PID, network, mount, user, UTS) and cgroups (which limit what a process can consume — CPU, memory, I/O). No hypervisor, no guest OS. That's why containers start in milliseconds instead of minutes, and why you can run dozens on a laptop.

VMs vs Containers — Shared Kernel Advantage

Virtual Machines

Host OS
Hypervisor
Guest OS
App
Boot:0sMem:2GB+

Containers

Host OS
Kernel (shared)
Runtime
Container
Boot:0sMem:50MB

How Images Work

A container image is a layered, content-addressed, immutable filesystem. Each instruction in a Dockerfile creates a layer. Layers are stacked using a union filesystem — the container sees a single merged view, but each layer is stored and cached independently.

This has practical consequences. Layer caching means instruction order determines build times. If you copy your dependency manifest first, install dependencies, then copy your source code, changing a line of code only rebuilds from the source copy onward. If you copy everything first, changing one line invalidates the dependency install cache too.

# Good: dependencies cached separately from source
COPY package.json package-lock.json ./
RUN npm ci
COPY . .

# Bad: any source change busts the npm install cache
COPY . .
RUN npm ci

At runtime, the container gets one additional writable layer on top. Writes go there and are discarded when the container stops. This is why containers should be stateless — the writable layer is ephemeral, tightly coupled to that specific container instance, and carries a performance penalty from the union filesystem's copy-on-write mechanism.

Multi-Stage Builds

A naive Dockerfile installs compilers, build tools, and dev dependencies, then ships all of that in the final image. Multi-stage builds fix this. You build in a fat image with everything you need, then copy only the compiled artifacts into a minimal runtime image.

FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-slim
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]

For the runtime base, you have choices. Alpine is tiny (~5MB) but uses musl libc instead of glibc, which causes subtle bugs with some native modules and DNS resolution edge cases. Distroless images from Google contain only your app and its runtime dependencies — no shell, no package manager, smaller attack surface. Slim variants of official images are a solid middle ground.

Registries and Tags

Images live in registries. When you docker pull nginx, you're pulling from Docker Hub. The tag latest is not a version — it's just the default tag name, and it's mutable. Someone can push a completely different image as latest tomorrow. For anything that matters, pin by digest: nginx@sha256:abc123.... This guarantees you get exactly the image you tested.

Security Practices

Running containers as root is the default and it's a bad default. If an attacker escapes the container, they're root on the host. Use USER in your Dockerfile to run as a non-root user. Scan images for known vulnerabilities with tools like Trivy or Grype — integrate them into CI so vulnerable images never reach production.

Never bake secrets into image layers. Even if you delete a file in a later layer, the secret is still present in the earlier layer and trivially extractable. Use runtime secret injection instead — environment variables, mounted files, or your provider's secrets manager.

Container Runtimes and the OCI Standard

Docker popularized containers, but Docker the runtime is now just one option. The OCI (Open Container Initiative) standardized the image format and runtime spec. containerd is the runtime that Kubernetes actually uses under the hood — Docker itself wraps containerd. For local development, Docker Desktop or Podman (daemonless, rootless) are the common choices.

Docker Compose lets you define multi-container setups in a YAML file — your app, a database, a cache, a message broker — and start everything with docker compose up. It's the standard for local development environments, not for production.

AWS Implementation

AWS provides Elastic Container Registry (ECR) for image storage with integrated vulnerability scanning. Container runtime options include ECS with Fargate (serverless) or EC2 launch types, and EKS for Kubernetes workloads. AWS App Runner can take a container image and run it with minimal configuration.

GCP Implementation

Artifact Registry stores container images (and other artifacts) with vulnerability scanning via Container Analysis. Cloud Run takes a container image and runs it serverlessly. GKE runs Kubernetes workloads. Cloud Build provides a managed CI pipeline that builds containers from source.

Azure Implementation

Azure Container Registry (ACR) handles image storage with integrated Defender scanning. Azure Container Apps provides serverless container hosting with KEDA-based autoscaling. AKS runs Kubernetes. Azure Container Instances offers single-container hosting without orchestration — useful for batch jobs.

Side-by-Side Comparison

AspectAWSGCPAzure
Container registryECRArtifact RegistryACR
Serverless containersApp Runner, ECS FargateCloud RunContainer Apps
KubernetesEKSGKEAKS
Image scanningECR scanning, InspectorContainer AnalysisDefender for Containers
Local devDocker Desktop / FinchDocker Desktop / PodmanDocker Desktop
Build serviceCodeBuildCloud BuildACR Tasks

When to Use What

Docker containers are the standard packaging format regardless of where you deploy. The real decision is the runtime. For local dev and CI, Docker (or Podman) plus Compose is the default choice. For production, you pick from serverless containers, managed orchestration, or Kubernetes based on your complexity needs — topics covered in the next lessons.

The universal advice: keep images small, keep containers stateless, don't run as root, pin your versions, and scan for vulnerabilities in CI.

Key Takeaways

  • Containers share the host kernel and isolate via namespaces and cgroups — they are not VMs and don't carry the overhead of a guest OS
  • Image layer order matters — put infrequently changing instructions first to maximize cache hits and reduce build times
  • Multi-stage builds separate build dependencies from runtime, dramatically reducing image size and attack surface
  • Never bake secrets into layers, never run as root, and always scan images for vulnerabilities before deploying
  • Pin image references by digest, not by tag — latest is a mutable pointer, not a version
  • Docker Compose is for local dev; production needs a proper container orchestrator or serverless platform

Next, we'll look at where those images live between build and deploy — container registries.

Enjoyed this breakdown?

Get new lessons in your inbox.