Module 1 · Start with Docker

Session 1 · Getting started

Install Docker and understand the basic parts

By the end of this session, you can:

  • Install Docker Desktop or Docker Engine for your operating system.
  • Explain the difference between the Docker CLI and the Docker Engine.
  • Check that Docker is installed and ready before starting a project.

In simple words: First make sure Docker is installed and that your terminal can talk to the Docker Engine.

Part 1 - Theory

Core concepts and mental model

Docker has two pieces you need to know first. The CLI is the command-line tool you type into. The Docker Engine is the service that creates and runs containers. Docker Desktop packages both together for Windows and macOS.

You typeDocker CLI
→
Docker Enginedoes the work
→
Containerruns your app

On Windows, Docker Desktop commonly uses WSL 2 behind the scenes. On macOS, Docker Desktop provides a small Linux environment. On Linux, Docker Engine can run directly. You do not need to understand the virtual machine details yet.

Five words to remember

  • CLI: the commands you type, such as docker ps.
  • Engine: the background service that does Docker work.
  • Image: a saved package or blueprint for an app.
  • Container: a running copy of an image.
  • Registry: a place that stores and shares images.

Part 2 - Demonstration

Build it live

After installing Docker Desktop or Docker Engine, open a new terminal and run these commands:

docker --version
docker version
docker run --rm hello-world

docker --version checks the command. docker version checks the command and the Engine. The hello-world command proves that the full setup can download and run a container.

If Docker says that it cannot connect to the daemon, start Docker Desktop or start the Docker service. On Windows, also check that your WSL distribution is enabled in Docker Desktop settings.

Part 3 - Practice

  1. Install Docker using the official instructions for your operating system.
  2. Run docker version and write down whether both Client and Server are shown.
  3. Run hello-world and explain each part of the command: run, --rm, and the image name.

Answer key:

The CLI sends a request to the Engine. The Engine downloads the image if needed, creates a container, starts it, and reports the result. Root or administrator access does not install Docker or automatically connect a different terminal environment to Docker.

Quick questions:

What should you do if docker is not recognized?

Install Docker or add its CLI to the current environment's PATH; changing to root does not install it.

What does hello-world prove?

It proves that the CLI, Engine, image download, and container startup are working together.

Part 4 - Mini Challenge

Project task: Add a setup section to your capstone README with installation links, docker version, and the expected hello-world result.

Wrap Up

Start by checking the complete path: terminal → Docker CLI → Docker Engine → container.

Quick Reference - Session 1

Remember: Start by checking the complete path: terminal → Docker CLI → Docker Engine → container.

Session 2 · Mental model

Why containers exist

By the end of this session, you can:

  • Explain the difference between an image, a container, and a host.
  • Recognize the isolation and portability that containers provide.
  • Choose a container boundary for a small application.

In simple words: A container is a small, separate box for running one program and its dependencies.

Part 1 - Theory

Core concepts and mental model

A container is an isolated process that shares the host kernel while carrying its own filesystem view, environment, and process configuration. It is lighter than a virtual machine, but it is not a tiny virtual machine.

Sourceapplication + dependencies
→
Imageimmutable package
→
Containerrunning process

Images are reusable blueprints. Containers are runtime instances of those images. The host supplies the kernel and hardware resources, so a container should be treated as disposable unless its data is stored separately.

Part 2 - Demonstration

Build it live

Run a first container and remove it automatically when its process exits:

docker run --rm hello-world

The command downloads an image if needed, creates a container, starts its main process, prints output, and removes the stopped container because of --rm.

Part 3 - Practice

  1. Draw the path from your source code to a running container.
  2. Run hello-world twice and compare what Docker downloads the second time.
  3. Write down which data in your current project must survive container removal.

Answer key:

The source becomes an image during a build. A container runs that image. Persistent database files and user uploads should live in a volume or external storage, not only inside the writable container layer.

Quick questions:

Is an image the same thing as a running container?

No. An image is an immutable template; a container is a runtime instance.

What does a container share with its host?

Typically the host kernel and underlying hardware resources.

Part 4 - Mini Challenge

Project task: Create container-map.md with the process, port, configuration, and persistent data for an application you want to containerize.

Wrap Up

Containers package process environments; they do not remove the need to understand the host and the application boundary.

Quick Reference - Session 2

Remember: Containers package process environments; they do not remove the need to understand the host and the application boundary.

Session 3 · CLI essentials

The Docker CLI workflow

By the end of this session, you can:

  • Use the core commands for running, inspecting, and stopping containers.
  • Read container names, IDs, ports, and logs confidently.
  • Build a repeatable command-line troubleshooting loop.

In simple words: These commands let you start, see, enter, stop, and remove containers.

Part 1 - Theory

Core concepts and mental model

