Legacy modernization
What Actually Breaks When You Move Legacy .NET Off IIS
Every containerization plan starts the same way: write a Dockerfile, push to ECR, define a task, point a load balancer at it.
Then you run it and find out the application was never really running on IIS. It was running on one specific Windows server, with a decade of undocumented assumptions baked into it.
I've spent several hours moving systems off IIS onto containerized .NET on AWS ECS, now targeting .NET 10. Here's what actually breaks, roughly in the order it surprises you.
Case sensitivity, in the first ten minutes
Windows doesn't care whether you wrote Views/Shared/_Layout.cshtml or views/shared/_layout.cshtml. Linux does.
This is the first failure and the easiest to fix, but it sets the tone: every place the application has been sloppy for ten years is now a runtime error. Embedded resource names, view paths, static file references, paths built by string concatenation. You'll find them all at once.
Fix it before you containerize. Run the app on Linux locally first — dotnet run under WSL surfaces most of it in an afternoon, before you've spent a day debugging a task that won't start in ECS.
Windows-only APIs that compile perfectly
These are worse than compile errors, because the build is green and the failure is at runtime, in production, on the one code path nobody tested.
System.Drawing.Common has been Windows-only since .NET 7 — it throws PlatformNotSupportedException on Linux. Any legacy app that generates thumbnails, stamps PDFs, or builds barcodes is using it. You're moving to ImageSharp or SkiaSharp, and that's a real port, not a package swap.
Registry access, COM interop, and anything touching Office are the same story. Inventory them before you plan the timeline, not after.
Time zones, quietly, three weeks later
This one doesn't crash. It produces wrong data.
TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time") works on Windows and throws on Linux, where the identifier is America/New_York. .NET 6 and later will usually translate between the two — but "usually" is doing real work in that sentence, and if you're storing those strings in a database, you're on your own.
For anything that schedules, timestamps, or reports, audit every time zone reference before you migrate. In a city system, a report that silently shifts by five hours is worse than one that fails.
The connection string that worked for a decade
Moving from System.Data.SqlClient to Microsoft.Data.SqlClient flips Encrypt to true by default. Against an on-prem SQL Server with a self-signed certificate, every connection now fails on certificate validation.
The temptation is TrustServerCertificate=true, which makes the error go away and gives you encryption with no authentication. Fix the certificate instead. You're standing up new infrastructure anyway — this is the cheapest moment you'll ever get to do it properly.
The container defaults changed under you
The ASP.NET Core images stopped listening on port 80 as of .NET 8. They listen on 8080. Point a target group at 80 and you'll get health check failures with an application that is, as far as its own logs are concerned, running perfectly.
The chiseled images also run as a non-root user. Anything that writes to a path it doesn't own, or binds a privileged port, fails with permission errors that look nothing like permission errors.
Both are improvements. Both will cost you an afternoon if you meet them for the first time in ECS.
Anything that writes to local disk
Containers are ephemeral and horizontally scaled, which breaks three habits at once.
File uploads written to App_Data or a UNC share vanish when the task recycles, and are invisible to the other tasks behind the load balancer. Those move to S3.
Logs written to local files by log4net or NLog disappear with the container. They go to stdout, and CloudWatch picks them up.
InProc session state works on one task and breaks the moment you scale to two. You either enable sticky sessions — a delay rather than a solution — or move session to Redis or SQL Server. Move it.
Windows Authentication, structurally
For municipal and enterprise applications this is usually the real blocker, and it isn't a bug. It's an architectural dependency.
NTLM and Kerberos assume a domain-joined machine. A Linux container isn't one. There's no configuration flag that fixes this — the application has to move to a token-based model, which in practice means Entra ID with OIDC, and it means rebuilding whatever authorization sat on top of User.IsInRole against AD groups.
Budget for this as its own project, not a migration task. It touches authentication, authorization, session handling, and usually the entire admin surface.
Health checks, which will kill you in a loop
This is the failure that costs people the most time, because it doesn't look like an application problem.
An old ASP.NET application can take 60 to 90 seconds to warm up. The load balancer health check gives it 30. The check fails, ECS kills the task, restarts it, the check fails again. You watch the service cycle indefinitely and see nothing useful in the logs, because the application never got far enough to log anything.
Set the health check grace period from measured cold start rather than optimism. And keep the health endpoint cheap — if it queries the database, a slow database becomes a service outage.
Graceful shutdown, which nobody tests
ECS sends SIGTERM and waits before SIGKILL. If the application ignores SIGTERM, every deployment drops in-flight requests.
For a public-facing form, that's a resident losing a submission mid-deploy. Handle the signal, drain connections, and set the deregistration delay to match.
Connection pools, at exactly the wrong moment
Each task runs its own connection pool. Four tasks with a max pool size of 100 is 400 connections against a SQL Server that may not be sized for it.
This never shows up in staging with one task running. It shows up under load, in production, as timeouts that look like a database problem.
The one you shouldn't migrate
Web Forms has no path to .NET 10. There's no lift-and-shift, no compatibility shim, no clever Dockerfile. The .aspx page lifecycle, ViewState, and server controls end at .NET Framework 4.8.
If it's Web Forms, the options are: leave it on Windows and migrate something else, run it in a Windows container and accept the cost, or rewrite it. All three are legitimate. Pretending there's a fourth is how a three-month project becomes a year.
Be honest about this during planning, when it's a scoping decision, rather than in month four when it's a failure.
The order I'd do it in
If I were starting again:
- Get it running on Linux locally, outside a container. Case sensitivity, time zones, and Windows-only APIs all surface here, cheaply.
- Resolve authentication. It's the longest pole and it changes the shape of the application.
- Externalize state — files to S3, session to Redis, logs to stdout, secrets to Parameter Store.
- Containerize. On .NET 10 that's often dotnet publish /t:PublishContainer and no Dockerfile at all.
- Then ECS, load balancer, and pipelines — the part the tutorials cover, and by this point the easy part.
Most failed migrations I've seen inverted this list. They started with the Dockerfile, because that's the part that looks like the work, and found the architectural problems after the infrastructure was already built around them.
On .NET 10 you may not even write one. It was never the hard part.
Found this useful? Share it.
A ready-made LinkedIn post is one click away, or share the link wherever your team talks.
Every plan to containerize legacy .NET starts the same way: write a Dockerfile, push to ECR, point a load balancer at it. Then you run it and find out the application was never really running on IIS. It was running on one specific Windows server, with a decade of undocumented assumptions baked in. What actually breaks when you move legacy .NET to Linux containers on AWS ECS, roughly in the order it surprises you: • Case sensitivity, in the first ten minutes • Windows-only APIs that compile perfectly and throw at runtime: System.Drawing, registry, COM • Time zones that do not crash, they produce wrong data three weeks later • Windows Authentication, which is an architecture change, not a config flag • Health checks that kill a slow-starting app in a loop • Web Forms, which has no path to .NET 10 at all And the order I would do it in if I were starting again. Hint: the Dockerfile is last, and on .NET 10 you may not write one. {url}