DEV Community

Cover image for Why I Left Vercel for Coolify on My Own VPS
Diogo Soares
Diogo Soares

Posted on

Why I Left Vercel for Coolify on My Own VPS

A huge share of MVPs today look the same: a Next.js app, pushed to GitHub, deployed on Vercel in two clicks. I've built that way too, and for a first demo it's hard to beat.

The problem shows up when the MVP starts working. A page gets shared, a bot finds your API, a query gets slow. On a usage-based platform, every one of those requests is on the bill.

Today I run 7 applications, 5 databases and an observability stack on a single VPS, managed by Coolify. From Next.js and Node.js to Python services and self-hosted infrastructure, I deploy different stacks without depending on a platform-specific runtime. And an AI agent helps me run it, under permissions that limit what it can touch.

This post covers why I left Vercel, what Coolify on a VPS gives you (and what it doesn't), how to move a Next.js app, and how I let an AI agent manage the infrastructure without handing it the keys.


The problem with Vercel for MVPs: you can't predict the bill

Vercel is a great product. The issue is the pricing model. On the Pro plan you pay per seat, then usage on top of the included amounts: function invocations, active CPU, data transfer, edge requests. At the time of writing, the plan starts at $20 per seat per month, and published usage rates include around $0.15 per GB of data transfer beyond the included 1 TB (it varies by region) and $0.60 per million invocations. Check vercel.com/pricing for current numbers.

For a steady, low-traffic app that's cheap. For an MVP it's a risk, because MVP traffic is the one thing you can't forecast. Some hypothetical but realistic scenarios:

What happens What it costs you
An image-heavy page goes viral: 5 TB in a month ~4 TB over the included amount, roughly $600 in data transfer alone
A scraper calls your API in a loop Invocations and CPU time grow with every call
A slow query makes each function run longer Active CPU grows even when traffic doesn't

Vercel's Spend Management lets you set a budget, get alerts and optionally pause production deployments when you reach it. That's useful, but it leaves you choosing between a bigger bill or a paused app, right when people are looking.

What I wanted was boring: an infrastructure bill I know in advance.


What Coolify on a VPS gives you

Coolify is an open-source, self-hostable platform that covers most of what I used on Vercel:

  • Git push to deploy, with Nixpacks, a Dockerfile or Docker Compose
  • Automatic HTTPS with Let's Encrypt, through Traefik
  • Preview deployments for pull requests
  • One-click databases with scheduled backups to S3-compatible storage
  • Scheduled tasks, env variables, logs, health checks, rollbacks and an API

Everything runs in Docker, so each app is a standard container and nothing is tied to Coolify.

For me, the biggest advantage isn't the price. It's not being tied to one framework or runtime. My Next.js apps run alongside Node.js APIs, Python services and background jobs, and I deploy all of them the same way: a Dockerfile or a Docker Compose file, a Git repository, a merge into main. A new stack doesn't mean a new hosting provider or a new deploy workflow.

That extends beyond applications. Postgres, Redis, monitoring and S3-compatible object storage (MinIO or SeaweedFS, for example) run on the same server, and Coolify manages their deployment and lifecycle. It feels less like hosting a few apps and more like running my own small application platform. One caveat: object storage on a single VPS doesn't give you the durability of a managed service like Amazon S3, so keep anything you can't lose backed up elsewhere. Coolify can also send its backups to external S3-compatible storage.

My setup is a single KVM VPS with Coolify on the same machine:

Server 4 vCPUs, 16 GB RAM, 200 GB NVMe
Bandwidth 16 TB/month included
Running on it 7 apps, 5 databases, an observability stack
Price €27.99/month regular, often on promotion around €10.99 (what I pay today). VPS only.

Look at the last two lines together. At the promotional price, the whole server costs less than one Vercel Pro seat; at the regular price, roughly one and a half. Either way, the viral month from the table above, 5 TB of transfer, fits inside the bandwidth the plan already includes. One caveat: promotional prices usually cover the first billing term, so budget for the regular price when you renew.

If you want the same plan, here's my referral link. Disclosure: it's an affiliate link, so I may earn a commission if you sign up through it.

To be precise about "fixed": the server price doesn't change with traffic. A traffic spike can still saturate the server and slow things down, and external services (email, object storage, a CDN) bill separately. What changes is that a bot hammering my API costs me CPU I've already paid for, not a new line on an invoice.

On sizing: Coolify's current docs list a minimum of 2 CPU cores, 2 GB of RAM and 10 GB of free disk, for Coolify alone. Apps, databases and Docker images need room on top. A smaller plan with 2 vCPUs, 8 GB of RAM and 100 GB of disk is a reasonable start for a few small apps with Postgres. I went bigger mostly because builds are the heaviest moment: a next build can use a lot of memory, and it runs on the same machine that serves your traffic.

What you give up

On Vercel On a single VPS
Autoscaling You resize the server or add servers yourself
Global edge network One region, unless you put a CDN in front
Managed redundancy One server is a single point of failure
Platform security OS patches, firewall, SSH, secrets are your job

That last row is why most people stay on Vercel, so it comes first.


Harden the server before the first deploy

The baseline I'd apply to any VPS before installing anything:

  • SSH with keys only, no password login, no root login.
  • Automatic security updates (unattended-upgrades on Ubuntu/Debian).
  • A firewall at the cloud provider allowing only 22 (ideally from your IP), 80 and 443.
  • The Coolify dashboard on its own domain with HTTPS and 2FA, with the default ports (8000, 6001, 6002) closed once the domain works.
  • Databases never published to the internet. Apps reach them over the internal Docker network; use an SSH tunnel for local access.
  • Backups off the server, at a different provider, and a restore you've actually tested.

The trap almost everyone falls into: Docker writes its own iptables rules, so ports published by containers bypass ufw. A "deny all" ufw policy can still leave a database on 5432 open to the internet. That's why the firewall belongs at the provider level, and why databases shouldn't publish ports at all.


Moving a Next.js app off Vercel

Next.js runs as a normal Node server. Enable standalone output so the build contains only what the server needs:

// next.config.js
module.exports = { output: "standalone" };
Enter fullscreen mode Exit fullscreen mode

Then a multi-stage Dockerfile that runs as a non-root user:

FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production PORT=3000 HOSTNAME=0.0.0.0
RUN addgroup -S app && adduser -S app -G app
COPY --from=build --chown=app:app /app/.next/standalone ./
COPY --from=build --chown=app:app /app/.next/static ./.next/static
COPY --from=build --chown=app:app /app/public ./public
USER app
EXPOSE 3000
CMD ["node", "server.js"]
Enter fullscreen mode Exit fullscreen mode

What matters here: the runtime image holds only the compiled output the server needs, not your full source tree, dev dependencies or a root user.

Keep secrets out of the build. In Coolify, every env variable has an "Available at Buildtime" option. Turn it off for secrets. Values passed as build arguments can end up in the image metadata (docker history) or baked into the output if your code reads them during next build. Only variables the build really needs should be available there, like NEXT_PUBLIC_*, which are public anyway because Next.js inlines them into the client bundle. If a build truly needs a secret, use Docker build secrets instead of build arguments.

Then replace the Vercel-specific pieces: vercel.json crons become Coolify Scheduled Tasks, Vercel Postgres/KV/Blob become Postgres, Redis and any S3-compatible storage, and analytics can move to Plausible or Umami.

About downtime on deploys. Coolify can do rolling updates: it starts the new container and removes the old one only after the new one passes its health check. That requires a working health check, no host port mapping and no custom container name, and it doesn't apply to Docker Compose apps. Even then, Coolify's docs are clear that it doesn't guarantee zero downtime: your app still has to report readiness correctly and shut down cleanly.


Letting an AI agent manage it, with least privilege

The real cost of self-hosting isn't the server. It's the attention needed to set up every project correctly: database, env vars, previews, monitoring, and every security step, every time.

I do most of that with Claude Code connected to Coolify's built-in MCP server, which Coolify serves at /mcp on your own instance and authenticates with a Coolify API token. Through it, the agent inspects every app, database and deployment and checks env variable names against my rules. When a deploy fails, it reads the status and the failure reason and tells me what went wrong.

The agent can't deploy. The token has no deploy ability, so deploying, starting, stopping or restarting through the MCP is refused. Deploys happen only through Git: each app is connected to its GitHub repository, and Coolify deploys automatically when something is merged into main. The agent's changes, whether app code, SQL for a database role or a new exporter in my monitoring repo, go through a pull request that I review and merge. The only path to production is a merge I make.

In practice, GitHub and Coolify work very well together here. Coolify's GitHub App connects each repository, GitHub Actions runs the tests and checks on every pull request, and Coolify can spin up a preview deployment for the same pull request, so I review the code and see it running before I merge. With branch protection requiring those checks to pass, nothing reaches main untested, and the merge itself is the deploy. It's the same flow I had on Vercel, on infrastructure I control.

Architecture: Claude Code reads Coolify through the built-in MCP with a read-only, team-scoped token, and every change goes through a GitHub pull request that I merge to main, which triggers Coolify's auto-deploy

The key idea: security is enforced by permissions, not by asking the agent nicely. An instruction like "don't read secrets" can be ignored or bypassed by a prompt injection. A token that can't read secrets can't be talked into it.

Layer What it enforces
API token scope Bound to one team: the MCP only returns resources that team owns.
No read:sensitive The token doesn't have it, so sensitive data stays out of reach. I tested it: asking for a deployment's log summary is refused with Missing required permissions: read:sensitive.
Names, never values The MCP lists env variable names and flags (runtime, build time, preview), never their values, and never returns configuration snapshots.
No deploy ability Deploy, start, stop and restart all need it, and the token doesn't have it. Production changes only through the GitHub auto-deploy when I merge into main.
No shell access No SSH key and no shell on the host. The agent only sees what the MCP exposes.
Human approval Every change reaches production through a pull request I review and merge. SQL is reviewed before it runs.

The rules are the other half. My CLAUDE.md has a checklist that the agent applies to every new project before its first deploy:

## New project on Coolify
- Dedicated database role (NOSUPERUSER NOCREATEDB NOCREATEROLE),
  never the `postgres` superuser in DATABASE_URL.
- Admin seed runs once; then ADMIN_PASSWORD is removed and the
  password is changed in the app, with 2FA.
- Secrets are runtime-only: "Available at Buildtime" unchecked.
- Preview env vars get their own values, never production ones.
- The new database gets a monitoring user and an exporter.
Enter fullscreen mode Exit fullscreen mode

Each rule closes a specific hole. An app connected as the database superuser turns one SQL injection into "drop every database on the server". A preview pointing at production lets a buggy pull request change real data. A leftover admin password is a backdoor nobody remembers to rotate.

What permissions don't solve. CLAUDE.md is guidance, not isolation: it makes the agent consistent, but only the token and the MCP server actually restrict it. Prompt injection through logs or web pages is still possible, which is why the token's scope is the real limit on the damage. And the agent can be confidently wrong, so I review what it changes.

Writing these rules down also made something uncomfortable visible: apps I had set up before the rules existed don't all follow them. Asking the agent to audit my own infrastructure against them is the subject of the next post.


When I'd still choose Vercel

  • Nobody on the team wants to own infrastructure, even with an agent helping.
  • Traffic is global and very spiky, and edge latency matters more than cost.
  • You depend on Vercel-specific features and time-to-market beats everything.
  • It's a short-lived demo that stays inside the included usage.

For side projects and early MVPs, a VPS with Coolify and a well-constrained agent has been the better deal for me. And if an MVP outgrows one server, every app is already a Docker image: resize, split the database out, add servers, or move to a managed platform without rewriting anything.


FAQ

Isn't your time worth more than the savings?
The server setup is mostly a one-time cost. The per-project work is what the agent now does from the checklist. What remains is reviewing its changes and watching alerts.

What happens if the server dies?
Everything on it goes down; that's the biggest trade-off. Mitigation: off-site database backups, app config in Git, every app rebuildable from its image. Test the restore at least once.

What about DDoS and abusive bots?
A single VPS can't absorb a real DDoS. Put a CDN/proxy like Cloudflare in front and rate limit expensive endpoints. Within the plan's bandwidth allowance, an attack costs you performance rather than an unexpected bill, though what happens beyond that allowance, and with any paid external services, depends on your provider.

Why Coolify and not Dokploy, CapRover or plain Docker Compose?
All valid. I picked Coolify for its previews, database backups and, mainly, its API, which is what lets an agent manage it.

Do I need Claude Code specifically?
No. Any agent that supports MCP or can call the Coolify API works. The guardrails matter more than the agent.


Takeaway

  • Vercel's problem for MVPs is predictability, not quality. Usage-based billing means a bigger bill or a paused app at the worst moment.
  • Coolify on a VPS covers most of the developer experience at a known monthly price, with no lock-in.
  • Harden the server first, and remember that Docker-published ports bypass ufw.
  • Keep secrets out of the build: runtime-only env vars, build secrets when unavoidable.
  • AI doesn't remove infrastructure responsibility. It changes how you manage it: least-privilege tokens enforce the limits, written rules give consistency, and a human approves what can't be undone.

If you let an agent touch your infrastructure, how do you limit what it can do? I'd like to compare approaches in the comments.


I'm Diogo Soares, a Staff AI Engineer working on AI agents and legacy modernization. If this was useful, follow me here on DEV.to, let's connect on LinkedIn, or check out diogosoares.dev.

Top comments (0)