The four steps, every time start here
Before containers, deploying this API meant uploading a zip of Python source and letting the server install dependencies and restart itself. Now the deployable artifact is a finished image, which makes the process shorter and far more predictable — but adds a registry in the middle.
- Build the image from the Dockerfile. This project builds in Azure with az acr build rather than locally — the source is uploaded, built on a managed agent, and the result lands in the registry in one step. No local Docker required.
- Push it to the Azure Container Registry as metals-api:<tag> or metals-ui:<tag>. With az acr build this is included — build and push are the same command.
- Point the Web App at that image, with az webapp config container set. Only strictly necessary when the tag changes, but running it every time keeps the deployment idempotent.
- Restart the Web App so it actually pulls. This step is the one people forget — see below.
Nothing pushes an image to the Web App. The Web App pulls it from the registry, authenticating with its own system-assigned managed identity, which has been granted the AcrPull role. There are no registry usernames or passwords anywhere in this project — the registry's admin account is explicitly disabled.
That identity and role assignment are created for you by both the manual script and Terraform.
Why a restart is required the :latest catch
This project tags its images latest. Pushing a new image under a tag that the Web App is already configured to use changes nothing from Azure's point of view — the configured value is identical, so there's nothing to react to, and the old container keeps running the old image indefinitely.
az webapp restart forces the platform to start a fresh container, which re-pulls the tag and therefore picks up the new image. Both az_deploy and the GitHub Actions workflows do this as a deliberate final step, with a comment saying why.
The alternative is to tag every build uniquely — with a commit SHA, for example — so the configured value genuinely changes and no restart is needed. That's a common production pattern; this project favors the simpler moving tag, and pays for it with one extra command.
A restart returns immediately, long before the new container is serving traffic. Every deployment path here finishes by polling /health on both apps until each answers 200, so a failed rollout surfaces as a failed deployment instead of a silent one.
Infrastructure vs. application two different clocks
It's worth being precise about which kind of change you're making, because they move at different speeds and carry very different risk.
| Infrastructure | Application | |
|---|---|---|
| What changes | The registry, Web Apps, database server, identities, roles. | The contents of the two container images. |
| How often | Rarely — once to create it, then occasionally. | Constantly, with every code change. |
| Done by | az_create_resources or Terraform. | az_deploy or the deploy workflows. |
| If it goes wrong | Potentially destructive — a deleted database loses data. | Recoverable — push a corrected image and restart. |
| Automation policy | Always manual. Even with CI/CD set up, apply requires someone to click Run workflow. | Automatic on merge to main, after tests pass. |
Three ways to do it pick a path
All three produce the same running application. They differ in how much is automated, how much is previewed before it happens, and how much is remembered between runs.
Path 1
By hand
Run the scripts in utility_scripts/ from your own terminal — a fixed list of az commands, in order, with nothing remembered between runs. The clearest way to see exactly which Azure calls a deployment actually makes.
Path 2
GitHub Actions
The same commands, run by a workflow instead of a person: tests on every push, then build, push, and deploy automatically when a change reaches main. Authenticates to Azure with OIDC, so no credentials are stored anywhere.
Path 3
Terraform
Declares the infrastructure as code, previews every change before making it, and remembers what it built. This is how the Azure resources themselves are managed — though it doesn't build images; that's still one of the two paths above.
Pick a page to learn go deeper
How GitHub Actions works
Workflows, jobs, steps, triggers, and where secrets come from.
Page 2 of 3This project's workflows
The three YAML files and the rule that decides when each deploys.
Page 3 of 3Identity & permissions
Passwordless OIDC login, and the exact Azure roles required.
Path 1The manual scripts
Creating and deploying everything by hand with the Azure CLI.