Most Docker work follows a short loop: create or run, observe, inspect, interact, stop, and remove. Learn the nouns first: images, containers, networks, and volumes.

docker image ls
docker ps
docker ps -a
docker logs <container>
docker inspect <container>
docker exec -it <container> sh
docker stop <container>
docker rm <container>

Part 2 - Demonstration

Build it live

Start an interactive web server in the background, then inspect it:

docker run -d --name demo-web -p 8080:80 nginx
docker ps
docker logs demo-web
docker inspect demo-web --format '{{.State.Status}}'
docker stop demo-web
docker rm demo-web

The host port is 8080 and the container port is 80. Port publishing is explicit; a process listening inside a container is not automatically reachable from the host.

Part 3 - Practice

  1. Run nginx with a memorable name and publish it on a different host port.
  2. Use docker exec to list the files in the web root.
  3. Stop and remove the container, then confirm it is absent from docker ps -a.

Answer key:

Use docker ps for running containers, docker ps -a for all containers, and docker logs for the main process output. Use docker inspect when you need the complete configuration.

Quick questions:

What is the difference between docker stop and docker rm?

stop asks a running container to exit; rm removes a stopped container record.

Why use a named container?

It makes commands and logs easier to read and repeat than using an auto-generated ID.

Part 4 - Mini Challenge

Project task: Create a command checklist for starting, checking, entering, stopping, and cleaning up your project container.

Wrap Up

The Docker CLI is an observability tool as much as it is a launch tool.

Quick Reference - Session 3

Remember: The Docker CLI is an observability tool as much as it is a launch tool.

Session 4 · Runtime behavior

Container lifecycle and process behavior

By the end of this session, you can:

  • Explain why the main process controls the life of a container.
  • Distinguish foreground, detached, exited, and restarting states.
  • Use exit codes and restart policies during diagnosis.

In simple words: A container normally lives only as long as its main program is running.

Part 1 - Theory

Core concepts and mental model

A container lives while its PID 1 process lives. If that process exits, the container stops even when other files remain. This is why a server image should start the server as its foreground process instead of hiding it behind a shell script that exits.

Use docker inspect to examine State.Status, State.ExitCode, and State.Health. Restart policies can recover from transient failures, but they should not hide a broken application.

Part 2 - Demonstration

Build it live

docker run -d --name lifecycle-demo --restart unless-stopped nginx
docker inspect lifecycle-demo --format '{{.State.Status}} {{.HostConfig.RestartPolicy.Name}}'
docker stop lifecycle-demo
docker start lifecycle-demo
docker rm -f lifecycle-demo

Use restart policies for expected runtime behavior, and use logs plus exit codes to find the underlying reason for repeated exits.

Part 3 - Practice

  1. Run a container whose command exits immediately and inspect its exit code.
  2. Compare docker start with docker run.
  3. Write one case where unless-stopped is useful and one case where it could mask a bug.

Answer key:

docker run creates and starts a new container. docker start starts an existing stopped container. A restart policy can restart a process, but it cannot fix a missing environment variable or invalid application command.

Quick questions:

What normally stops a container?

The container's main process exits, or an operator stops or kills it.

Should every container run a supervisor?

Usually no; one main foreground process keeps lifecycle and observability clear.

Part 4 - Mini Challenge

Project task: Document the expected start command, readiness signal, normal exit behavior, and restart policy for your application.

Wrap Up

Design the container around one clear main process and make failures visible.

Quick Reference - Session 4

Remember: Design the container around one clear main process and make failures visible.

Module 2 · Build images

Session 5 · Image model

Understand images and versions

By the end of this session, you can:

  • Describe how Docker image layers support caching and reuse.
  • Tell the difference between a mutable tag and an immutable digest.
  • Inspect an image before trusting it in a project.

In simple words: An image is the saved recipe; a tag is its name, and a digest is its exact identity.

Part 1 - Theory

Core concepts and mental model

An image is assembled from read-only layers. A Dockerfile instruction usually creates a layer or changes build metadata. Containers add a writable layer on top, which disappears when the container is removed.

Tags such as node:22 are convenient labels that can move. A digest identifies the exact content. For reproducible production builds, use deliberate versioning and record the image digest that was tested.

Part 2 - Demonstration

Build it live

docker pull nginx:alpine
docker image ls
docker image history nginx:alpine
docker image inspect nginx:alpine

History shows how the image was assembled. Inspect shows metadata such as the default command, exposed ports, architecture, and configured user.

Part 3 - Practice

  1. Inspect two tags of the same base image and record their sizes.
  2. Find the configured user and default command in an image inspect result.
  3. Explain why latest is a poor release strategy by itself.

Answer key:

A tag is a human-friendly pointer that may change. A digest is content-addressed and points to a specific image manifest. Pinning should be balanced with a process for intentional updates.

Quick questions:

Why do layers matter for builds?

