Capstone Project
Why this matters
Every module in this track exercised one skill in isolation: writing a Dockerfile, mounting a volume, wiring a network, pushing to a registry, deploying to Azure. Real work requires all of them together, in the right order, with the small mistakes (a wrong port, a missing credential, a volume that doesn't persist) that only show up when everything has to work at once. This project is that integration test — for you, not for a grader. There's no answer key because the point is debugging your own decisions.
This bridges directly to the next track: once you can reliably containerize and deploy a multi-service app to Azure by hand, the Kubernetes track picks up exactly where this leaves off — the same container images, deployed instead to a cluster that handles scaling, self-healing, and rolling updates for you.
The project
Build, containerize, compose, and deploy a small two-service web application, end to end, on your own. No starter files are provided — design and write the application yourself (it can be as simple as a Flask or FastAPI app with two or three routes; reuse patterns from earlier modules freely).
Required components
A small web app with a multi-stage Dockerfile. The app should have at least one route that depends on a second service (a database or Redis) to prove the two are actually wired together — e.g. a counter that increments in Redis, or a route that reads/writes a Postgres table. The Dockerfile must use at least two build stages (module 07): one that installs dependencies, a second, leaner stage that only contains what's needed at runtime. It must run as a non-root user (module 09).
A
compose.yamlwiring the app to a second service with a named volume. The second service is a database (Postgres, MySQL) or Redis, using an official image (no need to build it yourself). Its data must be stored in a named volume (module 04) so it survives adocker compose down(without-v). Usedepends_onwithcondition: service_healthy(module 06) so the app doesn't start before the database/cache is actually ready.Local verification. Before touching Azure, get the whole thing working locally with
docker compose up— prove the app can read and write through the second service, and that data survives a restart.Push the app's image to Azure Container Registry. Reuse an ACR instance from earlier modules or create a new one (module 08). The second service (database/Redis) does not need to be pushed to ACR — you'll pull its official image directly from Docker Hub in whichever Azure hosting option you choose next.
Deploy to either ACI or App Service, with the registry credentials configured correctly so the deployed container can actually pull your image (modules 10/11). Pick whichever platform fits your app's needs better and be ready to explain why: ACI is simpler and cheaper for a single-container deployment without a custom domain requirement, while App Service adds HTTPS out of the box and separates the compute plan from the individual deployment. If your app needs the second service (database/Redis) reachable too, decide deliberately how you're providing that in the Azure environment — this is the part of the project with no single correct answer, and the hints below discuss the trade-off.
Verify it's reachable over the public internet — from a browser or
curl, hitting the public FQDN/hostname, notlocalhost.Tear everything down cleanly — no lingering ACI container groups, App Service plans, or ACR instances left running after you're done. Confirm with
az group showthat the resource group is gone.
Acceptance criteria checklist
- Dockerfile has at least two
FROMstages, and the final stage does not contain build-only tooling that the first stage needed. - The final image runs as a non-root user (confirm with
docker run --rm <image> whoami). -
.dockerignoreexcludes anything irrelevant to the build (.git, virtual envs,__pycache__, etc.). -
compose.yamldefines at least two services and one named volume. - The database/cache service has a
healthcheck:, and the app service'sdepends_onusescondition: service_healthy. - Locally,
docker compose down(no-v) followed bydocker compose up -dagain preserves previously written data;docker compose down -vfollowed byup -ddoes not. - The app's image is pushed to an ACR instance you created or
control, tagged meaningfully (not just
latest). - The app is deployed to ACI or App Service, successfully pulling from your private ACR instance (not a public image).
- You can reach a working route over the public internet using the deployment's public hostname, and it demonstrably depends on the second service (e.g. a counter that increases across requests).
- You can explain, in your own words, why you chose ACI vs. App Service for this specific app.
- Every Azure resource created for this project has been deleted,
confirmed with
az group show <name>returning "not found."
Hints (not a solution)
- On the second-service-in-Azure question: the simplest version of this project doesn't require a persistent database in Azure at all — using Redis (or an in-memory counter as a fallback) and accepting that ACI's container group or an App Service instance restart may reset data is a legitimate, honestly-labeled simplification for a learning project. If you want persistence in Azure specifically, the two realistic paths are: (a) an ACI multi-container group running both your app and a Redis/Postgres container together, sharing a network namespace, or (b) Azure's managed database offerings (outside this track's scope, but worth knowing they exist for anything beyond a demo). Don't feel obligated to solve production-grade persistence here — the grading bar is "you made a deliberate, explainable choice," not "you built what a real production system would use."
- On multi-container ACI specifically:
az container createaccepts a YAML file describing multiple containers in one group (rather than the single-container flags used in module 10) if you want to try running both services together in ACI — look ataz container create --file <yaml>if you go this route. - On healthchecks: if your
healthcheck:command isn't available inside the official database/cache image (e.g. nocurl), use the tool that image actually ships —redis-cli pingfor Redis,pg_isreadyfor Postgres — the same "use what's actually in the image" lesson from module 02's exec exercises. - On credentials reaching Azure: re-use the ACR admin username/password pattern from modules 10/11 rather than inventing a new auth mechanism — this project is about integration, not learning a new credential type.
- On debugging a deployment that "works locally but not on Azure":
work through the same checklist you used in modules 10/11's
diagnose-and-fix exercises, in order — check the registry credentials
first, then the port configuration, then the logs (
az container logs/az webapp log tail) — rather than guessing. - On knowing when you're done: if you can close your laptop, open it
again tomorrow, run one
curlagainst a public hostname, and see the right response — you've actually deployed something, not just gotten a command to exit successfully once.
Further reading & sources
- Docker: Multi-stage builds - for the two-stage Dockerfile the capstone requires.
- Compose file reference - for the multi-service
compose.yamlwith a named volume and health-gateddepends_on. - Azure: Deploy a multi-container group to ACI with YAML - the
az container create --filepath referenced in the multi-container hint. - Azure: Run a custom container in App Service - the App Service deployment option for the capstone.
- OWASP: Docker Security Cheat Sheet - to sanity-check the non-root, minimal-image requirements before you ship.
- Docker: Build best practices - a final checklist for image size, caching, and
.dockerignorebefore deploying.
Next
This is the last module in the Docker track. From here, continue to the Kubernetes track — the container images and Compose files you just built are exactly what you'll translate into Kubernetes objects (Pods, Deployments, Services) next; nothing about how you build and structure images changes, only how they're scheduled and networked.
Before you move on
A day or two after you finish, tear the whole capstone down and rebuild it from memory — build the image, push it to your registry, and redeploy it to Azure — without re-reading your own notes or this page. If you can get it live again from a blank directory and a working recollection alone, that's the real signal the whole track stuck. Do that retention check before you start the Kubernetes track.