Menu

Topic 3 of 8

Containerization Basics

Learn Containerization Basics for free with explanations, exercises, and a quick test (for Backend Engineer).

Published: January 20, 2026 | Updated: January 20, 2026

Why this matters

Backend Engineers ship services that must run the same way on laptops, CI, and production. Containers provide that consistency and make CI/CD pipelines faster and safer. You will use containers to: package services, run tests in isolated environments, tag versions for releases, push images to registries, and deploy to orchestrators.

  • Real tasks you will do: build Docker images in CI, scan for vulnerabilities, push to a registry, run integration tests with dependent services via Compose, and set health checks for production.

Who this is for

  • Backend Engineers and Platform/DevOps beginners who need a reliable way to run services across environments.
  • Anyone integrating applications into CI/CD pipelines.

Prerequisites

  • Basic CLI skills
  • Ability to run local commands with Docker or another OCI-compatible runtime
  • Basic knowledge of your service runtime (e.g., Python, Node.js, Go)

Concept explained simply

A container is a lightweight, isolated process with everything it needs to run: code, runtime, and dependencies. An image is the read-only template used to start containers. You build an image from a Dockerfile (or similar) and run containers from that image.

Mental model

Think of an image as a frozen recipe card with all ingredients and instructions. A container is the dish cooked from that recipe. Layers in the image are like pre-prepped ingredients; if they don’t change, the kitchen reuses them to cook faster next time (build cache).

Key terms you should know

  • Image: Read-only template for containers. Built from layers.
  • Container: A running instance of an image.
  • Tag: A label for an image version (e.g., 1.2.0, latest).
  • Registry: Remote store for images (e.g., company registry).
  • Build context: Files sent to the builder. Controlled by .dockerignore.
  • Layer: A cached snapshot of filesystem changes.
  • Entrypoint/CMD: What runs when the container starts.
  • Port mapping: Expose container ports to the host (e.g., -p 8080:80).
  • Volume/bind mount: Persist or share data/config between host and container.
  • Healthcheck: A command to report if the container is healthy.

Worked examples

Example 1 — Minimal Flask service (Python)
# app.py
from flask import Flask
import os
app = Flask(__name__)
@app.get("/")
def hi():
    return {"msg": os.getenv("GREETING", "Hello" )}
if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)
# requirements.txt
flask==3.0.0
# Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENV GREETING=Hello-from-container
EXPOSE 5000
CMD ["python", "app.py"]

Build and run:

docker build -t flask-demo:1.0 .
docker run --rm -p 8080:5000 -e GREETING=Hi flask-demo:1.0
# curl http://localhost:8080 -> {"msg":"Hi"}
Example 2 — Production-focused Node.js (install only prod deps)
# server.js
const express = require('express');
const app = express();
app.get('/', (_, res) => res.json({msg: process.env.GREETING || 'Hello'}));
app.listen(3000, '0.0.0.0');
# package.json
{
  "name": "node-demo",
  "version": "1.0.0",
  "main": "server.js",
  "type": "module",
  "dependencies": {"express": "^4.19.2"}
}
# Dockerfile
FROM node:18-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev

FROM node:18-alpine
WORKDIR /app
ENV NODE_ENV=production GREETING=Hello-from-node
COPY --from=deps /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

Benefit: smaller runtime image, faster cold starts and fewer dev-only packages in production.

Example 3 — Go static binary with multi-stage
# main.go
package main
import (
  "fmt"; "net/http"; "os"
)
func main(){
  http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request){
    fmt.Fprintf(w, "{\"msg\":\"%s\"}", os.Getenv("GREETING"))
  })
  http.ListenAndServe(":8080", nil)
}
# Dockerfile
FROM golang:1.22 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o app

FROM scratch
ENV GREETING=Hello-from-go
EXPOSE 8080
COPY --from=build /src/app /app
ENTRYPOINT ["/app"]

Result: very small, minimal attack surface. Ensure your binary is static (CGO_ENABLED=0).

Step-by-step: Dockerize a service

  1. Choose a base image that matches your runtime (e.g., python:3.11-slim). Prefer slim or alpine variants if compatible.
  2. Set a working directory (WORKDIR /app) and copy only what you need early (requirements files) to leverage cache.
  3. Install dependencies using no-cache flags where possible.
  4. Copy application code after dependencies to avoid busting cache on every change.
  5. Configure runtime: environment variables, EXPOSE, HEALTHCHECK, and a clear CMD/ENTRYPOINT.
  6. Run the container with port mappings and env vars. Test with curl.
  7. Tag and push to your registry when ready (e.g., myorg/service:1.0.0).
