Containers & Docker

Status: 🟩 COMPLETE Last updated: 2026-06-19 Plain-English tagline: A container is a portable bundle that includes your app PLUS everything it needs to run (OS libraries, runtimes, dependencies) — so it runs identically on any machine, anywhere. Docker is the dominant tool for creating + running containers.


In plain English

The classic problem in software: “it works on my machine.” Your code runs locally; ships to a server; breaks. Different OS, different library versions, different config. Hours of debugging.

A container is a way to bundle your app + ALL its dependencies (OS libraries, runtime, env vars, configuration) into a single unit. The container then runs IDENTICALLY on any machine that supports the container runtime. The phrase: “ship the whole environment, not just the code.”

Docker is the tool that pioneered + popularized containers. The verb “to Docker” something means to package it as a container. There are alternatives (Podman, containerd) but Docker is overwhelmingly dominant.

A container is similar to a virtual machine but much LIGHTER:

  • A VM virtualizes the whole hardware + OS. Boots in minutes; uses GBs of RAM.
  • A container shares the host’s OS kernel; only the app + its libraries. Starts in milliseconds; uses MBs of RAM.

The mental model: a container is a SHIPPING CONTAINER for software. Standardized, portable, predictable contents.

For Bible Quest-style work, you DON’T usually touch containers directly:

  • Vercel runs your code in containers internally (you don’t see this)
  • Supabase runs Postgres in containers internally (you don’t see this)
  • Local development is usually npm run dev, not Docker

But you’ll encounter Docker:

  • Running a local Postgres for development (docker run postgres)
  • Self-hosting tools (Plausible Analytics, n8n, Ghost CMS)
  • Reading job postings (Docker shows up constantly)
  • AI tools that use containers internally (Claude Code, some MCP servers)

This entry explains what containers ARE so you can recognize them; for hands-on Docker use, see specific tutorials.


Why it matters

Three concrete reasons even a non-coder benefits:

  1. “Docker” appears constantly. GitHub repos have Dockerfiles. AWS / GCP / Azure have container services (ECS, GKE, AKS). Knowing what’s there demystifies them.

  2. It explains “works on my machine” historically. Pre-containers, deploys were fragile. Containers + cloud changed software industrially. Understanding the WHY matters.

  3. The “image” / “container” / “registry” vocabulary is universal. Whenever you see these words, the concepts in this entry apply.

The trade-off: containers add complexity at small scale. For solo projects on Vercel + Supabase, you can ignore them entirely. Worth knowing once a project grows beyond that.


The core vocabulary

Three terms that always appear together:

TermWhat it is
ImageA template / blueprint. A read-only bundle of files + metadata. Think: a frozen snapshot of an app + its environment.
ContainerA RUNNING instance of an image. You can have many containers from one image (each is independent).
RegistryA storage system for images. Docker Hub is the most-used. Companies often run private registries (AWS ECR, GitHub Container Registry).

The flow:

Dockerfile (a recipe)
  ↓ docker build
Image (the snapshot, stored in registry)
  ↓ docker run
Container (a running process)

A Dockerfile is a recipe; build it to get an image; run an image to get a container.


A concrete example: containerizing a Next.js app

A simple Dockerfile for a Next.js app:

# Start from a base image with Node.js installed
FROM node:22-alpine
 
# Set the working directory inside the container
WORKDIR /app
 
# Copy package files
COPY package*.json ./
 
# Install dependencies
RUN npm ci
 
# Copy the rest of the code
COPY . .
 
# Build the production app
RUN npm run build
 
# Expose the port the app uses
EXPOSE 3000
 
# Command to run when the container starts
CMD ["npm", "start"]

Build the image:

docker build -t my-app .

Run a container:

docker run -p 3000:3000 my-app

Now http://localhost:3000 runs your app — INSIDE a container, isolated from your machine. The container thinks it’s running on a fresh Alpine Linux with Node 22; nothing else from your machine is visible to it.

You can run multiple containers (docker run again with different ports). Each is independent.

Push the image to a registry:

docker push my-username/my-app:latest

From any other machine: docker pull + docker run and the SAME bundle runs identically.


What containers solve

The “dependency hell” of pre-container days:

  • “It needs Python 2.7 but the server has Python 3.9”
  • “It needs libssl 1.0 but Ubuntu 22 ships libssl 3.0”
  • “It needs a specific font that’s not on the deploy machine”
  • “It needs a database initialization step that no one documented”

A container bundles ALL of this. The image is the dependency declaration AND the deployment artifact. The “config drift” problem largely disappears.

The benefits compound at scale:

  • Reproducible builds — the same image produces the same behavior anywhere
  • Easy rollback — keep old images; rerun them if needed
  • Microservices — each service in its own container; standardized communication
  • Multi-tenant hosts — one server runs many containers from different teams safely
  • CI/CD simplicity — build once, deploy the image everywhere

