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:
-
“Docker” appears constantly. GitHub repos have Dockerfiles. AWS / GCP / Azure have container services (ECS, GKE, AKS). Knowing what’s there demystifies them.
-
It explains “works on my machine” historically. Pre-containers, deploys were fragile. Containers + cloud changed software industrially. Understanding the WHY matters.
-
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:
| Term | What it is |
|---|---|
| Image | A template / blueprint. A read-only bundle of files + metadata. Think: a frozen snapshot of an app + its environment. |
| Container | A RUNNING instance of an image. You can have many containers from one image (each is independent). |
| Registry | A 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-appNow 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:latestFrom 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 upNow you have:
- Your app running in one container on port 3000
- Postgres running in another container, accessible from your app as
db:5432 - Volume
dbdatapersists database files between restarts
Tear down:
docker-compose downThis 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:16What happened:
- Docker downloaded the official
postgres:16image (a few hundred MB) - Started a container in the background (
-dfor 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
| Aspect | Container | Virtual Machine |
|---|---|---|
| What’s virtualized | The app + its libs | The whole OS |
| Boot time | Milliseconds to seconds | Minutes |
| Resource overhead | MBs of RAM, ~zero CPU | GBs of RAM, real CPU cost |
| Density | 100s per host | 5-20 per host |
| OS isolation | Shares host kernel | Full OS in each VM |
| Best for | Apps, microservices, dev environments | Strong 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
USERdirective in Dockerfiles. - Use minimal base images. Alpine (~5MB) over Ubuntu (~70MB). Distroless (~10MB) for ultra-minimal.
- Don’t expose more ports than necessary. Each
EXPOSEis 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 runcreates a NEW container each time. Repeated runs accumulate containers (visible viadocker ps -a). Clean up withdocker container prune. -
Image size matters for cold starts. A 5GB image takes longer to pull than 50MB. Use Alpine bases, multi-stage builds,
.dockerignore. -
.dockerignoreis critical. Without it,COPY . .copies yournode_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 ciBEFORECOPY . .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/amd64if needed. -
Networking inside Docker Compose is automatic. Services can talk to each other by SERVICE NAME (the
db:5432in the example). Outside the compose network, they’re isolated. -
Volumes persist data across container restarts.
docker run -v dbdata:/var/lib/postgresql/data postgresmounts 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
-eflags, ENV directives, or .env files. Multiple ways; pick one. Compose’senvironment:block is the common form. -
docker logs <container>shows stdout/stderr. Run logs follow with-f. Useful for debugging. -
docker exec -it <container> bashopens 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
CMDdetermines 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=alwaysrestarts forever;--restart=on-failurerestarts only on errors. -
The Docker daemon runs as root. Members of the
dockergroup 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.dockerignorecan take minutes just to send the context. -
docker system prune -areclaims 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 . .BEFORERUN npm ci- Using full Ubuntu when Alpine works
- No multi-stage build
- Missing
-
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-composeis being slowly renamed todocker 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
- What is “the cloud”? 🟩 — containers run in the cloud
- Operating systems — overview 🟩 — containers share the host OS
- What is hosting? đźź©
- Vercel 🟩 🟦 — uses containers internally
- Serverless functions đźź©
- Postgres 🟩 🟦 — commonly run as a container locally
- Mobile development — overview 🟩
- CD 🟩 — builds + tests often run in containers
- Windows dev environment 🟩 — Docker on Windows / WSL
- Glossary: Container, Docker, Image, Kubernetes
Sources
- Docker docs — canonical reference
- Docker — Get started — introductory tutorial
- Kubernetes docs — for orchestration
- Play with Docker — free in-browser environment
- Awesome Docker — curated list of resources
- The Twelve-Factor App — design principles often realized via containers