Unchanged layers can be reused, reducing build time and transferred data.

What does a container writable layer contain?

Runtime changes made above the read-only image layers; it is not a durable data store.

Part 4 - Mini Challenge

Project task: Create image-inventory.md with each base image, tag or digest, purpose, and update owner for your capstone.

Wrap Up

Treat image provenance and versioning as part of application reliability.

Quick Reference - Session 5

Remember: Treat image provenance and versioning as part of application reliability.

Session 6 · Build instructions

Write your first Dockerfile

By the end of this session, you can:

  • Use FROM, WORKDIR, COPY, RUN, CMD, and EXPOSE correctly.
  • Separate build-time instructions from runtime behavior.
  • Build and run an image from a small application.

In simple words: A Dockerfile is a step-by-step recipe that turns your project into an image.

Part 1 - Theory

Core concepts and mental model

A Dockerfile is a readable build recipe. FROM chooses a base, WORKDIR sets the working directory, COPY adds project files, RUN executes build-time commands, and CMD supplies the default runtime command.

FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
EXPOSE 3000
CMD ["npm", "run", "start"]

Part 2 - Demonstration

Build it live

docker build -t notes-api:dev .
docker run --rm -p 3000:3000 notes-api:dev

Build context is the directory sent to the builder. Keep it small and intentional; it can contain source, configuration, credentials, and build artifacts if you do not exclude them.

Part 3 - Practice

  1. Containerize a small Node, Python, or Go HTTP service.
  2. Build it with a versioned local tag and run it on a non-default host port.
  3. Make sure the server binds to 0.0.0.0 inside the container.

Answer key:

The application must listen on the container interface, not only on localhost. Publishing a port maps host traffic to the container port, but it cannot make a process reachable if the process only binds to 127.0.0.1.

Quick questions:

Does EXPOSE publish a port to the host?

No. It documents intended ports; -p or Compose ports performs the host mapping.

Why copy package manifests before source code?

Dependency installation can remain cached when only application source changes.

Part 4 - Mini Challenge

Project task: Add a Dockerfile to the application selected for your capstone and record the exact build and run commands in its README.

Wrap Up

A good Dockerfile makes the build inputs, runtime command, and network contract explicit.

Quick Reference - Session 6

Remember: A good Dockerfile makes the build inputs, runtime command, and network contract explicit.

Session 7 · Build quality

Make builds fast and images small

By the end of this session, you can:

  • Keep secrets, dependencies, and generated files out of the build context.
  • Order Dockerfile instructions to make caching useful.
  • Use multi-stage builds to keep runtime images small.

In simple words: Good build order makes Docker faster, and a final stage keeps the running image small.

Part 1 - Theory

Core concepts and mental model

Build performance depends on cache stability. Copy files that change rarely before files that change often, and use .dockerignore to exclude node_modules, build output, Git metadata, local environment files, and credentials.

Multi-stage builds use one stage to compile or bundle and another stage to run the result. The final image receives only the runtime artifacts, not compilers, package caches, or source files that it does not need.

Part 2 - Demonstration

Build it live

# .dockerignore
node_modules
.git
.env*
dist
coverage

# Dockerfile pattern
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:22-alpine AS runtime
WORKDIR /app
COPY --from=build /app/dist ./dist
CMD ["node", "dist/server.js"]

Adapt the runtime stage to the framework and package manager you use. The important idea is to make the final stage contain only what production needs.

Part 3 - Practice

  1. Add a .dockerignore file and verify that .env files are not in the build context.
  2. Build once with plain progress output and identify which steps are cached on the second build.
  3. Compare the size of a development image and a multi-stage runtime image.

Answer key:

Cache invalidation should happen near the point where changing files are copied. Secrets should never be relied on being hidden by a later layer; keep them out of the context and use runtime secret delivery.

Quick questions:

Why is deleting a secret in a later Dockerfile layer insufficient?

Earlier layers may still contain the secret and remain available in image history.

What is the purpose of the final stage?

To produce a smaller runtime image with fewer tools and a smaller attack surface.

Part 4 - Mini Challenge

Project task: Refactor your application image into a production-oriented multi-stage build and document the size difference.

Wrap Up

Fast, small, safe images begin with disciplined build context and layer design.

Quick Reference - Session 7

Remember: Fast, small, safe images begin with disciplined build context and layer design.

Module 3 · Connect apps and keep data

Session 8 · Networking

Let containers talk to each other

By the end of this session, you can:

  • Explain bridge networks, published ports, and container DNS.
  • Connect two containers without hard-coding changing IP addresses.
  • Choose which ports should be public and which should stay private.

In simple words: Containers can talk to each other by name, while published ports let outside users reach them.

Part 1 - Theory

Core concepts and mental model

Containers on the same user-defined network can reach each other by service or container name. Host port publishing is for traffic crossing from outside the network into a container.

