On September 5, I moved PreVibe from Vercel to a DigitalOcean VPS to get more control over its hosting.
PreVibe helps founders research SaaS ideas before investing time in building them. Besides the web dashboard, it exposes research tools through MCP, so people can use them from their AI assistants. Those tools can keep a request open for several minutes while research runs.
That workload made me reconsider the hosting model. The migration became a useful exercise in taking control of a live app's infra, from the initial server setup to what happens when the next deployment replaces a running container.
Why I moved
Reducing the hosting cost of long MCP calls on Vercel's Fluid Compute was the main reason for the move. I also wanted a setup whose capacity, costs, and deployment behavior I could understand more easily, and chose to return to DigitalOcean.
For PreVibe, I preferred paying for a set server capacity I could scale when needed, and running the app myself. That gives me more control over long requests, proxy behavior, and deployments. But it also puts server maintenance and capacity planning on my plate, which I'm okay with.
Keep the migration focused
I moved the Next.js application, including its MCP endpoint, while keeping the database and authentication on Supabase Cloud. Payments, email, and AI providers also stayed with their existing services.
The new setup is small:
| Component | Where it runs |
|---|---|
| HTTPS and reverse proxy | Caddy container on the VPS |
| Next.js web app and MCP tools | App container on the same VPS |
| Database and authentication | Supabase Cloud |
| AI, payments, and email | Existing external providers |
Caddy accepts public web traffic and forwards application requests over Docker's internal network. The app's port is not published on the host.
Keeping the public domain and database unchanged meant fewer moving parts. Existing integration URLs could stay as they were, and the hosting move needed no database migration. It also made the rollback plan simpler: the old and new deployments could use the same managed backend.
Get the application running on the VPS
I started with Ubuntu 24.04 on a small Droplet and added swap because the application builds on the server. How much memory the build needs matters when picking a small VPS, even if the running app fits easily.
For the initial server setup, I used secure-ubuntu-server, the open-source hardening script I maintain. It set up key-based SSH access, a non-root sudo user, firewall rules, fail2ban, and Docker. Huge time-saver. But future OS updates, backups, and monitoring still need my attention after launch, of course.
I packaged PreVibe using a multi-stage Docker build and Next's standalone output. The runtime image contains the traced server files, plus the public and static assets copied explicitly.
Two details needed particular attention:
First, browser configuration has to be present when the image is built. Next.js inlines NEXT_PUBLIC_* values into the client bundle. Supplying them only when the container starts is too late. I passed the public Supabase configuration and application URL as build arguments, with server-side secrets supplied through the runtime environment.
Second, the standalone server initially failed at startup because an @swc/helpers module was missing from the packaged output. The final setup uses a hoisted dependency layout inside the Docker build and explicitly includes the helper files in Next's output tracing.
That is why I tested the actual container on the VPS. A successful build alone wouldn't have caught the startup failure. I also checked dynamic social preview image generation, which runs through different code than loading the homepage.
Move traffic in stages, keep a way back
I first brought up the application on a temporary subdomain with real HTTPS. That let me check the container, proxy, certificates, and endpoints before switching to the live domain. The authentication round trip needed another check after cutover because its URLs still pointed to the production domain.
The move covered eight hostnames: the main domain and seven alternatives that redirect to it. I moved the main domain first, checked the site and login flow, then moved the remaining names and verified their redirects. Caddy manages the HTTPS certificates, and preserves paths and query strings when redirecting to the main domain.
I kept the previous Vercel deployment live as a DNS rollback target and disconnected its Git integration before merging the migration branch. That preserved the known working deployment while the new infrastructure became the source of production traffic. A DNS rollback would still depend on propagation and caches, so it was a recovery option rather than an instant switch.
The cutover checks covered the homepage, login, MCP access, HTTPS, and all seven redirects. I also updated PreVibe's privacy page to reflect the hosting provider change.
Deployments and CI
Getting the first container online was only part of the job. During follow-up deployments, I noticed proxy errors while the application container was being replaced. The final Caddy configuration fixes this with dynamic DNS discovery for the app container and a bounded retry window to bridge a brief startup gap, and the deployment workflow validates and reloads that configuration before rebuilding and replacing the app.
Another subtle issue was the configuration mount. Mounting a single Caddyfile could leave the container reading the old file after Git replaced it. Mounting the configuration directory made those file replacements visible to Caddy.
Deployments now run through GitHub Actions on application changes pushed to main, with a manual trigger available too. Deploy runs are serialized so they don't overlap on the server.
Replacing the app container can still interrupt an active MCP research call. PreVibe already has recovery for this at the application level: its MCP server exposes retry_research and retry_deep_dive, which regenerate an unfinished report on the existing record, with a cooldown so a call that stopped before it could record a failure isn't retried prematurely. The MCP client invokes the retry, which starts generation again rather than resuming the interrupted call - a recovery path for users, while leaving room to improve how deployments handle work in progress.
One future direction is to move the build off the production server entirely: build the image in GitHub Actions, publish it to GitHub Container Registry, and have the VPS pull and run that prebuilt image. That would leave the server responsible only for downloading the image and replacing the running container.
A grain of salt
The first tradeoff is deployment speed. My Vercel deployments took about one minute. On the VPS, a deployment with a cold cache takes about five minutes. But that build time isn't downtime. The app keeps serving while the new image builds; only the quick container swap at the end interrupts it.
Building on the VPS has a subtler cost than speed. Over a week of repeated on-server builds, Docker's memory use crept upward until it started competing with the running app on a small VPS. My current mitigation is blunt: a scheduled dockerd restart once a week to reclaim it. The better fix is to stop building on the production box at all.
For PreVibe, I'm comfortable with that longer release cycle. The current Docker build already caches dependencies and compilation work, so a warm build is faster than the cold-cache case above.
Where PreVibe stands now
PreVibe now serves its web app and MCP tools from the VPS, with HTTPS, domain redirects, and automated deployments in place. The database and external integrations remain on their existing services.
As PreVibe's founder, I chose to take on server maintenance in exchange for more control over how the product runs. The next step is to see how the VPS handles real research traffic and what the hosting costs look like over a full month, then use that experience to decide what needs improving.
The work involved packaging the app, establishing a server baseline, checking real user flows, preserving a rollback option, and fixing deployment behavior. Getting the application online was only the first step. Checking recovery paths and making subsequent deployments reliable took work too.
For a similar project, I'd start with the workload and the reason for moving. Request duration, concurrency, the team's ability to operate a server, and the cost of that work all shape whether a VPS makes sense.
Disclosure: The DigitalOcean link in this post is a referral link. If you open a new account through it, you may receive sign-up credit and I may earn a small credit, at no extra cost to you.