Why containers at all start here
Before containers, deploying this API meant telling a server: install Python 3.11, install these exact library versions, set these environment variables, run it with this command. Every one of those steps could drift — a server on Python 3.9, a library a minor version ahead, a forgotten variable. A container image freezes all of it into a single artifact, so “deploying” becomes “run this exact image” instead of “reproduce this environment correctly.”
A virtual machine carries an entire guest operating system — its own kernel, its own boot process, gigabytes of files — and that's why VMs take minutes to start. A container shares the host's kernel and packages only the layer above it: your code, your runtime, your libraries. That's why the API image here starts in about a second, and why it's measured in hundreds of megabytes rather than tens of gigabytes.
- One command to run everything. docker compose up starts the database, API, and UI together, wired to each other, with no local Python or PostgreSQL install required.
- The UI gets a real web server. Instead of a development-only static file server, the UI image runs Nginx — the same server that will serve it in Azure.
- Deployment becomes a file copy. Shipping a new version means pushing an image to a registry and telling Azure to pull it, covered in the Deployment section.
The three words worth learning first vocabulary
Nearly every Docker error message and command uses these three terms, and mixing them up is the single most common source of confusion when starting out.
| Term | What it means, in plain terms |
|---|---|
| image | A read-only template — the packaged filesystem and the command to run. Built once from a Dockerfile, never modified afterward. Think of it as a class definition, or an ISO file. |
| container | A running instance of an image. You can start many containers from one image; each gets its own writable layer, its own process, its own network address. Think of it as an object created from that class. |
| registry | A server that stores and serves images, so other machines can pull them. Docker Hub is the public one; this project pushes its three images to a private Azure Container Registry. |
What this project actually containerizes three images
Three Dockerfiles, each producing one image: the two halves of the application, plus the tutorial site you're reading. The database is not one of them — locally it runs from the official postgres:16 image straight off Docker Hub with no customization, and in Azure it isn't a container at all but a managed PostgreSQL Flexible Server.
| File | What it builds |
|---|---|
| metals_api/Dockerfile | The metals-api image — Python 3.11 plus the Flask app, served by Gunicorn on port 5000. (dig deeper) |
| metals_ui/Dockerfile | The metals-ui image — Nginx serving the static UI on port 8080 and proxying /api/ through to the API. (dig deeper) |
| tutorials/Dockerfile | The metals-tutorials image — Nginx serving this tutorial site on port 8080. Independent of the application: no database, no API, no configuration. |
| docker-compose.yml | Not an image — the recipe that runs all three of the above plus a PostgreSQL container together on your machine. (dig deeper) |
| .dockerignore | Two of them (repo root and metals_ui/) controlling which files are allowed into each build. |
Pick a page to learn go deeper
Images & containers
Layers, the build cache, and how a base image is chosen.
Page 2 of 6Inside a Dockerfile
Every instruction this project uses, and what each one does.
Page 3 of 6The commands
Building, running, inspecting, and cleaning up.
Page 4 of 6The API image
metals_api/Dockerfile, line by line.
Page 5 of 6The UI image
Nginx, the config template, and the API proxy.
Page 6 of 6Running the whole stack
docker-compose.yml and the three services it starts.