Not every Umbraco site belongs on Umbraco Cloud, and not every one belongs on bare Azure. Here's the split we settled on - and why it's actually three options, not two.

The three buckets

We run everything across three environments. Which one something goes into is determined by who owns it, what it does, and whether Umbraco Cloud's deploy model is an asset or an obstacle.

Umbraco Cloud is where client sites go when the client holds the subscription. Some of our key clients hold their own Umbraco Cloud accounts, we technically manage them on their behalf. Others, we provide everything ourselves and charge a monthly management fee.. Cloud handles backups, SSL, environment promotion (dev to staging to live), and the git-based deploy workflow. The tradeoff is predictability: Cloud runs a managed Umbraco version, handles the infrastructure layer, and gives the client a clean upgrade path. The constraint is that anything outside standard Umbraco - custom background services, bespoke SQL tables, non-standard middleware - is harder to justify or run.

Azure App Service is where services go - not sites. Printo's API (api.printo.im) runs on a B1 Windows App Service in UK West, with Azure SQL for tenant data and Azure Blob Storage for PDF caching. Eddi's central service is the same pattern. These are ASP.NET Core Web APIs with no Umbraco dependency, and Azure's managed environment is the right fit: Application Insights for telemetry, EF Core migrations to Azure SQL, GitHub Actions auto-deploy on push to main. The cost is higher per unit than IIS, but the operational overhead is lower.

Dedicated Windows IIS is where our own Umbraco 17 products live. Patch, Printo's marketing site, Eddi's site, Bayr - all deployed to the same Windows server via self-hosted GitHub Actions runners and robocopy. This gives us full control: custom SQL tables via EF Core, data protection keys persisted outside the site root, background services, Smidge for asset versioning, and the ability to run multiple products on one box at a fixed monthly cost. It's not serverless and it's not zero-ops, but for products we own and operate ourselves, it's the right tradeoff.

When Cloud wins

For a client site with a content team and no unusual infrastructure requirements, Cloud pays for itself quickly. The deploy workflow - commit to git, Cloud promotes through environments - removes a whole category of "it works on staging but not on live" problems. Environment variable management, SSL renewals, Umbraco version compatibility - all handled.

The practical test: if the site could have been built by anyone with standard Umbraco knowledge and deployed to Cloud out of the box, it probably belongs there. Content-heavy, single-site, editor-facing - Cloud.

One hard rule: never let the database straddle Cloud and a custom host. Umbraco Cloud manages the database for sites on the platform. If you need custom SQL tables for user data or application state, you either keep everything on Cloud (using Umbraco content nodes for that data, accepting the constraints) or you move the whole thing off Cloud. Mixing the two creates a support problem with no good owner.

When we go to IIS instead

The signal that something belongs on our own server is usually one of these: it needs EF Core custom tables alongside Umbraco, it has background services that need to persist across deploys, or it's a product we're iterating on fast enough that Cloud's deploy cadence would slow us down.

Patch is a good example. It runs Umbraco 17 as the CMS and backend, but the user-facing app is a PWA served through a Capacitor wrapper. Seven custom SQL tables - garden beds, plantings, tasks, harvests, diary entries - live alongside the Umbraco content tables. Data protection keys sit outside the site root so auth cookies survive deploys. A daily task generation service runs on a hosted service timer. None of this is impossible on Cloud, but the friction would have been significant.

The operational gotchas on IIS are real and worth knowing before you commit. Robocopy with /MIR - which mirrors the source and deletes anything not in it - will wipe your web.config and your data protection keys if they're inside the site root. Secrets belong in appsettings.Production.json on the server, excluded from robocopy. The Smidge asset cache survives deploys unless you explicitly clear it. These are solvable problems, but they're your problems to solve.

The Printo split

Printo is the clearest example of the hybrid pattern. The API (api.printo.im) runs on Azure App Service with Azure SQL and Blob Storage - a proper cloud-native service architecture with Application Insights, Polly retry policies, and auto-scaling available if needed. The marketing site and member portal (printo.im) runs on IIS alongside everything else, deployed via the same robocopy pipeline.

The split exists because they're genuinely different things. The API is a multi-tenant service that could serve any customer. The site is an Umbraco 17 install with a member portal, a LemonSqueezy webhook endpoint, and backoffice tooling for tenant management. Hosting them the same way would mean either running Umbraco on Azure App Service (fine, but expensive and operationally heavier than IIS for this workload) or running a stateless API on a Windows IIS box (possible, but you lose the managed scaling and monitoring that Azure gives you for free).

The rule we keep coming back to

Pick the host that matches the team who'll be answering the pager at 2am. For a client site on Cloud, that's Umbraco HQ for infrastructure and you for the application. For a service on Azure, that's Microsoft for the platform and you for the code. For a product on your own server, that's you for everything - which is fine as long as you've made that decision consciously.