Migrating Legacy .NET Apps to Azure Container Apps: A Step-by-Step Guide
Legacy .NET applications are still common in Malaysian and Southeast Asian enterprises. Many of them run on Windows Server VMs with IIS, scheduled patching, manually managed certificates, and deployment processes that still depend on RDP or Web Deploy.
Azure Container Apps can be a strong target for modernised .NET applications, but there is an important boundary that must be clear before any migration starts:
Azure Container Apps requires Linux-based x86-64 container images. It is not a lift-and-shift target for Windows Server Core containers running full .NET Framework.
That means the right migration path depends on the application:
- If the application can be moved to .NET 8 or later and packaged as a Linux container, Azure Container Apps is a practical serverless container target.
- If the application must remain on full .NET Framework, IIS, COM components, or Windows-only dependencies, use a Windows-capable hosting option such as Azure App Service for Windows Containers, Azure Kubernetes Service with Windows node pools, or remain on Windows VMs until the application is modernised.
This guide focuses on the practical assessment and migration path for moving legacy .NET applications toward Azure Container Apps safely, without pretending every .NET Framework workload can be dropped into Container Apps unchanged.
The First Decision: Rehost or Modernise?
Before writing a Dockerfile, decide whether the workload is a rehost candidate or a modernisation candidate.
| Situation | Recommended target |
|---|---|
| ASP.NET Core or .NET 8+ application that can run on Linux | Azure Container Apps |
| .NET Framework 4.x application requiring IIS and Windows Server Core | Azure App Service for Windows Containers, AKS Windows node pools, or Windows VM |
| Application with COM automation, Office Interop, GAC dependencies, or registry-heavy behaviour | Windows VM or refactor first |
| Stateless web API with externalised configuration and logging | Strong Azure Container Apps candidate |
| In-process session, file-based state, or local scheduler dependencies | Refactor state and background work before Container Apps |
Azure Container Apps is excellent for serverless container operations, HTTP scaling, revision management, managed ingress, Dapr integration, and event-driven workloads. It is not the right first landing zone for an unchanged Windows-only .NET Framework application.
Migration Assessment
1. Runtime and Operating System
Check the target runtime first:
- Full .NET Framework 4.x requires Windows Server Core when containerised.
- .NET 8 and later can target Linux containers, Windows Nano Server, or Windows Server Core depending on the application and base image.
- Azure Container Apps requires Linux-based x86-64 container images.
If your application is still .NET Framework 4.x, the Azure Container Apps route usually starts with a code migration to .NET 8 or later, not with a direct Windows container deployment.
2. Windows-Specific Dependencies
Look for dependencies that block Linux containerisation:
- COM automation or Microsoft Office Interop.
- Windows registry access.
- GAC-registered assemblies.
- IIS modules that do not have a Linux/container equivalent.
- Windows-only graphics or printing libraries.
- Server-side WCF implementations.
- Local Windows services installed beside the web application.
Some dependencies can be replaced. Others justify keeping the workload on a Windows-capable platform until the business case supports deeper refactoring.
3. State Storage
Container Apps expects stateless, restartable containers. Review:
- In-process session state.
- Static variables used as shared state.
- Files written to local disk.
- Local cache directories.
- Scheduled jobs running inside the web server process.
Recommended patterns:
- Store session state in Redis or a database.
- Store files in Azure Blob Storage or Azure Files where a file share is required.
- Move background jobs to Azure Container Apps jobs, Azure Functions, or a workflow engine.
- Send logs to stdout, Application Insights, or Azure Monitor instead of local files.
4. Database and Identity
For database access:
- Move SQL Server databases to Azure SQL Database or Azure SQL Managed Instance where appropriate.
- Replace hardcoded connection strings with configuration injected at deployment time.
- Prefer managed identity for Azure resource access where the target service supports it.
- Avoid embedding secrets in Docker images, YAML files, or source control.
Step-by-Step Migration Path to Azure Container Apps
Step 1: Port or Upgrade the Application
For Azure Container Apps, the practical target is normally .NET 8+ running on Linux.
Typical work items:
- Convert project files to SDK-style projects where possible.
- Replace .NET Framework-only packages with supported .NET packages.
- Replace
System.Drawing.Commonusage with cross-platform libraries such as ImageSharp or SkiaSharp where image manipulation is required. - Replace Windows Authentication assumptions with Microsoft Entra ID, OpenID Connect, or an application-specific authentication model.
- Externalise configuration and secrets.
Do not skip this step for .NET Framework applications. A Windows Server Core Dockerfile is not sufficient for Azure Container Apps because the platform requires Linux container images.
Step 2: Build a Linux Container Image
Example Dockerfile for a modernised ASP.NET Core application:
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
EXPOSE 8080
ENV ASPNETCORE_URLS=http://+:8080
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o /out
FROM runtime AS final
WORKDIR /app
COPY --from=build /out .
ENTRYPOINT ["dotnet", "MyApp.dll"]
Build and test locally:
docker build -t my-modernised-app:1.0.0 .
docker run --rm -p 8080:8080 my-modernised-app:1.0.0
Step 3: Push to Azure Container Registry
az acr create \
--resource-group rg-containerapps \
--name wfengcontainerregistry \
--sku Basic \
--admin-enabled false
az acr build \
--registry wfengcontainerregistry \
--image my-modernised-app:1.0.0 \
.
az acr build is useful when build agents do not have Docker available locally. For large enterprise applications, use CI/CD with a controlled build agent and repeatable version tags rather than manual image builds.
Step 4: Create the Container Apps Environment
az containerapp env create \
--name wfeng-env \
--resource-group rg-containerapps \
--location malaysiawest \
--logs-destination log-analytics \
--logs-workspace-id <workspace-id> \
--logs-workspace-key <workspace-key>
Use workload profiles when you need dedicated compute characteristics, larger resource sizes, or isolation beyond the default consumption profile. Dedicated workload profiles include general-purpose profiles such as D4, D8, and D16. Do not describe these profiles as Windows-only; they are Container Apps workload profile sizes, not proof of Windows container support.
Step 5: Deploy the Container App
az containerapp create \
--name my-modernised-app \
--resource-group rg-containerapps \
--environment wfeng-env \
--image wfengcontainerregistry.azurecr.io/my-modernised-app:1.0.0 \
--target-port 8080 \
--ingress external \
--min-replicas 1 \
--max-replicas 5
For production, configure ACR access using managed identity or another approved identity pattern. Avoid enabling registry admin credentials unless there is a temporary migration reason and a compensating control.
Step 6: Configure Scaling
Container Apps supports HTTP scaling and event-driven scaling patterns. For internal applications with predictable idle periods, scale-to-zero can reduce runtime consumption, but it introduces cold starts.
az containerapp update \
--name my-modernised-app \
--resource-group rg-containerapps \
--scale-rule-name http-scale \
--scale-rule-type http \
--scale-rule-http-concurrency 100 \
--min-replicas 1 \
--max-replicas 10
Use min-replicas 0 only when cold starts are acceptable. For customer-facing portals or operational systems, keep at least one replica running unless the business has accepted the latency trade-off.
Step 7: Configure Storage and Secrets
For persistent files, prefer application-level storage services such as Azure Blob Storage. If the application still needs a shared file system, Container Apps can mount Azure Files, but this should be treated as a transitional design rather than a permanent substitute for application refactoring.
For secrets:
- Store secrets in Key Vault or Container Apps secrets.
- Inject configuration at deploy time.
- Do not bake secrets into container images.
- Rotate credentials after migration if they were previously embedded in
web.configor deployment scripts.
Step 8: Configure Custom Domain and TLS
Azure Container Apps supports custom domains and free managed certificates for eligible publicly reachable domains. Certificate issuance and renewal depend on the domain continuing to meet the managed certificate requirements.
A typical pattern is:
az containerapp hostname add \
--name my-modernised-app \
--resource-group rg-containerapps \
--hostname app.example.com
az containerapp hostname bind \
--name my-modernised-app \
--resource-group rg-containerapps \
--hostname app.example.com \
--environment wfeng-env
Validate the current Azure CLI syntax in your build pipeline because Container Apps CLI extensions change over time.
What If the App Must Stay on .NET Framework?
If the application still requires full .NET Framework and Windows Server Core, do not force it into Azure Container Apps.
Recommended interim options:
- Azure App Service for Windows Containers
- Good for IIS-based web applications that can be packaged as Windows containers.
- Lower operational complexity than AKS.
- Azure Kubernetes Service with Windows node pools
- Suitable when you already operate Kubernetes or need stronger orchestration control.
- Higher platform operations burden than App Service or Container Apps.
- Windows Server VM modernisation landing zone
- Suitable when dependencies are too coupled for a near-term container migration.
- Still improve the platform with patching, monitoring, backup, deployment automation, and least-privilege access.
Treat these as transition states. The long-term target should usually be .NET 8+ unless the application is near retirement.
Production Hardening Checklist
Before cutover:
- Confirm the application runs as a Linux container if the target is Azure Container Apps.
- Verify startup, liveness, and readiness behaviour.
- Move session state and file state outside the container.
- Configure Azure Monitor and Application Insights.
- Configure alerts for restarts, failed revisions, latency, and 5xx responses.
- Use managed identity where possible.
- Store secrets outside source code and images.
- Test scale-out and scale-in behaviour.
- Test cold-start impact if scale-to-zero is enabled.
- Validate rollback using Container Apps revisions.
- Confirm DNS, TLS, WAF, private networking, and outbound firewall requirements.
- Document the fallback path if the modernised container fails production validation.
Cost Considerations
Container Apps can reduce cost for variable or intermittent workloads because it can scale based on demand, including scale-to-zero for suitable applications. However, savings are not guaranteed.
Cost depends on:
- Replica minimums.
- CPU and memory allocation.
- Workload profile type.
- Request volume and execution time.
- Logging volume.
- External dependencies such as Azure SQL, Redis, Storage, Key Vault, and Application Insights.
- Whether the app has been modernised enough to run efficiently as a Linux container.
For steady 24/7 workloads, a VM, App Service plan, or AKS node pool may be cheaper. Use Azure Pricing Calculator with actual CPU, memory, traffic, and availability assumptions before committing to a migration business case.
Key Takeaways
- Azure Container Apps is not a direct Windows Server Core hosting target. It requires Linux-based x86-64 container images.
- Full .NET Framework usually means Windows Server Core. If the app cannot move to .NET 8+ or Linux, choose a Windows-capable Azure hosting option first.
- State externalisation is mandatory. Session state, files, cache, configuration, and background jobs must be moved out of the container or redesigned.
- Container Apps is a strong target after modernisation. Once the application runs as a Linux container, Container Apps provides managed ingress, scaling, revisions, Dapr integration, and simpler operations than a self-managed Kubernetes cluster.
- Do not sell cost savings without sizing. Variable workloads may benefit, but fixed workloads need a calculator-backed comparison.
Legacy .NET migration is not a one-size-fits-all exercise. If you have a .NET Framework application on Windows Server and need a realistic Azure migration path, start with the dependency assessment first — then choose the platform that matches the technical facts, not the platform that sounds newest.