Last week we moved our production chatbot platform — an AI assistant that answers visitors’ questions using each client’s own website content (retrieval-augmented generation, or RAG) — from Microsoft Azure to a single IONOS Cloud Cube, IONOS’s vCPU-based virtual server line. Six services, two databases, a vector store and an API gateway; from the decision to “fully off Azure, teardown included” took ten days, with production cutover on day eight. We watched the chat logs through the cutover window and saw uninterrupted sessions — no client-side errors surfaced. This is the story of how, and of the decisions that made it small.
The original prompt
The chatbot began life as an innovation project: a multi-tenant RAG chatbot grounded in each client site’s own knowledge base. It grew up on Azure — App Services, Azure Functions, API Management, Azure SQL, Cosmos DB with vector search — largely because that’s where the credits were.
Three things prompted the move. Our Azure sponsorship credits had an expiry date. The platform’s Azure bill was heavier than its workload justified — the API gateway product alone cost more than all the compute underneath it. And strategically, we’d already had a good experience consolidating our WordPress estate onto a single IONOS Cube, so we wanted the chatbot on infrastructure we understood and controlled.
IONOS or Cloudflare?
We seriously evaluated two destinations. Cloudflare was tempting — Workers would have mapped neatly onto our Azure Functions API layer, Vectorize onto the vector store, Workers AI onto the LLM. But the evaluation kept coming back to one inconvenient truth: Workers only fit half the system. Our backend services and (especially) our Rails admin interface have no comfortable home on Cloudflare today, so “migrate to Cloudflare” really meant “migrate to Cloudflare and somewhere else, while rewriting the API layer” — under a deadline, with a two-person team.
IONOS took the whole system as-is: every service in one Docker Compose file on one Cube, sized to match the single Azure instance everything already ran on. The decision wasn’t anti-Cloudflare — it was pro-sequencing. A platform migration and an architecture rewrite are each manageable; coupling them is how migrations go wrong. Cloudflare stays on our roadmap for a future, leaner version of the platform.
The architecture we ended up with
On Azure: five Node App Services, one Azure Functions app, a Rails admin container, API Management in front, two Azure SQL databases, and Cosmos DB providing vector search. On IONOS:
- One Cube in IONOS’s UK (London) data centre running everything under Docker Compose
- nginx as the only internet-facing container — TLS via Let’s Encrypt, and it validates the same subscription-key headers API Management used, so existing clients needed nothing but a URL change
- PostgreSQL in a container for the relational data (chat logs, admin), migrated from Azure SQL with a driver-and-dialect refactor
- pgvector in a second container replacing Cosmos DB’s vector search — deliberately kept separate from the relational store so each has its own blast radius and backup cadence. The estate is modest and we’ll say so plainly: tens of megabytes of relational data and a few hundred embedded knowledge chunks across a handful of tenant knowledge bases — which is exactly why one well-run box beats a fleet of managed services here
- Hourly backups of every database to IONOS’s S3-compatible Object Storage, with daily and weekly tiers behind them — and restores actually tested to a scratch database before we trusted them. (IONOS’s managed PostgreSQL is the obvious next step if we ever want the databases off our own Compose file.)
There’s a data-jurisdiction dividend too: application, databases, vectors and backups now all sit in a UK data centre with an EU-headquartered provider — a materially easier conversation with clients than walking through a hyperscaler’s region and sub-processor map. The AI layer itself still calls OpenAI today; moving that to European processing is on the roadmap (see below), and we’d rather say that plainly than imply otherwise.
One choice worth highlighting: the Azure Functions app kept its runtime. Microsoft publishes the Functions host as a public container image, and with storage bindings emptied it runs happily on any Docker host. Keeping it meant zero changes to eight working HTTP handlers. Refactoring to plain Node can happen later, on our schedule — the boring path strikes again.
The other choice that saved real work: we kept our existing embedding model — the component that turns documents into the numerical vectors the AI searches — so every stored vector remained valid. Swapping embedding providers during a migration means re-embedding every tenant’s knowledge base and resizing vector columns — a whole project hiding inside a config change. We declined to smuggle it into this one.
And the bill? The new platform is a flat, predictable monthly cost instead of a dozen metered line items: roughly £23 a month all-in — the Cube, a static IP, and backup object storage. For scale: on Azure, the API gateway product alone cost several times that, before counting any compute or databases. Forecasting the bill now takes seconds.
The migration itself
Audit and the decision register
First, a complete inventory of the production environment — every resource, identity, setting and workflow — cross-checked by an LLM review pass that caught a handful of gaps before they could surprise us. Then a plan built around a decision register: sixteen decisions (database engine, gateway, region, DNS, LLM path, cutover strategy…), each written down with options and trade-offs, then closed one by one. Nobody re-litigated anything mid-migration.
Rehearsing locally: SQL Server to PostgreSQL, Cosmos DB to pgvector
The full data transformation — from SQL Server and Cosmos DB exports to PostgreSQL and pgvector — ran end-to-end against a copy of production data on a laptop before any cloud resource was provisioned. Rehearsal is where the work actually bit: the chat logger’s data layer had to be rewritten off the Microsoft SQL driver, and the Rails admin app had Azure-AD authentication baked into its production configuration that had to come out along with its SQL Server adapter. Better to find that on a laptop than during a cutover window. By provisioning day the scripts already worked; the day was execution, not discovery.
Cutover and teardown
Cutover was a WordPress plugin release pointing at the new endpoint, shipped to both production sites after fresh data dumps. The old platform stayed up as a rollback path while the new one soaked. Teardown followed within days: restores re-verified, the production resource group deleted in one command, and a final sweep confirmed nothing chatbot-related remained — including the forgotten dev-environment stragglers that every teardown seems to surface. Temporary credentials used during the migration window were revoked the same day. Net result: ten days, a rollback path live throughout, tested restores at every gate, and no data loss.
Tools that earned their keep
- The decision register — sixteen decisions with options, trade-offs and outcomes; the single best defence against mid-migration re-litigation
- Rehearsal scripts on production data — the export/transform/load pipeline was proven on a laptop, then reused verbatim on the server
- A Makefile over the backup/restore scripts — one entry point for dumps, tiered retention and scratch-database restore tests
- Microsoft’s Azure Functions container image — the escape hatch that let the API layer leave Azure without a rewrite
Lessons we’re carrying forward
- Rehearse on production data before provisioning anything. The single biggest de-risker. Migration day should be boring.
- A decision register beats a long thread. Write the options down, close them one by one, never revisit.
- Don’t change the embedding model mid-move. Vector data is only portable if its meaning is — keep the model stable and the vectors travel free.
- A backup that’s never been restored is Schrödinger’s backup. We gated teardown on tested restores, and it turned a scary step into a routine one.
- Match the migration to the team. One VM and Docker Compose is unfashionable and exactly right for six small services and two engineers. We accept the single-host blast radius — hourly tested backups and a Compose file that rebuilds the box are our answer at this scale. Kubernetes can wait for a problem worthy of it.
What’s next
CI/CD is being repointed from Azure pipelines to the Cube, the last Azure-specific logging configuration is being stripped as branches merge, and we’re planning an evaluation of open-weight models on IONOS AI Model Hub — Llama-class chat with European data processing — against our current OpenAI baseline. That would complete the residency story: the whole platform, AI layer included, running under European jurisdiction. And when the platform’s planned consolidation into a leaner monolith lands, we’ll revisit Cloudflare with an architecture that actually fits it.
If you’re weighing a similar move — off a hyperscaler, onto infrastructure you can reason about, or adding a grounded AI assistant to your own product — we offer exactly what you’ve just read about: an audit and a decision register for your setup, before anything moves. Get in touch.