TutorialsDeploymentManual › az_deploy

Manual Deployment · Script 2 of 4

az_deploy: Building and Shipping Both Images

Once the infrastructure exists, this is the script you run over and over. It builds the metals-api and metals-ui images inside Azure Container Registry, points each Web App at the resulting tag, restarts them so they actually re-pull, and waits until both answer /health. No local Docker installation is involved at any point.

Codey the Sr Developer, standing with a pointer, ready to walk through the guide
Script → az_deploy.ps1 / .sh Builds → in Azure, not locally Images → metals-api, metals-ui Default tag → latest Terraform equivalent → none — Terraform doesn't build images

Purpose of this page

Deploy a new version of the API, the UI, or both to Azure by hand — and understand each of the four steps well enough to run them individually when something needs debugging.

Codey typing on a laptop
build, point, restart, verify

Before you run it prerequisites

  • Azure CLI installed, and az login already done — the script never logs you in.
  • The resources from az_create_resources must already exist; this script only updates them.
  • Run it from the repository root, not from inside utility_scripts/ — it locates the build contexts relative to the project.
  • No Docker needed. The images are built by Azure Container Registry, not on your machine.
PowerShell, from the repository root.\utility_scripts\az_deploy.ps1 -UserName dzierzon
Bashbash utility_scripts/az_deploy.sh --user-name dzierzon

What the script does four steps, twice

The same four operations are applied to each of the two applications. Everything below is from az_deploy.ps1; the Bash version does the same work with --kebab-case options.

Step 1 — build each image in the registryaz acr build
Push-Location $webApp.Context
try {
    Invoke-Az acr build --registry $RegistryName --resource-group $resourceGroup `
        --image "$($webApp.Image):$ImageTag" --file $webApp.DockerfilePath .
}
finally { Pop-Location }

One command per image. It packs the build context into an archive, uploads it, and asks the registry to run the Dockerfile on a managed build agent — the finished image is pushed into the registry without ever existing on your machine.

The Push-Location isn't decoration. az acr build validates --file against the current directory rather than the source argument, so the API (context: repo root, file: metals_api/Dockerfile) and the UI (context: metals_ui, file: Dockerfile) each have to run from their own context.

Step 2 — point the Web App at the tagconfig container set
Invoke-Az webapp config container set --name $name --resource-group $resourceGroup `
    --container-image-name $image `
    --container-registry-url "https://$($registry.loginServer)" --output none

Sets which image the app should run. Strictly it's only needed when the tag changes, but running it every time makes the script idempotent — and it's what converts an app that was previously configured for something else.

No registry credentials appear here. The Web App authenticates to the registry with its own managed identity, configured back in az_create_resources.

Step 3 — restart so it re-pullsthe easily-missed one
Invoke-Az webapp restart --name $name --resource-group $resourceGroup --output none

Because this project reuses the latest tag, step 2 usually sets the image name to the value it already had — so Azure sees no change and keeps running the old container indefinitely. An explicit restart forces a fresh pull. Skip this and the deployment silently does nothing.

Step 4 — wait for healthydon't trust a restart
for ($attempt = 1; $attempt -le 10; $attempt++) {
    try {
        $response = Invoke-WebRequest -Uri "https://$hostName/health" -TimeoutSec 10 -UseBasicParsing
        if ($response.StatusCode -eq 200) { $healthy = $true; break }
    }
    catch { Start-Sleep -Seconds 10 }
}

webapp restart returns immediately, long before the new container is serving traffic. The script polls each app's /health endpoint up to ten times, ten seconds apart, and warns clearly if one never comes up — so a broken rollout looks like a failure rather than a success.

The hostname is read back from Azure with az webapp show --query defaultHostName rather than assembled from the app name, because Azure's actual hostname can include a region or uniqueness suffix.

The parameters and when you'd use them

Defaults are chosen so that the bare command does the common thing: build every image as latest and deploy them.

Codey holding up a sticky note
defaults cover the common case
PowerShellBashUse
-UserName--user-nameResource-name suffix; default dzierzon. Must match what you created resources with.
-RegistryName--registry-nameOverride the generated registry name, if you had to pick a different one.
-ImageTag--image-tagTag to build and deploy; default latest.
-SkipBuild--skip-buildSkip building entirely and just repoint and restart at an already-pushed tag — useful for rollbacks and for retrying a failed deploy.
-FollowLogs--follow-logsStream the API's container logs after deploying, instead of returning to the prompt.

Without -FollowLogs the script prints the two az webapp log tail commands you'd need to watch either app, so you can follow whichever one you care about.

Why az acr build instead of docker build + push

The obvious alternative is building locally and pushing. Letting the registry build instead removes several moving parts — and it's exactly what the GitHub Actions workflows do too, for the same reasons.

Codey pointing to the right
upload source, not layers
What you avoidfour things
  • Installing Docker on every machine that might deploy, including CI runners.
  • A separate registry login. Your existing az login is the only authentication involved.
  • Uploading image layers. Only the source is sent; the built layers never leave Azure.
  • Architecture mismatches. The image is always built on the platform it will run on, so an ARM laptop can't accidentally produce an image the x86 Web App can't start.
The trade-offbe fair about it

You lose the local build cache — each az acr build starts fresh, so a small code change costs a full rebuild rather than a few cached seconds. For a deployment step that's an acceptable price; for the tight edit-and-test loop you'd still use Compose locally.

Common errors and how to fix them

What you seeLikely causeHow to fix it
Unable to find 'Dockerfile'az acr build resolved --file against your current directory, not the source argument.Run the script from the repository root. This is exactly what the Push-Location in step 1 exists to handle.
AuthorizationFailed on listBuildSourceUploadUrlThe identity can reach the registry but lacks permission to start a build.Building needs Contributor on the registry — see the roles table. Your own az login account usually already has it.
Deploy succeeds, but the site is unchangedThe new image was pushed under the same tag and nothing restarted.Confirm step 3 ran. Rerun with -SkipBuild to repoint and restart without rebuilding.
did not report healthy ... within the timeoutThe container started but is failing, or is slower than 100 seconds to come up.Read the container logs: az webapp log tail --resource-group <rg> --name <app>. A missing app setting is the usual culprit.
ResourceNotFound for the registryThe resources don't exist yet, or -UserName doesn't match what they were created with.Run az_create_resources first, using the same username.
The UI loads but every API call failsThe UI container can't reach the API — typically a DNS resolver or upstream setting.Check the UI app's API_UPSTREAM and NGINX_RESOLVER settings; see the UI image page.
Codey giving a thumbs up

Deploying by hand works — next, hand the same job to GitHub Actions so a merge does it for you.