Browserhost port 8080
↓
Webcontainer port 3000
↓
Databaseprivate service port

A database normally needs no published host port when only the application connects to it. Fewer public ports mean fewer exposed entry points.

Part 2 - Demonstration

Build it live

docker network create app-net
docker run -d --name web --network app-net nginx
docker run --rm --network app-net busybox wget -qO- http://web
docker network inspect app-net

The second container resolves web through Docker-provided DNS. This is more stable than discovering a container IP manually.

Part 3 - Practice

  1. Create a network and attach two containers to it.
  2. Publish only the web container port to the host.
  3. Test name-based connectivity from one container to the other.

Answer key:

Use a user-defined network and address the dependency by its service or container name. Publish a port only when a host or external client needs access.

Quick questions:

Should an application connect to a database using localhost?

Not when they are separate containers; use the database service name on the shared network.

What does -p do?

It maps a host port to a container port.

Part 4 - Mini Challenge

Project task: Draw the public and private network edges for your capstone, including every published port and service hostname.

Wrap Up

Design networks around communication needs, not around the current IP addresses of containers.

Quick Reference - Session 8

Remember: Design networks around communication needs, not around the current IP addresses of containers.

Session 9 · Storage

Keep your data safe

By the end of this session, you can:

  • Choose between a named volume, a bind mount, and ephemeral container storage.
  • Persist database data across container replacement.
  • Plan ownership, permissions, backup, and cleanup for persistent data.

In simple words: Containers can be replaced; volumes keep important data safe when that happens.

Part 1 - Theory

Core concepts and mental model

Container writable layers are disposable. Named volumes are managed by Docker and are a strong default for database data. Bind mounts connect a host path to a container path and are useful for local development when you want live source changes.

Persistence is not the same as backup. A volume survives a container replacement, but it still needs a backup and restore procedure.

Part 2 - Demonstration

Build it live

docker volume create notes-data
docker run -d --name notes-db -v notes-data:/var/lib/postgresql/data postgres:16
docker volume inspect notes-data
docker stop notes-db
docker rm notes-db

Removing the container does not remove the named volume. Remove the volume only when its data is no longer needed or has been backed up.

Part 3 - Practice

  1. Run a stateful service with a named volume.
  2. Replace its container and verify that data remains available.
  3. Write a restore test, not just a backup command.

Answer key:

Use a named volume for durable local container data. Use a bind mount for developer-controlled files such as source code. In production, consider a managed database or storage system with tested backups.

Quick questions:

Does deleting a container delete a named volume?

No, not unless the volume is explicitly removed.

Why can bind mounts be risky in production?

They couple the container to host paths and permissions and can expose more host data than intended.

Part 4 - Mini Challenge

Project task: Add a persistence plan to your capstone README: volume path, backup frequency, restore steps, and data-loss boundary.

Wrap Up

Make data ownership and recovery explicit before calling an application production-ready.

Quick Reference - Session 9

Remember: Make data ownership and recovery explicit before calling an application production-ready.

Session 10 · Configuration

Add settings and protect secrets

By the end of this session, you can:

  • Separate image configuration from environment-specific values.
  • Use environment variables and env files without committing secrets.
  • Recognize why build arguments are not secret storage.

In simple words: Build the image once, then provide settings and secrets when the container starts.

Part 1 - Theory

Core concepts and mental model

Build an image once and configure it when it runs. Environment variables are appropriate for many non-secret settings, while credentials should use a secret-management mechanism supported by the deployment platform.

Never bake passwords into Dockerfiles, image layers, source control, or public logs. A build argument can appear in build metadata and should not be treated as confidential.

Part 2 - Demonstration

Build it live

docker run --rm   -e APP_ENV=development   -e API_BASE_URL=http://api:3000   notes-api:dev

For local work, keep values in an ignored .env file or a local secrets manager. For deployment, inject values through the platform rather than copying local files to a server.

Part 3 - Practice

  1. Classify each configuration value in your app as public, environment-specific, or secret.
  2. Add .env files to .gitignore and .dockerignore where appropriate.
  3. Search the image history and repository for accidental credentials.

Answer key:

Public defaults can live in code or the image. Environment-specific values belong at runtime. Secrets should be injected by a secure runtime mechanism and rotated independently of the image.

Quick questions:

Why should the same image run in staging and production?

Only runtime configuration should change, which reduces environment drift and makes releases repeatable.

Are environment variables always safe for secrets?

They can be exposed through process inspection or logs; use the deployment platform's secret facility when available.

Part 4 - Mini Challenge

Project task: Create an example.env template with safe placeholder names and write a secret injection plan for local, CI, and production environments.

Wrap Up

Configuration is a deployment concern; credentials deserve stronger handling than ordinary settings.

Quick Reference - Session 10

