Containers solve a classic problem in software delivery: "it works on my machine." A container bundles an application together with its runtime, libraries, and configuration — so it runs identically in development, CI, staging, and production.
Containers vs Virtual Machines
| Factor | Container | Virtual Machine |
|---|---|---|
| OS kernel | Shared with host | Own kernel (hypervisor) |
| Startup time | Milliseconds | Seconds to minutes |
| Image size | Megabytes | Gigabytes |
| Isolation | Process-level (namespaces, cgroups) | Hardware-level |
| Density | 100s per host | 10s per host |
| Portability | Any OCI-compliant runtime | Hypervisor-specific formats |
Containers are not as strongly isolated as VMs — a kernel vulnerability could potentially allow container escape. For multi-tenant workloads with strict isolation requirements, VMs (or sandboxed runtimes like gVisor or Kata Containers) provide stronger boundaries.
Docker Architecture
- Docker Engine: The daemon (
dockerd) that manages containers on the host. - Docker CLI: The client (
docker) that sends commands to the daemon over a socket. - Image: A read-only, layered filesystem snapshot. Immutable — you build a new image rather than modifying an existing one.
- Container: A running instance of an image. A writable layer is added on top of the image layers.
- Registry: A service that stores and distributes images (Docker Hub, ECR, GCR, ACR, Quay).
The Dockerfile
A Dockerfile defines how to build an image. Each instruction creates a new layer:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
Key instructions:
FROM— base image to build onWORKDIR— set the working directory for subsequent instructionsCOPY— copy files from the build context into the imageRUN— execute a command during the build (installs packages, compiles code)EXPOSE— documents which port the container listens on (informational only)CMD— the default command to run when the container starts
Multi-Stage Builds
Multi-stage builds use multiple FROM statements in one Dockerfile. Only the final stage ends up in the shipped image — build tools, compilers, and test frameworks are left behind.
# Stage 1: build
FROM golang:1.22 AS builder
WORKDIR /src
COPY . .
RUN go build -o /app ./cmd/server
# Stage 2: runtime image
FROM gcr.io/distroless/static
COPY --from=builder /app /app
CMD ["/app"]
The result is a tiny, minimal image containing only the compiled binary — not the Go toolchain. This dramatically reduces attack surface and image size.
Container Security Basics
- Run as non-root: Add
USER nonrootin your Dockerfile. Many base images provide a non-root user. - Use minimal base images: Alpine, distroless, or scratch images reduce the number of packages that could contain vulnerabilities.
- Scan images: Use tools like Trivy, Snyk, or Docker Scout to scan for known CVEs before pushing to production.
- Pin image tags: Use a specific digest (
image@sha256:...) or version tag rather thanlatest—latestcan change under you silently. - Read-only root filesystem: Set
--read-onlyon containers to prevent runtime writes to the filesystem.
Containers in a DevOps Pipeline
A typical container workflow in a CI/CD pipeline:
- Developer pushes code to Git
- CI runs tests and builds a Docker image tagged with the commit SHA
- CI scans the image for vulnerabilities
- CI pushes the image to a container registry (e.g., ECR)
- CD updates the deployment manifest with the new image tag
- Kubernetes (or another orchestrator) pulls and runs the new image