Migrating a legacy .NET app to a Windows VPS

Moving a legacy .NET application off a shared hosting plan or an aging on-premise server onto a Windows VPS is one of those tasks that looks straightforward on paper but hits you with a dozen small surprises the first time you do it. The application pool resets under load, the SQL connection string points to a named instance that no longer exists, the old web.config section is locked by a parent applicationHost.config. This guide takes you through it step by step using a fresh Windows Server VPS with full Administrator access, IIS 10 pre-installed, and enough RAM to keep the w3wp.exe processes happy.
将旧版 .NET 应用迁移到 Windows VPS 时,IIS 应用程序池、连接字符串和 SQL Server 配置是最常见的障碍。
When migrating a legacy .NET app to a Windows VPS, the three most common friction points are IIS application pool configuration, connection string paths, and SQL Server setup.
Prerequisites
- A Windows Server VPS (2019 or 2022) with Administrator access, this guide uses a Windows VPS provisioned with a dedicated IPv4 from the Vietnam range, NVMe storage, and the Windows Server 2022 image.
- The .NET application files (compiled binaries,
web.config, static assets) available on your local machine or a file share. - Database backup if the app uses SQL Server (a
.bakfile or a SQL script with data). - Remote Desktop access (RDP) enabled, the VPS comes with RDP on port 3389; the Administrator password is set during provisioning.
- Basic familiarity with IIS Manager and SQL Server Management Studio (SSMS).
Why move to a VPS instead of staying on shared hosting
Shared hosting for .NET applications usually means you share the IIS worker process with dozens of other tenants, you cannot set your own application pool identity, you cannot install a specific .NET runtime version, and the SQL Server instance is often a shared one with a database size cap. A Windows VPS gives you a dedicated IIS instance where you control the application pool settings (idle time-out, recycling, process model), you choose whether to use SQL Server Express, Standard, or a remote instance, and you get a full Administrator account to tweak the OS-level knobs. For a legacy app that relies on specific COM+ components, registry keys, or a particular .NET Framework version range, this control is the difference between it running and it failing silently in the event log.
Step 1, Set up IIS and install the required Windows features
Log into the Windows VPS via Remote Desktop. Open Server Manager (it opens by default), click Manage → Add Roles and Features. In the wizard, select Role-based or feature-based installation, pick your server, and under Web Server (IIS) ensure these are checked:
- Common HTTP Features: Static Content, Default Document, Directory Browsing (if you need it for debugging).
- Application Development: ASP.NET 4.x, .NET Extensibility 4.x, ISAPI Extensions, ISAPI Filters.
- Security: Basic Authentication, Windows Authentication, URL Authorization (if your app uses integrated security).
- Management Tools: IIS Management Console, IIS 6 Management Compatibility (sometimes needed for older apps using IIS 6 metabase calls).
Let the installation complete and reboot if prompted. After the reboot, verify IIS is running by opening a browser on the VPS and going to http://localhost, you should see the default IIS welcome page.
# In an admin PowerShell, you can also check the IIS service:
Get-Service W3SVC
# Expected: Running
Step 2, Create an application pool for the legacy app
One of the most common mistakes when migrating a .NET app to a VPS is dropping it into the DefaultAppPool. Legacy apps often require a specific .NET CLR version, a specific pipeline mode (Classic vs Integrated), or a custom identity with access to a network share or a local folder. A dedicated pool isolates the app and lets you tune its settings without affecting anything else.
Open IIS Manager, select Application Pools, then click Add Application Pool on the right. Name it something descriptive (e.g. LegacyAppPool).
For a legacy .NET Framework 2.x or 3.x app: set .NET CLR Version to v2.0.50727 (this also handles 3.x). For .NET 4.x+ apps: select v4.0.30319.
Set Pipeline mode: if the app was written for IIS 6 or uses HttpModule entries in web.config the old way, start with Classic. You can switch to Integrated later if nothing breaks. I have more than once spent an hour debugging a 500 error that turned out to be a third-party HTTP module incompatible with Integrated mode, so Classic is safer initially.
Under Process Model → Identity, if the app needs to write to a specific folder (logs, uploads) or access a network share, assign it a domain service account or use ApplicationPoolIdentity and grant that account ACL permissions to the folder. The default ApplicationPoolIdentity is sufficient for most apps and is the recommended security posture.
# After creating the pool, verify it from PowerShell:
Get-IISAppPool -Name "LegacyAppPool" | Select-Object Name, State
# Expected: Name=LegacyAppPool, State=Started
Step 3, Deploy the application files to the VPS
Copy the compiled .NET application folder (the one containing bin/, web.config, .aspx or .aspx files, and static assets) to the VPS. The simplest method is to RDP in and copy-paste through Remote Desktop Clipboard (enable Clipboard in the RDP connection settings under Local Resources). For larger applications, compress the folder into a .zip on your local machine, transfer it via RDP, then extract.
Place the application folder under C:\inetpub\wwwroot\LegacyApp\. You can pick any location, but keeping it under wwwroot avoids permission surprises because the IIS_IUSRS group already has read access to that parent tree by default.
Right-click the folder, go to Properties → Security, and verify that IIS AppPool\LegacyAppPool (or the identity you set) has Read & execute and, if the app writes log or upload files, Modify permission on a dedicated subfolder (never grant Modify on the entire app directory unless you are certain).
# From PowerShell, grant read access to the application pool identity:
$folder = "C:\inetpub\wwwroot\LegacyApp"
$acl = Get-Acl $folder
$permission = "IIS AppPool\LegacyAppPool","ReadAndExecute","ContainerInherit,ObjectInherit","None","Allow"
$accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule $permission
$acl.SetAccessRule($accessRule)
Set-Acl $folder $acl
Step 4, Configure the site in IIS and point it to the new pool
In IIS Manager, right-click Sites and select Add Website.
- Site name: for example,
LegacyApp. - Application pool: select
LegacyAppPool(the one you created in Step 2). - Physical path:
C:\inetpub\wwwroot\LegacyApp. - Binding: If the app uses the VPS's public IP directly, set Type =
http, IP address =All Unassigned(or the specific dedicated IPv4 of the VPS), Port =80. If you have a domain, set the Host name accordingly. Leave the https binding for after you install a certificate, for now, HTTP on port 80 for internal testing.
Click OK. The site should start immediately. Right-click the site and click Manage Website → Browse to see if the application loads. If you get a yellow error page, read it carefully, it often points to a missing assembly version or a connection string problem, which we handle next.
Step 5, Handle the SQL Server database
Your options for the database layer vary depending on the size and licensing of the legacy app:
| Option | When to use | Notes |
|---|---|---|
| SQL Server Express (free, 10 GB database limit) | Small internal apps or apps under 10 GB | Installs locally on the VPS; the connection string uses localhost\SQLEXPRESS |
| SQL Server Standard | Production apps over 10 GB or with high concurrency | Requires a license; the VPS's monthly plan must include enough RAM (4 GB+ is recommended for SQL + IIS on the same box) |
| Remote SQL Server (another server or Azure SQL) | When you already have a database server in another location | The connection string changes from a local named instance to a remote server name or IP |
If you decide to install SQL Server Express locally: download the installer from Microsoft, run it, choose Basic or Custom installation, and during setup enable Mixed Mode Authentication (SQL Server + Windows Authentication). This is essential if the legacy app's connection string uses SQL logins rather than integrated security. Restore your .bak file using SQL Server Management Studio (SSMS): right-click Databases → Restore Database → Device, select the file, and restore.
# After the restore, verify the database exists:
SELECT name, state_desc FROM sys.databases;
-- Expected: your database name with state_desc = ONLINE
Step 6, Update the connection string in web.config
This is the step that trips up most people during a migration. The old web.config likely contains something like:
<connectionStrings>
<add name="MyConnectionString" connectionString="Data Source=localhost\SQLEXPRESS;Initial Catalog=LegacyDB;User ID=sa;Password=oldpassword;" providerName="System.Data.SqlClient" />
</connectionStrings>
If you installed SQL Express locally, change Data Source to your VPS's local instance. If you use a remote server, replace localhost\SQLEXPRESS with the remote IP or hostname and ensure SQL Server Browser and port 1433 are open on that server's firewall. Also verify that the User ID and Password match the SQL login you created during the Express install or on the remote instance.
A frequent gotcha: if the web.config connection string section is encrypted (common in production environments), decrypt it on the old server first using aspnet_regiis -pd before moving the file, then re-encrypt it on the VPS after updating the values. Attempting to decrypt a protected configuration section on a different machine will fail because the decryption key is machine-specific.
Test the connection from the VPS: open SQL Server Management Studio and try to connect with the same credentials from the web.config. If it connects, the application will too. If it does not, the error message tells you whether it is a firewall, credential, or network path problem.
Step 7, Bind the site to the public IP and test externally
Once the application loads correctly on http://localhost from within the VPS, test it from your local machine. Open a browser and go to http://[the VPS's dedicated IPv4]. If you see the application, the IIS binding and the VPS's firewall are both working.
If the page does not load, check two things: the Windows Firewall (open wf.msc → Inbound Rules → ensure World Wide Web Services (HTTP Traffic-In) is enabled for port 80) and the IIS binding (verify that the site is not restricted to localhost only).
# From a PowerShell on the VPS, test that port 80 is listening:
netstat -an | findstr :80
# You should see LISTENING on 0.0.0.0:80 or the specific IP
Troubleshooting common migration issues
The application pool stops immediately after starting. Check the Windows Event Log under Windows Logs → Application. A common cause is a missing .NET runtime version, go to Application Pools → Advanced Settings → .NET CLR Version and match it to the version the app was compiled against. If the app targets .NET 2.0 but you selected v4.0, the pool will crash.
HTTP 500.19, Internal Server Error. This usually means IIS cannot read the configuration section in web.config. Common causes: a locked parent section in applicationHost.config (run %windir%\system32\inetsrv\appcmd unlock config -section:system.webServer/handlers from an admin prompt), or a missing IIS role feature (re-run the Server Manager wizard and add the feature).
Login failed for user 'NT AUTHORITY\NETWORK SERVICE'. The application pool identity changed, and SQL Server does not know it. Solution: create a SQL login for the new identity (IIS AppPool\LegacyAppPool) and map it to the database, or switch the connection string to use a SQL login instead of integrated security.
FAQ
Can I migrate a .NET Core app the same way?
This guide focuses on the legacy .NET Framework (Web Forms, MVC, WCF). A .NET Core app runs as a self-contained process behind IIS (using the ASP.NET Core Module) and follows a different deployment pattern. For .NET Core, you would typically publish to a folder, configure the ANCM in web.config, and run it under dotnet.exe with a reverse proxy.
What if the app uses a COM+ component that must be registered on the VPS?
Copy the DLL to the VPS, open an Administrator command prompt, and run regsvr32 path\to\file.dll for 32-bit components, or use %windir%\SysWOW64\regsvr32 if the app runs in 32-bit mode. Then configure the application pool to Enable 32-Bit Applications = True in Advanced Settings.
Do I need a domain name for the migration to work?
No. You can test the entire migration using the VPS's dedicated IPv4 address. Add a domain later by binding it to the site in IIS and setting the DNS A record to point to the IPv4.
How do I handle HTTPS on the migrated app?
Install a TLS certificate (you can use Let's Encrypt via Certify The Web or a paid certificate) through IIS Manager → Server Certificates. Then change the site binding from HTTP to HTTPS on port 443 and optionally configure HTTP redirection to HTTPS in the site's web.config using the rewrite module.
Should I use Web Deploy or manual copy for the files?
For a one-time migration, manual copy is simplest. For ongoing updates, Web Deploy (msdeploy) allows incremental sync from a CI/CD pipeline. Install the Web Deploy feature via the Web Platform Installer or directly from Microsoft, then configure a deployment user through IIS Manager.
What if my app uses Windows Authentication and the VPS is not on the corporate domain?
In a workgroup environment (no Active Directory), Windows Authentication across different machines requires matching local user accounts with identical usernames and passwords. The simplest workaround for a small app is to switch to Forms Authentication or to SQL-based authentication during the migration, then plan a domain-joined setup later.
Related articles
- Sizing RAM for Windows Server VPS, why 2 GB is never enough
- Securing RDP on a public IP, the four settings that matter
- Migrate a website to a new VPS with zero downtime
将旧版 .NET 应用迁移到 Windows VPS 的关键步骤
本指南详细介绍了将旧版 .NET 应用从共享主机或本地服务器迁移至 Windows VPS 的完整流程。核心要点包括:在 IIS 10 中创建专用应用程序池并设置正确的 .NET CLR 版本和管道模式;处理 SQL Server 数据库(本地 Express 或远程实例);以及更新 web.config 中的连接字符串(注意解密和重新加密的步骤)。常见问题如应用程序池崩溃(检查事件日志和运行时版本)和 500.19 错误(解锁被锁定的配置节)都提供了排查方法。迁移完成后,使用 VPS 的专用 IPv4 进行外部访问测试即可确认一切正常。