Remember: Configuration is a deployment concern; credentials deserve stronger handling than ordinary settings.

Module 4 · Run apps with Compose

Session 11 · Compose

Run several containers with Compose

By the end of this session, you can:

  • Describe services, networks, volumes, and environment values in Compose.
  • Start and stop a multi-container application from one file.
  • Keep the Compose file readable and safe to share.

In simple words: Compose puts several related containers into one readable application file.

Part 1 - Theory

Core concepts and mental model

Compose turns a multi-container setup into a declarative file. A service describes a container workload; Compose creates a private network so services can resolve each other by service name.

services:
  web:
    build: .
    ports:
      - "8080:3000"
    environment:
      API_URL: http://api:3000
  api:
    image: notes-api:dev
    depends_on:
      - db
  db:
    image: postgres:16
    volumes:
      - db-data:/var/lib/postgresql/data

volumes:
  db-data:

Part 2 - Demonstration

Build it live

docker compose config
docker compose up -d
docker compose ps
docker compose logs -f web
docker compose down

Use config to validate the resolved model before starting. Use down for containers and the network; do not add volume removal unless deleting local data is intentional.

Part 3 - Practice

  1. Write a Compose file with web, API, and database services.
  2. Validate it before starting and inspect the generated service configuration.
  3. Bring the stack down and confirm the named volume remains.

Answer key:

Compose models the application as services. The application should connect to db or another service name, not to a changing container IP. Keep secrets outside the committed Compose file.

Quick questions:

What does docker compose down normally remove?

Compose-created containers and networks, while named volumes remain unless you explicitly request volume removal.

Why run docker compose config?

It resolves and validates the final configuration before containers are launched.

Part 4 - Mini Challenge

Project task: Create compose.yaml for your capstone with at least an application service and one dependency.

Wrap Up

Compose makes the topology of a local or small hosted application executable and reviewable.

Quick Reference - Session 11

Remember: Compose makes the topology of a local or small hosted application executable and reviewable.

Session 12 · Application topology

Wait for services to be ready

By the end of this session, you can:

  • Separate startup order from service readiness.
  • Use healthchecks and dependency conditions where appropriate.
  • Avoid making the database publicly reachable without a reason.

In simple words: A service being started is not the same as being ready, so healthchecks make startup safer.

Part 1 - Theory

Core concepts and mental model

A process can be running before it is ready to accept requests. A database may need initialization, migrations, or recovery. Compose dependency order alone does not guarantee readiness, so use a healthcheck and application retry behavior.

services:
  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 10
  api:
    build: .
    depends_on:
      db:
        condition: service_healthy

Part 2 - Demonstration

Build it live

Start the stack and watch health state transitions:

docker compose up -d
docker compose ps
docker inspect <db-container> --format '{{json .State.Health}}'
docker compose logs db api

Healthchecks are signals for orchestration and diagnosis. They should be fast, deterministic, and representative of the service's ability to do useful work.

Part 3 - Practice

  1. Add a healthcheck to your database or API service.
  2. Make the application retry a dependency that is temporarily unavailable.
  3. Verify that no database port is published to the host unless you need local tooling access.

Answer key:

Use both sides of the contract: the dependency reports health, and the application handles temporary unavailability. This is more reliable than assuming that container start order equals application readiness.

Quick questions:

Does depends_on alone wait for a database to accept queries?

Not necessarily. A healthcheck or application-level retry is needed for readiness.

What makes a healthcheck useful?

It checks a meaningful, inexpensive readiness condition rather than merely checking that a process exists.

Part 4 - Mini Challenge

Project task: Add readiness behavior to your capstone and write a short failure test where the dependency starts slowly.

Wrap Up

Reliable startup is a protocol between services, not just a list of containers.

Quick Reference - Session 12

Remember: Reliable startup is a protocol between services, not just a list of containers.

Session 13 · Developer experience

Use Compose for daily development

By the end of this session, you can:

  • Use bind mounts and profiles without weakening the production image.
  • Separate development conveniences from production defaults.
  • Create repeatable logs, rebuild, and test commands.

In simple words: Use one repeatable set of commands so another developer can run, test, and debug the project.

Part 1 - Theory

Core concepts and mental model

Development often needs live source mounts, debuggers, hot reload, and test tools. Production needs immutable images, smaller permissions, and controlled configuration. Keep those concerns explicit with override files, profiles, or separate target stages.

A useful workflow makes the common commands obvious: start dependencies, run the app, view logs, execute tests, rebuild after dependency changes, and clean up safely.

Part 2 - Demonstration

Build it live

docker compose -f compose.yaml -f compose.dev.yaml up
docker compose exec api npm test
docker compose build --no-cache api
docker compose logs --tail=100 -f api

Use --no-cache as a diagnostic or deliberate refresh, not as the default for every build. Prefer a documented dev override over editing production settings by hand.