Docker Compose — multiple containers together

Real apps often have multiple components (app + database + cache + worker). docker-compose runs them together via a single YAML file:

# docker-compose.yml
version: "3"
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgresql://user:password@db:5432/mydb
    depends_on:
      - db
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
      POSTGRES_DB: mydb
    volumes:
      - dbdata:/var/lib/postgresql/data
volumes:
  dbdata:

Run:

docker-compose up

Now you have:

  • Your app running in one container on port 3000
  • Postgres running in another container, accessible from your app as db:5432
  • Volume dbdata persists database files between restarts

Tear down:

docker-compose down

This is widely used for LOCAL DEVELOPMENT — running the full stack locally without installing Postgres / Redis / etc. natively on your machine.


Kubernetes — orchestrating containers at scale

A container is fine for ONE process. Real production systems run hundreds or thousands. Kubernetes (k8s) is the orchestrator that:

  • Decides which machine each container runs on
  • Restarts containers that crash
  • Scales up / down based on load
  • Routes traffic between containers
  • Updates containers without downtime
  • Manages secrets, configuration, networking

For Bible Quest-scale: Kubernetes is overkill. Vercel + Supabase handle all this transparently. Kubernetes shows up when:

  • You’re running raw cloud infrastructure
  • You have many services to coordinate
  • You need fine control over deployment strategies
  • You have a dedicated platform team

Major managed Kubernetes services:

  • AWS EKS (Elastic Kubernetes Service)
  • GCP GKE (Google Kubernetes Engine)
  • Azure AKS (Azure Kubernetes Service)

Simpler alternatives:

  • Fly.io — runs containers globally without exposing Kubernetes
  • Railway, Render — PaaS that uses containers underneath
  • AWS Fargate — managed container hosting without Kubernetes

For solo developers in 2026, “Kubernetes” is mostly a thing to know exists, not a thing to use.


A concrete example: running Postgres locally

This is where Docker pays off for casual users.

Want to run Postgres for local development without installing it natively:

docker run -d \
  --name local-postgres \
  -e POSTGRES_PASSWORD=mypass \
  -p 5432:5432 \
  postgres:16

What happened:

  • Docker downloaded the official postgres:16 image (a few hundred MB)
  • Started a container in the background (-d for detached)
  • Named it local-postgres
  • Set the password
  • Forwarded port 5432 from your machine to the container

Connect with any Postgres client: postgresql://postgres:mypass@localhost:5432/postgres. No native install. No system pollution. Stop the container, remove it, your machine is unchanged.

Same pattern for Redis, MongoDB, MySQL, RabbitMQ, n8n, etc. The Docker Hub has official images for almost every service.

This is “Docker for local dev” — the most accessible use case.


Containers vs virtual machines

AspectContainerVirtual Machine
What’s virtualizedThe app + its libsThe whole OS
Boot timeMilliseconds to secondsMinutes
Resource overheadMBs of RAM, ~zero CPUGBs of RAM, real CPU cost
Density100s per host5-20 per host
OS isolationShares host kernelFull OS in each VM
Best forApps, microservices, dev environmentsStrong isolation, different OSes, kernel-level workloads

A practical analogy: containers are apartments in one building (shared foundation); VMs are detached houses (everything separate).

For modern web apps, containers are the right unit of deployment. VMs still matter for compliance / isolation requirements.


Container security basics

Containers are NOT magical security boundaries. They share the host kernel; a kernel exploit can escape any container.

Modern best practices:

  • Don’t run containers as root. Use the USER directive in Dockerfiles.
  • Use minimal base images. Alpine (~5MB) over Ubuntu (~70MB). Distroless (~10MB) for ultra-minimal.
  • Don’t expose more ports than necessary. Each EXPOSE is a potential attack surface.
  • Scan images for vulnerabilities. Tools: Trivy, Snyk, GitHub Container Scanning.
  • Don’t bake secrets into images. Use runtime env vars or secret managers.

