Skip to main content

Deploy a .NET App to Azure Container Apps — Part 6: Logs, Scaling, Cost and Cleanup

· 12 min read
Jagdish Kumawat
Founder @ Dewiride

Getting an app deployed is the easy half. This final part covers what you actually do with a running app: read its logs, understand how it scales, know what it costs — and take it all down again.

This is Part 6 of a 6-part series.

  1. Part 1: Build the App
  2. Part 2: Containerize It with Docker
  3. Part 3: Resource Group and Container Registry
  4. Part 4: Push Your Image to the Registry
  5. Part 5: Deploy to Azure Container Apps
  6. Part 6: Logs, Scaling, Cost and Cleanup (you are here)

Step 1: Read the logs

Open your container app in the portal and choose Monitoring → Log stream in the left menu.

The Azure portal Log stream showing live .NET startup logs including Now listening on http port 8080 and Hosting environment Production

This is your app's console output, live:

Log stream
Successfully Connected to container: 'ca-dotnet-status-page'
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://[::]:8080
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
Content root path: /app

Two things worth noticing.

Now listening on: http://[::]:8080 is the confirmation that the target port you set in Part 5 was right. If you ever get a gateway error, this line tells you the port the app actually chose — compare it to your ingress setting.

Hosting environment: Production — ASP.NET Core defaults to Production in a container, so detailed error pages are off. That's the correct default; it also means a crash shows a blank error page in the browser and the real detail only here in the logs.

There are two log categories in the dropdown at the top: Application (your app's output) and System (Azure's own events — image pulls, replica starts, scaling decisions). When a container won't start at all, System is where the answer lives.

Industry practice: log to stdout and let the platform collect it

The app never opens a log file. It writes to standard output, and Container Apps collects that. This is the standard contract for containers.

Writing to files inside a container is a trap: the filesystem disappears when the replica does, replicas come and go constantly, and you end up with logs scattered across machines you can't reach. Print to the console and let the platform handle the rest.

Step 2: Understand revisions and replicas

Choose Application → Revisions and replicas.

The Revisions and replicas page showing one active revision with running status, 100 percent traffic and 1 replica

Two words that are easy to mix up:

  • A revision is an immutable snapshot of your app's configuration — image tag, CPU, memory, environment variables. Change any of those and you get a new revision.
  • A replica is a running copy of a revision. One revision can have zero, one, or many replicas.

Right now: one revision, 100% of traffic, one replica.

This is why Part 4 insisted on version tags. Deploy 1.0.1 and you get a second revision. You can send 10% of traffic to it, watch, and either shift the rest across or send traffic back to 1.0.0 — a rollback in one click. Deploy latest twice and both revisions claim the same tag, so you lose that.

Step 3: Understand scaling

Choose Application → Scale.

The Scale settings page showing min replicas 0, max replicas 10, and an http-scaler scale rule

SettingValueWhat it means
Min replicas0With no traffic, everything shuts down
Max replicas10Under load, up to 10 copies
Cooldown period300Wait 300s of quiet before scaling down
Polling interval30Check the scaling trigger every 30s
Scale rulehttp-scalerScale on concurrent HTTP requests

Min replicas 0 is the setting that makes this nearly free. No traffic, no running containers, no compute charges.

The trade-off is a cold start. When a request arrives at a scaled-to-zero app, Azure has to start a replica first — a few seconds. You can see it in the status page: after a quiet period, "Uptime" resets and "Requests served" goes back to 1, because it's a brand-new replica.

Industry practice: min replicas 0 for dev, at least 1 for anything user-facing

Scale-to-zero is excellent for demos, internal tools, and dev environments — things that are idle most of the day.

For anything a customer touches, set min replicas to 1. One always-warm replica removes the cold start entirely. It does mean you're billed continuously for that replica, and idle replicas at the minimum count bill at a reduced idle rate rather than the full active rate. Paying for one small warm replica is usually the right call for user-facing traffic.

Step 4: What this actually costs

Container Apps' Consumption plan gives every subscription, every calendar month:

  • 180,000 vCPU-seconds
  • 360,000 GiB-seconds
  • 2 million HTTP requests

Our app is allocated 0.25 vCPU and 0.5 GiB. So one replica running continuously for a full 30-day month would use:

MeterCalculationMonthly totalFree allowance
vCPU-seconds0.25 × 2,592,000s648,000180,000
GiB-seconds0.5 × 2,592,000s1,296,000360,000

Running non-stop, you'd exceed the grant — you get roughly 8 days of continuous running free at this size. But with min replicas at 0, this app only consumes while it's serving requests. A demo you open a few times a day never gets close.

Here's the full picture for this series:

ResourceCost
Resource groupFree
Container Apps environmentFree (no charge unless you add a Dedicated workload profile)
Container app computeFree within the monthly grant
Log Analytics ingestionPennies at this volume
Azure Container Registry (Basic)~$0.167/day — the only meaningful cost

Which brings us to the important step.

Step 5: Clean up

Deleting the resource group deletes everything inside it in one action — the container app, the environment, the registry, the Log Analytics workspace.

Deleting a resource group is permanent

It deletes every resource in the group, not just the ones from this series. Confirm the name is rg-dotnet-container-demo and that it contains nothing else you care about. There is no undo.