Part 3 - Practice

  1. Create a development override with a source bind mount and a local-only port.
  2. Add a command for running tests inside the service environment.
  3. Write a cleanup command that does not delete persistent data by accident.

Answer key:

Keep production-like defaults in the base Compose file and add development-only behavior in an override or profile. Name commands in the README so a new contributor can use the project without tribal knowledge.

Quick questions:

Why not use a source bind mount in the production image?

It couples runtime behavior to a host filesystem and can bypass the immutable artifact you tested.

When is --no-cache useful?

When diagnosing stale build inputs or intentionally refreshing every build step.

Part 4 - Mini Challenge

Project task: Add a development workflow section to your capstone README with five copy-paste commands and their expected result.

Wrap Up

Good container tooling reduces setup friction without blurring the production contract.

Quick Reference - Session 13

Remember: Good container tooling reduces setup friction without blurring the production contract.

Module 5 · Make apps safe and reliable

Session 14 · Security

Make your image safer

By the end of this session, you can:

  • Reduce image attack surface through minimal runtime contents.
  • Run the application as a non-root user when possible.
  • Add image scanning and dependency update checks to the workflow.

In simple words: Use fewer tools, fewer permissions, trusted bases, and regular scans to reduce risk.

Part 1 - Theory

Core concepts and mental model

Container security starts before deployment. Choose a maintained base, remove build tools from the runtime stage, run as a non-root user, avoid unnecessary Linux capabilities, and keep packages current.

Scanning is a decision aid, not a substitute for ownership. Review the source of a finding, its reachability, the available patch, and whether the image is actually deployed.

Part 2 - Demonstration

Build it live

FROM node:22-alpine
WORKDIR /app
RUN addgroup -S app && adduser -S app -G app
COPY --chown=app:app dist ./dist
USER app
CMD ["node", "dist/server.js"]

Test the application as the declared user. File permissions, writable directories, and temporary files must be designed rather than assumed.

Part 3 - Practice

  1. Inspect which user your current image runs as.
  2. Remove a package or tool that is not needed at runtime.
  3. Run a vulnerability scan and classify findings as fix now, monitor, or accept with rationale.

Answer key:

A smaller image reduces components that can contain vulnerabilities, but size alone is not security. Combine least privilege, patching, provenance, secret hygiene, and runtime restrictions.

Quick questions:

Why avoid root inside a container?

A compromise has fewer privileges and less impact when the process is not root.

Does a clean scan guarantee a secure application?

No. It does not prove application logic, configuration, runtime, or supply-chain safety.

Part 4 - Mini Challenge

Project task: Add a security checklist to your capstone: non-root user, base image policy, scan command, secret policy, and update cadence.

Wrap Up

Treat the image as a release artifact with a security owner and an update path.

Quick Reference - Session 14

Remember: Treat the image as a release artifact with a security owner and an update path.

Session 15 · Reliability

Keep services healthy and controlled

By the end of this session, you can:

  • Define liveness and readiness signals for a service.
  • Set resource expectations and observe what happens under pressure.
  • Handle termination signals so requests and jobs can finish safely.

In simple words: Healthy services report their state, respect resource limits, and shut down politely.

Part 1 - Theory

Core concepts and mental model

Healthchecks answer whether a service can do useful work. Resource limits make the boundary explicit, but values should come from measurement. Graceful shutdown requires the application to handle termination signals and stop accepting new work before exiting.

Do not use a healthcheck that mutates data or depends on an unrelated external service. A health signal should be cheap and local enough to diagnose.

Part 2 - Demonstration

Build it live

services:
  api:
    build: .
    mem_limit: 512m
    cpus: 1.0
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/health"]
      interval: 10s
      timeout: 3s
      retries: 3
      start_period: 15s

Validate that the health endpoint reflects application readiness and that the process exits cleanly after a stop request.

Part 3 - Practice

  1. Add a health endpoint that does not expose sensitive data.
  2. Set a local memory or CPU expectation and observe logs under load.
  3. Test graceful shutdown with docker stop and a request that takes time to finish.

Answer key:

A healthcheck should reflect the service contract. Resource values should be tuned with observed usage, and graceful shutdown should close listeners, finish safe work, and release connections.

Quick questions:

What is the difference between readiness and liveness?

Readiness means the service should receive traffic; liveness means the process is stuck or unhealthy enough to restart.

Why can an overly strict healthcheck be harmful?

It can restart a healthy but temporarily busy service and create an outage loop.

Part 4 - Mini Challenge

Project task: Add health, resource, and shutdown tests to your capstone verification checklist.

Wrap Up

Operational signals should describe user-impacting behavior and be tested under failure, not only at startup.

Quick Reference - Session 15

Remember: Operational signals should describe user-impacting behavior and be tested under failure, not only at startup.

Session 16 · Operations