What goes into .dockerignore?
  • venv/, node_modules/, build/
  • .git/, .idea/, .vscode/
  • *.log, *.tmp
  • .env, secrets, local caches

Common mistakes and self-check

  • Sending huge build contexts: Add a proper .dockerignore. Self-check: run docker build with --progress=plain and ensure small context size.
  • Hardcoding secrets: Do not bake secrets into images. Use environment variables or secret managers.
  • Running as root: Create a non-root user. Self-check: docker exec whoami shows your app user.
  • Unpinned dependencies: Pin versions for reproducible builds.
  • No healthcheck: Add HEALTHCHECK to detect broken containers.
  • Exposing wrong ports: Ensure EXPOSE matches your app and -p mappings are correct.
  • Layer cache misses: Copy dependency files before source code so small code changes don’t reinstall all deps.
Quick self-audit commands
# See image layers
docker history <image:tag>
# Inspect metadata (env, exposed ports, user)
docker inspect <image:tag>
# Check health status
docker ps --format 'table {{.Names}}\t{{.Status}}'

Exercises

Mirror of the interactive exercises below. Do them locally and validate with the expected outputs.

Exercise 1 — Create and run your first containerized web service
  1. Create files: app.py, requirements.txt, Dockerfile (use Python example above).
  2. Build image: docker build -t hello-flask:1.0 .
  3. Run: docker run --rm -p 8080:5000 -e GREETING=LuvvHelp hello-flask:1.0
  4. Test: curl http://localhost:8080 should return your greeting in JSON.
  • Checklist:
    • Image builds without errors
    • Container responds on port 8080
    • Changing GREETING changes the response
Exercise 2 — Optimize and harden your image
  1. Add a .dockerignore that excludes venv/, .git/, node_modules/, *.log, .env.
  2. Modify Dockerfile: use python:3.11-slim, add a non-root user, use pip --no-cache-dir, and add a HEALTHCHECK.
  3. Rebuild with a new tag: hello-flask:1.1
  4. Run and verify whoami is not root and container becomes healthy.
  • Checklist:
    • Image size is smaller than initial build
    • whoami prints your non-root user
    • docker ps shows (healthy) after a short time

Practical projects

  • Containerize a small CRUD API (any language) with a database sidecar using Compose. Add healthchecks for both.
  • Create a CI job that builds, tags (commit SHA), and pushes your image to a registry mirror or local registry.
  • Implement multi-stage builds that run unit tests in the builder stage and copy only the final artifacts to the runtime stage.

Learning path

  • Start: Single-service containerization (this lesson)
  • Next: Docker Compose for local multi-service development
  • Then: Image scanning and SBOM in CI
  • Finally: Deploy to an orchestrator (Kubernetes basics)

Next steps

  • Refactor one of your existing services to run as non-root with a healthcheck.
  • Add a proper .dockerignore to all your repos.
  • Adopt consistent image tagging: app:1.2.0, app:1, app:latest, app:commit-sha.

Mini challenge

Ship a lean image
  1. Turn any of the worked examples into a multi-stage build.
  2. Target: cut image size by at least 30%.
  3. Prove it: show docker images before and after, and that the service still passes a curl smoke test.
About the quick test and saving progress

The quick test is available to everyone. If you are logged in, your progress is saved automatically.

Practice Exercises

2 exercises to complete

Instructions

  1. Create app.py and requirements.txt as in Example 1. Add a Dockerfile that installs dependencies, copies code, sets GREETING, and runs the app.
  2. Build the image:
    docker build -t hello-flask:1.0 .
  3. Run the container:
    docker run --rm -p 8080:5000 -e GREETING=LuvvHelp hello-flask:1.0
  4. In a second terminal, test:
    curl http://localhost:8080
Expected Output
{"msg":"LuvvHelp"}

Containerization Basics — Quick Test

Test your knowledge with 10 questions. Pass with 70% or higher.

10 questions70% to pass

Have questions about Containerization Basics?

AI Assistant

Ask questions about this tool