The 2 AM feeling, at 10 AM
I recently shipped a small feature to an internal tool: profile picture uploads backed by Azure Blob Storage. Locally, everything worked — Aspire, Azurite emulator, the whole local dev loop was green. I pushed to main, the GitHub Actions pipeline ran, every step turned green, and I went on with my day.
A little while later a colleague tried uploading a picture and got a 404 Not Found. Odd — the endpoint existed, I’d tested it. I opened the Container App logs in the portal and found this instead:
| |
The API had been crash-looping since the deploy. Every single request was hitting a container that never finished starting up. And yet the GitHub Actions run for that deployment was 100% green.
Why your pipeline doesn’t notice
If you deploy to Azure Container Apps from a workflow, there’s a good chance your rollout step looks something like this:
| |
This command waits for the Azure Resource Manager deployment to reach Succeeded. But that only means Azure accepted your request and created a new revision — it says nothing about whether the container inside that revision actually started, bound to its port, and started serving traffic. An unhandled exception during startup, a missing environment variable, a bad connection string — all of these crash the container after ARM has already reported success. Your pipeline goes green, your app goes down, and you find out from a colleague (or a customer) instead of from CI.
The fix is simple: after the rollout, ask Container Apps directly whether the revision you just shipped is actually healthy, and fail the job if it isn’t.
A full, working example
Below is a complete, minimal GitHub Actions workflow for “an arbitrary .NET solution” — swap in your own project names, registry, and resource group. It:
- Builds and publishes a container image directly from the
.csprojusing the .NET SDK’s built-in container support (noDockerfileneeded). - Pushes it to Azure Container Registry.
- Rolls it out to an existing Azure Container App.
- Verifies the new revision is actually running and healthy before letting the job succeed.
It assumes the Container App and Container Registry already exist (created once via Bicep/Terraform/az cli, and that the workflow authenticates via OIDC federated credentials — no stored client secrets.
The .NET project
Nothing special is required here — this works for any ASP.NET Core project. Just make sure it targets Linux/x64 so the SDK can build a container image for it:
| |
| |
The GitHub Actions workflow
| |
Walking through the health check
The two Container Apps-specific commands do all the work:
az containerapp show --query properties.latestRevisionNamefinds the name of the revision that was just created by the update — you need this to look at that specific revision rather than whatever happened to be active before.az containerapp revision showreturns, among other things,properties.runningState(Running,Processing,Failed,Degraded, …) andproperties.healthState(Healthy,Unhealthy,None). Together they tell you whether the platform managed to start your container and whether it’s currently considered healthy.
The loop polls both values every 10 seconds for up to 5 minutes (max_attempts=30). Three outcomes are possible:
- Healthy —
Running+Healthy— the step succeeds and the job moves on. - Clearly broken —
FailedorUnhealthy— no point waiting any longer, so it fails fast and dumps the last 200 lines of container logs straight into the workflow output. This is exactly the exception trace that took me a trip to the Azure Portal to find manually — now it’s right there in the Actions log. - Timeout — after 5 minutes without a clear answer, the step gives up, fails, and still dumps the logs for you.
That’s it. No extra infrastructure, no extra services, just two az calls wrapped in a small polling loop.
Why not just curl a health endpoint?
An alternative is to expose a /health endpoint and curl the app’s public URL after deploying. It works, but it has downsides for this use case:
- It requires your app to expose an unauthenticated, public health endpoint — something you may not want in production, and ASP.NET Core’s own health check middleware documentation actively warns about the security implications of enabling it outside development.
- It tells you the app answered HTTP requests, but not why it didn’t if it doesn’t — you’re back to digging through logs manually.
- It doesn’t work at all if your app has no HTTP-reachable health surface (background workers, for example).
Asking the Container Apps control plane directly sidesteps all three: it works for any container regardless of what’s running inside it, and a failure comes with the logs already attached.
Conclusion
“The pipeline is green” and “the app is running” are two different claims, and it’s worth spending five extra minutes making your pipeline actually check the second one. It would have saved me a 404 in production and a slightly awkward Teams message from a colleague. Now it just fails the build and shows me the stack trace instead — much better start to the day.