Containers and Docker
Containers and Docker
Containers and Docker
Containers let you bundle an application plus its runtime dependencies (language runtime, libraries, OS packages) into an immutable image. You can then run that image the same way on a laptop, CI server, or production cluster. Think of an image as a versioned filesystem snapshot + start command.
1. Why Containers? (Problem → Solution)
| Problem (Pre-Containers) | Impact | Container Benefit |
|---|---|---|
| "Works on my machine" differences | Deployment failures | Image has explicit dependency versions |
| Snowflake servers manually configured | Drift over time | Immutable rebuildable images |
| Scaling manual + slow | Cannot react to load spikes fast | Fast instantiation (seconds) |
| Mixed dependencies (Python 3.11 vs 3.10) | Conflicts / breakages | Isolated filesystem namespaces |
Core idea: Build once (CI), run many times (dev/staging/prod) without rebuild.
2. Key Vocabulary
| Term | Definition |
|---|---|
| Dockerfile | Declarative recipe to build an image layer by layer |
| Image | Read-only layered snapshot (immutable) |
| Container | Running instance of an image with writable layer |
| Registry | Remote store for images (Docker Hub, GHCR, ECR) |
| Layer Cache | Reuse of previously built steps to speed builds |
| Multi-stage Build | Multiple FROM stages to discard build tools in final image |
3. Layering Mental Model
Each Dockerfile instruction (mostly) creates a new layer. If a line changes, all subsequent layers must rebuild.
graph TD
A[FROM python:3.12-slim] --> B[RUN apt-get install build-essentials]
B --> C[WORKDIR /app]
C --> D[COPY requirements.txt .]
D --> E[RUN pip install -r requirements.txt]
E --> F[COPY src/ ./src]
F --> G[CMD python -m app]
4. Minimal Python Example
FROM python:3.12-slim AS base
WORKDIR /app
# 1. Install only build deps needed for compilation (if any)
RUN apt-get update && apt-get install -y --no-install-recommends build-essential \
&& rm -rf /var/lib/apt/lists/*
# 2. Dependency layer (separate to leverage caching)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 3. Copy application source last
COPY . .
ENV PORT=8000 PYTHONUNBUFFERED=1
EXPOSE 8000
CMD ["python", "main.py"]
Run locally:
docker build -t myapp:local .
docker run --rm -p 8000:8000 --env PORT=8000 myapp:local
5. Multi-Stage Build (Node Example)
Keep the final runtime image lean by compiling/building in an earlier stage.
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
FROM node:20-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:20-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/dist ./dist
COPY package*.json ./
COPY --from=deps /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/index.js"]
6. Security & Hygiene
| Practice | Why | Tip |
|---|---|---|
| Pin base image versions | Avoid silent major upgrades | python:3.12-slim not latest |
| Use slim/alpine base | Smaller attack surface | Verify needed libc features |
| Drop root privileges | Limit damage if exploited | USER app after creating non-root user |
| Scan images | Catch CVEs early | Trivy, Grype in CI |
| Minimal layers | Reduce size & complexity | Combine related RUN commands |
| .dockerignore | Faster builds + no secrets | Exclude .git, tests, local data |
Example .dockerignore:
.git
node_modules
__pycache__
*.log
.env
tests
7. Environment Variables & Config
12-Factor principle: config outside the image. Supply secrets and environment-specific values (DATABASE_URL, API_KEY) at runtime, not baked into the image. For local dev: .env file + --env-file (never commit secrets).
8. Persistent Data & State
Containers themselves are ephemeral. Use volumes for: * Databases (during development only; in prod use managed services or stateful sets) * Caches you don't want to rebuild constantly
Example: docker run -v mydata:/var/lib/postgresql/data postgres:16
9. Networking Basics
- Default bridge network: containers reachable by IP.
- Named networks: service discovery via container names.
- Ports:
-p 8000:8000maps host:container.
docker compose simplifies multi-container dev (app + db + cache) with a YAML file describing services and networks.
10. Observability in Containers
- Send logs to stdout/stderr (container platform collects).
- Health checks:
HEALTHCHECK CMD curl -f http://localhost:8000/health || exit 1. - Add minimal metrics endpoint or integrate OpenTelemetry for richer traces.
11. Common Pitfalls
| Pitfall | Symptom | Fix |
|---|---|---|
| Copying entire repo early | Rebuilds slow for small changes | Copy only manifests (package.json, requirements.txt) first |
Using latest tag |
Unpredictable prod behavior | Pin versions + renovate updates |
| Shipping dev dependencies | Bigger images, security risk | Multi-stage & prune dev deps |
| Storing secrets in image | Secret leakage | Pass via env vars / secret manager |
| Running everything as root | Escalation risk | Create user & switch |
12. Pushing to a Registry (Flow)
sequenceDiagram
participant Dev
participant CI as CI Pipeline
participant Reg as Container Registry
participant Prod
Dev->>CI: Push commit (Dockerfile)
CI->>CI: Build & tag (myapp:1.2.0)
CI->>Reg: Push image layers
Prod->>Reg: Pull myapp:1.2.0
Prod->>Prod: Run container(s)
Tagging strategy: myapp:<git-sha> for unique builds + myapp:1.2.0 for releases + myapp:latest pointing to stable if desired.
13. Performance Tips
- Use smaller base (e.g.,
alpine,distrolessfor final stage). - Leverage build cache: order instructions from least to most frequently changing.
- Clean build artifacts:
rm -rf /var/lib/apt/lists/*after apt installs. - Multi-arch builds via
docker buildxif you target ARM + x86.
14. Minimal Compose File Example
version: '3.9'
services:
api:
build: .
ports:
- "8000:8000"
environment:
DATABASE_URL: postgres://postgres:postgres@db:5432/app
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: postgres
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
Run: docker compose up --build.
15. Checklist
-
.dockerignoreexcludes large/unneeded files - Multi-stage build keeps runtime image lean
- Non-root user configured (if feasible)
- Base image version pinned
- Healthcheck defined (or readiness probe in orchestration)
- Environment config passed at run time
- Image scanned for CVEs in CI
- Build cache effective (manifests copied before source)
- Logs to stdout/stderr (no local file writes)
- Tagging strategy documented
16. Further Resources
- Docker docs – https://docs.docker.com/
- Podman (daemonless alternative)
- Trivy image scanner – https://aquasecurity.github.io/trivy/
- Snyk container scanning
- Google distroless images
17. Quick Reference (Cheat Table)
| Requirement | Dockerfile Hint |
|---|---|
| Fast rebuilds | Separate deps & source layers |
| Smaller image | Multi-stage + slim base |
| Security | Non-root, scan, pin versions |
| Deterministic | Avoid apt-get upgrade without version pins |
| Configurable | Use ENV + runtime env vars |