In the portal:

  1. Search for resource groups and open rg-dotnet-container-demo.
  2. Click Delete resource group in the toolbar.
  3. Type the resource group name to confirm.
  4. Click Delete.

Or from the terminal:

Terminal
az group delete --name rg-dotnet-container-demo --yes --no-wait

Deletion takes a few minutes. Confirm it's gone:

Terminal
az group exists --name rg-dotnet-container-demo
Output
false

You can also tidy up locally:

Terminal
docker stop status-page
docker rm status-page
docker rmi hello-container-app:1.0.0
Industry practice: decide how something dies before you create it

The most common cause of a surprising cloud bill isn't expensive resources — it's forgotten ones. A registry costing 17 cents a day is nothing for an afternoon and about $60 if you find it two years later.

Two habits worth forming: put every experiment in its own resource group so cleanup is one action, and set a budget alert on the subscription so Azure emails you when spend crosses a threshold you chose.

Production Checklist

Everything the series covered, plus what you'd add before real users show up.

Covered in this series

PracticeWhere
Target the current LTS runtimePart 1
Multi-stage build — ship the runtime, not the SDKPart 2
Pin base image tags, never :latestPart 2
Run as a non-root userPart 2
.dockerignore to keep secrets out of the imagePart 2
Order layers so dependency restore stays cachedPart 2
Predictable resource namingPart 3
Leave the ACR admin user disabledPart 3
Build explicitly for the target CPU architecturePart 4
Version your image tagsPart 4
Managed identity instead of stored credentialsPart 5
Right-size CPU and memoryPart 5
Let the platform terminate TLSPart 5
Log to stdoutPart 6
Delete what you're not usingPart 6

Next steps before production

PracticeWhy
Add health probesThe app already exposes /health. Wire it up as a liveness and readiness probe so Azure restarts a wedged replica and holds traffic until a new one is genuinely ready.
Never bake secrets into imagesUse Container Apps secrets or Azure Key Vault with a managed identity. Anything baked into an image is readable by anyone who can pull it.
Set min replicas to 1Removes cold starts for user-facing apps.
Automate the build and deployA GitHub Actions or Azure DevOps pipeline that builds, tags with the commit SHA, pushes and updates the app removes the human from the loop.
Add a budget alertGet told about spend before the invoice does it for you.
Scan images for vulnerabilitiesMicrosoft Defender for Cloud, or docker scout, catch known CVEs in your base image.
Pin base images by digestGuarantees byte-identical rebuilds.
Use a custom domainContainer Apps issues free managed certificates.

Common Mistakes

MistakeConsequenceFix
Leaving the registry running after finishing~$5/month indefinitelyDelete the resource group
Deleting individual resources one by oneSomething gets missedDelete the whole resource group
Min replicas 0 on a customer-facing appCold starts on first requestSet it to 1
Deploying :latestNo rollback, unclear what's runningVersion every tag
Writing logs to a file in the containerLogs vanish with the replicaWrite to stdout
Assuming the free grant covers everythingRegistry and Log Analytics still billKnow which meters are actually free

Troubleshooting

ProblemCauseFix
Log stream is emptyApp hasn't printed anything since you connectedLoad the page to generate output
No logs under ApplicationEnvironment set to "Don't save logs"Live streaming still works; switch destination for history
First request takes several secondsCold start from zeroExpected. Set min replicas to 1
Resource group won't deleteA resource lock, or a child resource still deletingCheck Locks; wait and retry
Uptime keeps resettingThe app scaled to zero and restartedExpected with min replicas 0

FAQ

How do I deploy an update? Build and push a new tag (1.0.1), then update the container app's image. Azure creates a new revision and moves traffic to it.

Can I roll back? Yes — that's what revisions are for. Activate the previous revision and shift traffic back.

Where do environment variables and secrets go? Environment variables on the Container tab; secrets under Security → Secrets, referenced by name. For anything sensitive, back them with Key Vault and a managed identity.

Does deleting the resource group delete my container image? Yes. The registry lives in that group, so the images go with it. Your local copy and source code are untouched.

What should I learn next? Custom domains, GitHub Actions for continuous deployment, and Key Vault integration are the natural three. Each builds directly on what you've deployed here.

Conclusion

Six parts ago you had an empty folder. Along the way you built a .NET app, packaged it into a container that runs anywhere, pushed it to a private registry, deployed it to a serverless platform with a real HTTPS URL, and then took it all down again.

The one detail that ties it together is that host name. MacBookPro, then b6a8ddec4d46, then ca-dotnet-status-page--zcf08be-75784458d8-hdbxn. Same code every time — only the machine underneath changed. That's the whole promise of containers, and you watched it happen rather than taking anyone's word for it.


Previous: ← Part 5: Deploy to Azure Container Apps

The full series:

  1. Build the App
  2. Containerize It with Docker
  3. Resource Group and Container Registry
  4. Push Your Image to the Registry
  5. Deploy to Azure Container Apps
  6. Logs, Scaling, Cost and Cleanup

Additional Resources

Stay Updated

Subscribe to our newsletter for the latest tutorials, tech insights, and developer news.

By subscribing, you agree to our privacy policy. Unsubscribe at any time.