Read logs and fix problems

By the end of this session, you can:

  • Build a fast diagnosis loop from status, logs, inspect, and events.
  • Clean unused objects without deleting active or persistent data.
  • Capture enough context to reproduce a container failure.

In simple words: When something fails, check status, logs, configuration, events, and the exact image before changing things.

Part 1 - Theory

Core concepts and mental model

Start with the smallest useful evidence: container status, recent logs, exit code, health state, environment shape, mounts, networks, and image version. Avoid changing multiple variables before collecting evidence.

Pruning is useful on development machines, but broad cleanup commands can remove cached images or unused volumes. Make data deletion a deliberate, separately documented action.

Part 2 - Demonstration

Build it live

docker compose ps
docker compose logs --since=10m api
docker inspect <container>
docker events --since 10m
docker system df
docker image prune

In an incident, record the image digest, Compose configuration, environment name, timestamps, and recent deployment change before making cleanup or restart changes.

Part 3 - Practice

  1. Break a container with an invalid environment value and diagnose it without rebuilding first.
  2. Use docker system df to understand what consumes local disk.
  3. Create a safe cleanup guide that explicitly warns about volumes.

Answer key:

Evidence first: status and logs show symptoms, inspect shows configuration, events show lifecycle changes, and image metadata shows what was actually deployed. Then make the smallest reversible change.

Quick questions:

Why record the image digest during an incident?

A human tag may move; the digest identifies the exact artifact running at the time.

Why avoid automatic volume pruning?

Volumes may contain the only copy of important data.

Part 4 - Mini Challenge

Project task: Write an incident runbook for three failures: crash loop, unhealthy dependency, and full disk.

Wrap Up

Reliable operations are built from observability, careful changes, and recoverable data.

Quick Reference - Session 16

Remember: Reliable operations are built from observability, careful changes, and recoverable data.

Module 6 · Share and deploy

Session 17 · Delivery

Share images and automate builds

By the end of this session, you can:

  • Tag an image for a registry without confusing local and release tags.
  • Push an image and understand its immutable identity.
  • Design a CI pipeline that tests before publishing.

In simple words: CI builds and tests one known image, then stores that exact artifact in a registry.

Part 1 - Theory

Core concepts and mental model

A registry stores and distributes image artifacts. A practical release flow builds from a commit, runs tests and scans, pushes a versioned tag, records the digest, and deploys that exact digest.

Use human-readable tags for navigation and a commit or release identifier for traceability. Credentials for a registry belong in CI secrets, not in the repository.

Part 2 - Demonstration

Build it live

docker login registry.example.com
docker build -t registry.example.com/team/notes-api:1.0.0 .
docker push registry.example.com/team/notes-api:1.0.0
docker image inspect registry.example.com/team/notes-api:1.0.0

Replace the example registry with the provider used by your deployment. CI should create the release tag from a trusted branch or release event.

Part 3 - Practice

  1. Create a CI checklist: install, lint, test, build, scan, publish.
  2. Choose a tag format that maps directly to a commit or release.
  3. Write how a failed scan or test prevents publishing.

Answer key:

Publishing should happen after automated checks. The deploy step should reference the exact image produced by the build, ideally by digest or an immutable release tag.

Quick questions:

Why not deploy latest from a registry?

latest can change without a deployment record and makes rollback ambiguous.

Where should registry credentials live?

In the CI or hosting platform's encrypted secret store.

Part 4 - Mini Challenge

Project task: Add a CI workflow for your capstone that builds and verifies the image on every pull request, then publishes only from the release branch.

Wrap Up

A container pipeline is complete only when the tested artifact can be traced to the deployed artifact.

Quick Reference - Session 17

Remember: A container pipeline is complete only when the tested artifact can be traced to the deployed artifact.

Session 18 · Production

Deploy your container app

By the end of this session, you can:

  • Plan the minimum production components around an application container.
  • Separate reverse proxy, application, database, storage, and secrets responsibilities.
  • Create a rollback and migration plan before the first release.

In simple words: Production also needs traffic, secrets, storage, monitoring, backups, migrations, and rollback.

Part 1 - Theory

Core concepts and mental model

A production deployment is more than starting an image. It needs a network entry point, TLS, environment and secret delivery, persistent storage, logs, health monitoring, backups, migrations, and a way to roll back.

For a small project, a managed container platform or a carefully maintained VPS can be enough. The right choice depends on traffic, operational experience, compliance, recovery goals, and the value of managed services.

Part 2 - Demonstration

Build it live

# Release checklist
1. Pull the exact tested image digest.
2. Inject production configuration and secrets.
3. Apply a backward-compatible database migration.
4. Start the new container beside the old one when possible.
5. Verify health, logs, and critical user flows.
6. Shift traffic and keep the rollback image available.

