The starting point
Almost every company has at least one system like this: it quietly does its job, hardly anyone uses it, and nobody dares to touch it. At one of our clients, it was a document and records-management system running on a single server:
- Windows Server 2003 Enterprise
- ASP.NET applications, a mix of .NET 2.0 and .NET 4.0
- SQL Server 2005 Enterprise, on the same machine as the web server
- 5 web applications and 7 databases
- No source code, only compiled DLLs and configuration files
The application worked. The problem was everything around it. Windows Server 2003 and SQL Server 2005 haven't had a security update in over a decade. The goal was simple: move the application to a platform that receives security updates, and make sure it works after every single step.
Of course, there was the usual back story:
- People still look up documents in it once or twice a month.
- The company that built it, developers and all, is long gone. Sleeping with the fishes, concrete shoes included.
- How this Windows Server 2003 x86 Enterprise with SQL 2005 Enterprise ended up at the client, and whether there's a licence for it, has been erased even from the history books.
- I'd tried a few times to untangle what goes where, but there was no documentation and the setup was a proper bodge. My first half-hearted attempt, an in-place upgrade from 2003 to 2008 R1 SP2 x86 on a cloned VM, naturally broke everything.
- Now that Claude Opus 5.5 is here, things move really fast, and this was the perfect proof of concept.
Why not the cloud?
That was the first question, and it's worth sharing why the answer was "not now".
- Cross-database calls. The databases refer to each other by name (
[Database].[dbo].[Table]). Azure SQL Database doesn't support that. Azure SQL Managed Instance does, but its pricing isn't exactly compatible with Hungarian public administration. On top of that, there's still some spare capacity on the on-prem host. Even a minimal configuration would be around 600–700 EUR a month. - A COM component. One of the DLLs is a COM interop component, which Azure App Service won't let you register without rewriting it.
- Cost versus usage. For a rarely used application, an always-on VM plus a Managed Instance would cost far more than the value it brings.
The decision: stay on-premises, but on a supported platform, until the application is retired. (Ha, they've been retiring it for 10 years.) Good architecture sometimes means deliberately not choosing the cloud.
Discovery: how does it actually work?
With a sensible amount of time invested in my earlier attempts, I'd got about as far as: IIS 6 frontend, ASP application, .NET 4.0 and SQL 2005 with 7 databases, plus all sorts of bodges. Once I uploaded the wwwroot folder and a few SQL database dumps to Claude, the picture cleared up properly.
With no source code and no up-to-date documentation, the first job was to understand how the pieces connect. The obvious starting point is the connection string, except it wasn't where you'd expect it.
- Configuration files. There was no standard
<connectionStrings>section. Each application built its connection from its own settings, and the five applications referred to the database server in three different ways, including a loopback address (127.0.0.1) that would have broken as soon as the database moved. - The compiled DLLs. Luckily, no full reverse engineering was needed. Method names like
GetAlrendszServerandGetAlrendszDbgave it away: at runtime, the application looks up which server and database to connect to for each subsystem ("alrendszer" in Hungarian). - The database backup. A SQL Server
.bakfile is binary, but stored procedure code sits inside it as text. Extracting it gave us back roughly 1,300 stored procedures, and we found the key table: a subsystem registry, where every row holds a subsystem's server name, database name and network protocol.
The conclusion: the server name was hard-coded in three independent places: the configuration files, an XML-based login map, and as data inside the database itself. Changing only the configuration files would have left half the application pointing at the old server.
We also found out what wasn't needed: two of the seven databases (old archive years) weren't referenced anywhere, so they didn't have to move.
The key design decision: same server name, same SQL named instance
Keeping the names meant there was no need to dig even deeper into the code. Rewriting every reference, including database rows we didn't fully understand, would have been risky. So we turned the task around:
- The new server gets exactly the same machine name, SQL instance name and database names.
That way, every reference in the configuration files, the XML and the registry table stays valid, and nothing in the application needs to change. It also gave us the simplest possible rollback: switch the new server off, switch the old one back on, and everything carries on where it left off.
This could work because:
- the old server wasn't domain-joined, so reusing the name was easy,
- the application could be offline for a longer period, so the old and new servers never had to run at the same time,
- all the databases moved together: they refer to each other by name and have no separate authentication between them, so they behave as one unit.
Planning the upgrade path
The target platform is Windows Server 2025 with SQL Server 2025 Express. You can't get there in one step, because newer SQL Server versions only reliably restore databases that have already been upgraded to at least the SQL 2008 format. That shaped the plan:
| Stage | Operating system | Database | Purpose |
|---|---|---|---|
| Starting point | Windows Server 2003 | SQL Server 2005 Enterprise | – |
| Stage 1 | Windows Server 2008 R2 | SQL Server 2008 R2 Express | 32-bit to 64-bit migration; upgrade the databases to the SQL 2008 format |
| Stage 2 (target) | Windows Server 2025 | SQL Server 2025 Express | A platform that receives security updates |
Isn't dropping from Enterprise to Express a risky step back?
- We checked: the largest database is 1.2 GB, well within the Express limits. The application doesn't use SQL Agent jobs, full-text search, linked servers or any Enterprise-only features. A simple query (
sys.dm_db_persisted_sku_features) confirmed this before we made the call. - The "temporarily used" SQL Enterprise licence also helped make the decision.
What we prepared for in advance
- SQL collation: the new instance had to get exactly the same (Hungarian) collation, set at installation time.
- SQL logins: the application uses SQL authentication, so users had to be moved with their original SID and password hash, not recreated.
- Compatibility level: the databases stay on their old compatibility level at first, and we only raise it later, as a separate, tested step during the next upgrade.
- Express defaults: Express disables TCP/IP by default, but the application needs it, so it was enabled in SQL Server Configuration Manager.
- Rollback: at every stage, the previous server stays switched off but untouched.
What's next?
In Part 2, we cover the actual move to Windows Server 2008 R2 and SQL Server 2008 R2: a corrupt backup, a .NET version surprise, and a login error that looked like a wrong password but wasn't quite.
In Part 3, I'm hoping to make it all the way to Windows Server 2025 and SQL Server 2025 Express.
Have a legacy application nobody dares to touch? Cloud Craft Consulting helps New Zealand organisations with infrastructure modernisation, Azure, AWS and on-premises architecture, and FinOps.
Get in touch