Common gotchas

  • An image is immutable; a container is not. You can modify a running container’s filesystem; those changes vanish when the container is removed.

  • docker run creates a NEW container each time. Repeated runs accumulate containers (visible via docker ps -a). Clean up with docker container prune.

  • Image size matters for cold starts. A 5GB image takes longer to pull than 50MB. Use Alpine bases, multi-stage builds, .dockerignore.

  • .dockerignore is critical. Without it, COPY . . copies your node_modules (huge) and .git (sensitive). Always include .dockerignore.

  • Layer caching speeds builds dramatically. Docker caches each Dockerfile step. If you change one line, only that step + later steps rebuild. Order matters — put rarely-changing steps first.

  • COPY package*.json ./ && RUN npm ci BEFORE COPY . . is the canonical pattern. Dependencies change less often than code; this maximizes cache reuse.

  • Multi-stage builds reduce final image size. Build in a heavy image; copy artifacts to a slim image:

    FROM node:22 AS build
    WORKDIR /app
    COPY . .
    RUN npm ci && npm run build
     
    FROM node:22-alpine
    WORKDIR /app
    COPY --from=build /app/.next ./.next
    COPY --from=build /app/package*.json ./
    RUN npm ci --omit=dev
    EXPOSE 3000
    CMD ["npm", "start"]
  • Docker on Mac and Windows runs via a VM. Docker Desktop is a heavyweight UI on top of a Linux VM. On Linux native, Docker runs without virtualization.

  • WSL2 on Windows runs containers efficiently. Docker Desktop integrates with WSL2 to avoid a separate VM layer.

  • Containers on ARM Macs need ARM-built images. Most popular images publish multi-architecture variants; some legacy ones are AMD64-only. Use --platform=linux/amd64 if needed.

  • Networking inside Docker Compose is automatic. Services can talk to each other by SERVICE NAME (the db:5432 in the example). Outside the compose network, they’re isolated.

  • Volumes persist data across container restarts. docker run -v dbdata:/var/lib/postgresql/data postgres mounts a named volume. Critical for databases.

  • Don’t store data inside the container’s writable layer. It vanishes when the container is removed. Always use volumes for persistent data.

  • Environment variables go in -e flags, ENV directives, or .env files. Multiple ways; pick one. Compose’s environment: block is the common form.

  • docker logs <container> shows stdout/stderr. Run logs follow with -f. Useful for debugging.

  • docker exec -it <container> bash opens a shell INSIDE a running container. Useful for inspecting state.

  • Containers can run forever or briefly. A web server runs until stopped; a database init script runs once and exits. The CMD determines this.

  • Health checks let Docker know when a container is “ready.” Useful for orchestration; orchestrators won’t route traffic until health checks pass.

  • Restart policies determine what happens on crash. --restart=always restarts forever; --restart=on-failure restarts only on errors.

  • The Docker daemon runs as root. Members of the docker group can effectively become root via container manipulation. Treat docker group membership as a privileged credential.

  • Build context (.) sends everything in the directory to Docker. A large project with no .dockerignore can take minutes just to send the context.

  • docker system prune -a reclaims disk space. Containers, images, networks, volumes accumulate. Periodically clean up.

  • Image registries cost money at scale. Docker Hub free tier limits pulls and storage. AWS ECR, GitHub Container Registry, Google Artifact Registry are common alternatives.

  • Container security is “shared kernel” — DIFFERENT from VM. A breakout from a container compromises the host. For high-security workloads, use VMs or sandboxed runtimes (gVisor, Firecracker).

  • GitHub Actions natively supports containers. Action runners are containers; you can run your build inside a custom container; you can build/push images via Actions.

  • Containers and serverless are different abstractions. A container is “a process you start”; serverless is “a function that runs in response to an event, scaled automatically.” Modern serverless platforms (Lambda, Cloud Run) often run containers underneath, but the abstraction is different.

  • Don’t confuse “containerized” with “cloud-native.” Cloud-native is an architecture pattern (stateless, scalable, observable); containerization is a packaging mechanism. They’re often together but distinct.

  • AI tools sometimes generate Dockerfiles that build slowly. Watch for:

    • Missing .dockerignore
    • COPY . . BEFORE RUN npm ci
    • Using full Ubuntu when Alpine works
    • No multi-stage build
  • Local development with Docker can be SLOWER than native. On Mac/Windows, file system performance through the VM layer is degraded. Strategies: use volumes carefully, or run native on Linux.

  • docker-compose is being slowly renamed to docker compose (no hyphen). New CLI integrated into Docker itself. Both forms work as of 2026.

  • Containers don’t replace good operational practice. Logging, monitoring, alerting, backup, secrets — all still needed. Containers just provide a consistent unit to apply them to.

  • Vercel functions are containers underneath. Same is true for Lambda, Cloud Run, Cloudflare Workers (for workloads using container mode). The abstraction is “function”; the implementation is “container.”

  • Containers are EXCELLENT for tutorials. A README that says “run docker run something/example” works for everyone, regardless of OS. No “install these prerequisites” friction.


When to use containers directly

For Bible Quest-style projects: rarely.

You’d reach for Docker when:

  • Running a local development service (Postgres, Redis) without native install
  • Self-hosting a tool (Plausible, n8n, Ghost) on your own server
  • Following a tutorial that assumes Docker
  • Distributing your app to others who need to run it
  • Working on a team that already uses Docker

For most webapp work: Vercel + Supabase abstract containers away. Knowing they exist + roughly how they work is enough.


See also


Sources