Provider-specific commands differ, but the release responsibilities remain similar. Keep the deployment configuration versioned and make the rollback command easy to find.

Part 3 - Practice

  1. Draw the production request path from DNS to the application container.
  2. Define where images, secrets, logs, and database backups live.
  3. Write a rollback plan for both application code and a failed database migration.

Answer key:

The application image should be immutable, configuration should be injected at runtime, state should live in managed or backed-up storage, and every release should have a verified health and rollback path.

Quick questions:

Why should migrations be backward-compatible during a rolling release?

Old and new application instances may run at the same time and must both understand the database shape.

What is a rollback without a data plan missing?

Code can roll back while a destructive schema change or data transformation cannot.

Part 4 - Mini Challenge

Project task: Write a one-page deployment runbook for your capstone with release, verification, rollback, backup, and incident contacts.

Wrap Up

Deployment is a system design problem: artifact, traffic, state, configuration, observability, and recovery must agree.

Quick Reference - Session 18

Remember: Deployment is a system design problem: artifact, traffic, state, configuration, observability, and recovery must agree.

Session 19 · Final project

Final project: ship a Docker app

By the end of this session, you can:

  • Containerize an application with a clear production runtime image.
  • Compose the local stack with networking, persistence, health, and configuration.
  • Demonstrate a tested path from commit to registry to deployment.

In simple words: The final goal is a project another developer can build, run, release, and recover without hidden steps.

Part 1 - Theory

Core concepts and mental model

The capstone brings the course together. Choose a small real application such as an API with a database, a full-stack dashboard, or a portfolio tool. Keep the scope narrow enough that you can test failure and recovery instead of only showing a happy path.

Commitsource + tests
→
Buildimage + scan
→
Releaseregistry digest
→
Runhealth + rollback

Part 2 - Demonstration

Build it live

Your final demonstration should show the complete workflow:

docker compose config
docker compose up -d --build
docker compose ps
docker compose exec api npm test
docker compose logs --tail=100 api
docker compose down

Then show the CI build, registry artifact, deployment configuration, health signal, persistent data check, and rollback procedure. Screenshots are less valuable than a reproducible README.

Complete starter files

These files give you a small working API. Create them in one folder, then run the Compose commands above.

server.js

import { createServer } from "node:http";

const port = Number(process.env.PORT || 3000);

const server = createServer((request, response) => {
  if (request.url === "/health") {
    response.writeHead(200, { "content-type": "application/json" });
    response.end(JSON.stringify({ ok: true }));
    return;
  }

  response.writeHead(200, { "content-type": "application/json" });
  response.end(JSON.stringify({ message: "Docker is working" }));
});

server.listen(port, "0.0.0.0", () => {
  console.log("API listening on " + port);
});

package.json

{
  "name": "docker-notes-api",
  "private": true,
  "type": "module",
  "scripts": {
    "start": "node server.js"
  }
}

Dockerfile

FROM node:22-alpine

WORKDIR /app

COPY package.json server.js ./

RUN addgroup -S app && adduser -S app -G app
RUN chown -R app:app /app

USER app

EXPOSE 3000
CMD ["npm", "start"]

.dockerignore

.git
.env
node_modules
npm-debug.log
Dockerfile*
compose*.yaml

compose.yaml

services:
  api:
    build: .
    ports:
      - "3000:3000"
    environment:
      PORT: 3000
    healthcheck:
      test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"]
      interval: 10s
      timeout: 3s
      retries: 5

compose.dev.yaml

services:
  api:
    build:
      context: .
    ports:
      - "3000:3000"
    volumes:
      - .:/app

.github/workflows/docker.yml

name: Docker

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/build-push-action@v6
        with:
          context: .
          push: false
          tags: docker-notes-api:ci

Part 3 - Practice

  1. Build the stack from a clean checkout on a second machine or fresh environment.
  2. Test a dependency outage, a container restart, and a restored data volume.
  3. Publish a versioned image and document how another person can deploy that exact version.

Answer key:

A strong submission is reproducible, secure by default, observable, and honest about its operational limits. It should explain what is automated, what is manual, what data is persistent, and how to recover.

Quick questions:

What makes the capstone production-ready?

A tested artifact and operational workflow with configuration, health, persistence, security, observability, and rollback—not merely a working Dockerfile.

What is the most important README section?

The exact setup, test, release, deploy, backup, and rollback commands, including assumptions and known limits.

Part 4 - Mini Challenge

Project task: Deliver a repository with Dockerfile, .dockerignore, compose.yaml, environment template, CI workflow, security checklist, runbook, and a short architecture diagram.

Wrap Up

Docker becomes valuable when it turns a project into a repeatable, inspectable, and recoverable delivery system.

Quick Reference - Session 19

Remember: Docker becomes valuable when it turns a project into a repeatable, inspectable, and recoverable delivery system.