Escaping the Cloud: How I Moved from Vercel & Supabase to a €5 VPS

Another email from Supabase. “Your project has been paused due to inactivity.”

I run a few small side projects, apps I built for myself and friends. Nothing fancy, and nothing with thousands of users. Every few weeks I’d get that email, and the database would be paused again.

I’d log in, click “restore,” wait a few minutes, and hope nothing broke. It was free. Free was costing me patience, so everything went onto a single €5/month VPS. No more pausing, and no more vendor lock-in.

The old setup

I hosted on Vercel’s free tier. The database was on Supabase’s free tier, which is why it kept pausing. Analytics ran through Vercel Analytics. Monthly cost, €0.

The new setup

The machine is a Hetzner CX32. 4 vCPU, 8GB RAM, 80GB SSD, €5/month. Coolify is the self-hosted Vercel alternative that deploys the apps. Pocketbase replaced Supabase. It uses SQLite and about 30MB of RAM. Umami replaced Vercel Analytics and is self-hosted. €5/month for everything.

One server, multiple apps. They don’t pause and they don’t cold-start. I also stopped getting “you’ve exceeded your serverless function invocations.”

Setting up the VPS

I went with Hetzner because they’re cheap and their servers are solid. I picked a CX32, probably overkill for my needs, but I wanted headroom.

The setup was stupidly simple.

  1. Create a Hetzner account
  2. Spin up an Ubuntu 24.04 server
  3. Add my SSH key
  4. SSH in and run one command:
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash

Coolify installs itself. It sets up Docker, configures Traefik for SSL, and gives you a web dashboard. Five minutes, tops.

What Coolify does

Coolify is what Vercel would be if Vercel let you own your infrastructure. I push to main and it builds and deploys. Let’s Encrypt issues the SSL certificates. The web UI handles databases, Docker images, static sites, whatever.

I opened the dashboard at http://my-server-ip:8000, connected my GitHub account, and pointed a subdomain at the server. They use a GitHub App for webhooks, so deploys are instant.

Moving the site off Vercel

My personal website is an Astro static site. Migration took about 3 minutes.

  1. Remove @vercel/analytics from the code. It won’t work outside Vercel.
  2. In Coolify: Add Resource → GitHub → Select repo
  3. Enable “Static Site”
  4. Set the publish directory to dist
  5. Add my domain
  6. Deploy

Coolify detected it was an Astro project, ran npm install && npm run build, and served the static files. The SSL certificate appeared automatically. The site was live on my own server.

Replacing Supabase with Pocketbase

Padelica was the bigger job. It’s a padel match organizer I built for my friend group, and it used Supabase for the database with real-time subscriptions.

Pocketbase is an open-source backend in a single file. You get a SQLite database, a REST API generated from your schema, WebSocket-based real-time subscriptions, and an admin UI. Authentication is in there too. I wasn’t using it, but it’s nice to have.

It runs on about 30MB of RAM. Ridiculous.

Deploying Pocketbase

In Coolify:

  1. Add Resource → Docker Image
  2. Image: ghcr.io/muchobien/pocketbase:latest
  3. Add a volume mount for /pb_data so the data survives restarts
  4. Expose port 8090
  5. Add a domain like db.myapp.com
  6. Deploy

I opened https://db.myapp.com/_/, created an admin account, and started setting up collections.

Recreating the schema

Pocketbase has a UI for creating collections, which are tables. I recreated my Supabase schema:

  • games: stores match info
  • player_responses: who can play when
  • player_preferences: preferred time slots
  • confirmed_players: final roster

Each collection took about 30 seconds to set up. Relations work by creating a “Relation” field type and pointing it at another collection.

The API rewrite

This was the tedious part. Supabase and Pocketbase have different APIs:

// Supabase
const { data } = await supabase
  .from('games')
  .select('*')
  .eq('invite_token', token)
  .single()

// Pocketbase
const data = await pb.collection('games')
  .getFirstListItem(`invite_token="${token}"`)

Not dramatically different, but different enough that I had to touch every database call. The real-time subscriptions also changed:

// Supabase
supabase.channel('games').on('postgres_changes', { ... }, callback)

// Pocketbase
pb.collection('games').subscribe('*', callback)

Pocketbase’s syntax is cleaner. The migration took maybe an hour of find-and-replace work.

API rules

By default, Pocketbase locks down all collections. Only admins can access them. My app doesn’t have user authentication, so I needed to open up the API.

  1. Go to each collection
  2. Click the gear icon → API Rules
  3. Set List, View, Create, Update, Delete to empty, which means allow everyone

I also turned on rate limiting in Pocketbase settings. Two requests per second for creates, 300 per 10 seconds for reads. Enough to stop abuse, not enough to block legitimate use.

Self-hosted analytics with Umami

Vercel Analytics was nice, but it only works on Vercel. I needed something self-hosted.

Umami is lightweight and the UI is clean. No cookies, GDPR-compliant by default.

Deploying Umami

Umami needs a PostgreSQL database. In Coolify:

  1. Add Resource → Database → PostgreSQL
  2. Note the connection string
  3. Add Resource → Docker Image
  4. Image: ghcr.io/umami-software/umami:postgresql-latest
  5. Set environment variable: DATABASE_URL=postgresql://...
  6. Expose port 3000
  7. Add domain analytics.mydomain.com
  8. Deploy

First login is admin / umami. Change this immediately.

Adding the tracking script

In Umami, I added each of my websites and got a tracking script:

<script defer src="https://analytics.mydomain.com/script.js"
        data-website-id="abc123"></script>

I dropped it in the <head> of each site. I now have analytics that I own, on data that never leaves my server.

Security

With everything running on one box, I wanted to lock it down.

Firewall

ufw allow 22   # SSH
ufw allow 80   # HTTP
ufw allow 443  # HTTPS
ufw enable

Pocketbase rate limiting

I turned this on under Settings → Rate Limiting so someone can’t fill the database with garbage.

Strong passwords

Coolify admin, Pocketbase admin, Umami admin. All unique, all stored in a password manager.

SSH keys only

I disabled password authentication for SSH. If you don’t have my key, you’re not getting in.

What I pay now

Vercel was free, with serverless cold starts. Supabase was free, with constant pausing. Analytics was Vercel Analytics, locked to their platform. I had no control. Monthly cost, €0.

Hetzner is €5/month. Coolify, Pocketbase, and Umami are free because they run on that VPS. I have total control and full access to my data. Monthly cost, €5.

I can host as many apps as I want. Umami runs analytics without selling data to anyone.

When I’d still use the free tier

The free tier of Vercel and Supabase is good for getting started. If you’re building your first project and need to ship fast, use them. Don’t let infrastructure slow you down.

I moved once I had a few projects running and started hitting the limits. The third “your project has been paused” email was enough. Coolify made self-hosting almost as easy as the managed platforms.

When something breaks at 2 AM, I can fix it myself. I SSH in and read the logs instead of waiting on a support ticket.

My projects don’t pause anymore.