Datalumina

Deployment

Operate and clean up

Mount persistent storage, verify the live app, read the portal correctly, and tear down hosting without touching the AI services.

By the end of this stage the deployment is finished and understood. The database survives restarts on Azure Files, the live app passes an end-to-end check, you can read the portal's revision and secrets screens without being misled, and you know the teardown commands that stop the meter without destroying your AI services.

daveebbelaar/invoice-review2:31:34

Mount Azure Files at /app/data

The app writes SQLite and uploads to /app/data, and right now that path lives inside the container. The share you registered with the environment needs to mount there. There is one quirk. az containerapp create cannot attach volumes, so the mount is a second step, export the app's config, add the volume, apply it back:

az containerapp show -g "$RG" -n "$APP_NAME" -o json > /tmp/ca-app.json

# Edit the JSON: add a volume for the environment storage and mount it.
# The full script that emits /tmp/ca-app.yaml is in docs/azure-deploy.md, step 5.

az containerapp update \
  --resource-group "$RG" \
  --name "$APP_NAME" \
  --yaml /tmp/ca-app.yaml

The edit itself is small. Into the app's template go a volume and a mount, referencing the storage definition from the previous stage:

volumes:
  - name: data
    storageType: AzureFile
    storageName: invoice-review-files
containers:
  - volumeMounts:
      - volumeName: data
        mountPath: /app/data

The runbook wraps this in a short Python script that patches the exported JSON and writes the YAML, so the whole step stays copy-paste. Confirm the result in one query:

az containerapp show -g "$RG" -n "$APP_NAME" \
  --query "{volumes:properties.template.volumes,mounts:properties.template.containers[0].volumeMounts,scale:properties.template.scale}" -o json

Keep the single replica

Check scale in that output. Min and max must both be 1. More than one container writing the same SQLite file over Azure Files crashes with sqlite3.OperationalError: database is locked. If you ever need more replicas, that is your signal to move to PostgreSQL, not to raise the number.

In the storage account's file share you can now watch documents.db and the uploads folder appear as the app writes, the same files that lived in backend/data/ locally, now on durable storage.

The failed-revision gotcha

Every config change in this stage created a new revision, and one of them probably crashed along the way. In the video the portal shows a failed revision while the app works fine, and the resolution is knowing how Container Apps routes traffic. Requests go to the latest ready revision (latestReadyRevisionName), not the latest attempt. A crashed revision sits harmlessly next to a healthy one serving all traffic.

The operational lesson cuts both ways. A failed revision does not mean the app is down, and an Overview page saying Running does not mean your newest change deployed. When in doubt, check which revision is receiving traffic, and deactivate crashed ones after you fix the cause.

Verify end to end

Three curls prove the deployment layer by layer:

FQDN="$(az containerapp show -g "$RG" -n "$APP_NAME" --query properties.configuration.ingress.fqdn -o tsv)"

curl -s "https://$FQDN/health"
# {"status":"ok"}

curl -s -o /dev/null -w "%{http_code}\n" "https://$FQDN/api/documents"
# 401

curl -s -c /tmp/ir-cookies.txt -b /tmp/ir-cookies.txt \
  -H "Content-Type: application/json" \
  -d "{\"password\":\"$APP_ACCESS_PASSWORD\"}" \
  "https://$FQDN/api/auth/login"
# {"auth_enabled":true,"authenticated":true}

The first says the container runs. The second says the gate holds, no cookie, no documents. The third logs in and collects the session cookie. Then do the real test in a browser. Open the URL, sign in with the saved password, and process a sample document. In the video the total-mismatch invoice goes through the full flow on the live URL, extraction, the expected error, rejection, and a drafted correction email, the entire pipeline running in Azure against your provisioned services.

For the storage proof, restart the active revision and check that the document history still lists what you processed. Ephemeral container, durable data. The mount works.

Reading the portal

Now that everything runs, the portal screens are worth one guided look, because the CLI put values in two different places:

  • Settings, then Secrets holds the write-only values: both API keys, the access password, the session secret, and the ACR password
  • Application, then Containers, then Environment variables holds the readable config: both endpoints, the deployment name, ALLOWED_ORIGIN, and the secretref: pointers into the secrets

Dave's aside applies: "the beauty of the CLI is that it knows how to manage all of this." The portal is where you verify; the CLI is where you act, and the same commands work next month regardless of how Microsoft rearranges the UI.

Clean up without collateral damage

The app is a demo. Once you have shown it around, stop paying for it, and do it surgically. These four commands remove the hosting and nothing else:

az containerapp delete --resource-group "$RG" --name "$APP_NAME" --yes
az containerapp env delete --resource-group "$RG" --name "$ENV_NAME" --yes
az acr delete --resource-group "$RG" --name "$ACR_NAME" --yes
az storage account delete --resource-group "$RG" --name "$STORAGE_NAME" --yes

The environment auto-created a Log Analytics workspace; delete that too if you spot it in the resource group.

Do not delete the resource group

az group delete would also take Document Intelligence and Foundry with it. The earlier pages set those up once for the whole project; the teardown here removes hosting only. Deleting the group is the move for a full project reset, and only then.

What this deployment is, honestly

Dave closes the deployment with a disclaimer worth keeping verbatim: this "has by no means been a full guide on how to create secure applications," because the single password is easy to break and the downstream services deserve their own hardening. The goal was the end-to-end path, an idea, a working app, a live URL, all on infrastructure a client would recognize. What separates this from something you would run a company on is exactly one page away.

Checkpoint

  • The app runs behind the password on a public URL, and a processed document survives a revision restart
  • You can point at every secret and every plain variable in the portal and say why it lives where it does
  • You can explain why a failed revision and a working app coexist
  • You know the four teardown commands and what they deliberately spare

On this page