Module 1 · Start with Docker
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.
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
- Install Docker using the official instructions for your operating system.
- Run docker version and write down whether both Client and Server are shown.
- 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.
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.
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
- Draw the path from your source code to a running container.
- Run hello-world twice and compare what Docker downloads the second time.
- 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.
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-webThe 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
- Run nginx with a memorable name and publish it on a different host port.
- Use docker exec to list the files in the web root.
- 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.
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-demoUse restart policies for expected runtime behavior, and use logs plus exit codes to find the underlying reason for repeated exits.
Part 3 - Practice
- Run a container whose command exits immediately and inspect its exit code.
- Compare docker start with docker run.
- 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
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
- Inspect two tags of the same base image and record their sizes.
- Find the configured user and default command in an image inspect result.
- 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.
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
- Containerize a small Node, Python, or Go HTTP service.
- Build it with a versioned local tag and run it on a non-default host port.
- 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.
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
- Add a .dockerignore file and verify that .env files are not in the build context.
- Build once with plain progress output and identify which steps are cached on the second build.
- 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
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.
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
- Create a network and attach two containers to it.
- Publish only the web container port to the host.
- 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.
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
- Run a stateful service with a named volume.
- Replace its container and verify that data remains available.
- 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.
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
- Classify each configuration value in your app as public, environment-specific, or secret.
- Add .env files to .gitignore and .dockerignore where appropriate.
- 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
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
- Write a Compose file with web, API, and database services.
- Validate it before starting and inspect the generated service configuration.
- 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.
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_healthyPart 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 apiHealthchecks 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
- Add a healthcheck to your database or API service.
- Make the application retry a dependency that is temporarily unavailable.
- 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.
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
- Create a development override with a source bind mount and a local-only port.
- Add a command for running tests inside the service environment.
- 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
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
- Inspect which user your current image runs as.
- Remove a package or tool that is not needed at runtime.
- 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.
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: 15sValidate that the health endpoint reflects application readiness and that the process exits cleanly after a stop request.
Part 3 - Practice
- Add a health endpoint that does not expose sensitive data.
- Set a local memory or CPU expectation and observe logs under load.
- 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.
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
- Break a container with an invalid environment value and diagnose it without rebuilding first.
- Use docker system df to understand what consumes local disk.
- 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
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
- Create a CI checklist: install, lint, test, build, scan, publish.
- Choose a tag format that maps directly to a commit or release.
- 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.
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
- Draw the production request path from DNS to the application container.
- Define where images, secrets, logs, and database backups live.
- 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.
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.
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: 5compose.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:ciPart 3 - Practice
- Build the stack from a clean checkout on a second machine or fresh environment.
- Test a dependency outage, a container restart, and a restored data volume.
- 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.