Vercel with a managed database versus a VPS on Coolify: 271 USD against 36 EUR a month at 2 TB of traffic, plus three failures we hit in production.

Serverless pricing is built so that starting costs nothing. It works: a project goes live without an invoice, and the first bills arrive only once something starts to live. The problem is that they grow in proportion not to revenue, but to traffic, headcount and function execution time — three things nobody forecasts at the start.
This article is an arithmetic exercise, not a manifesto. Vercel with a managed database on one side, your own VPS running Coolify on the other. The numbers come from vendor price lists (read on 5 October 2026), and the three failures described at the end happened to us in production, on exactly the stack that serves the site you are reading.
If you are looking for step-by-step instructions, they live separately: our public starter on GitHub carries a ready configuration of this stack, deployment files included.
The comparison only means something once both sides count the same things: the application, the database, traffic and file storage. Assume a three-person team and 2 TB of traffic a month — the scale of a small content site, not a funded startup.
Item | Vercel + Atlas | VPS with Coolify | Where the number comes from |
|---|---|---|---|
Application | 20 USD / seat → 60 USD | included with the server | |
Database | from 57 USD (Atlas M10) | included with the server | |
2 TB of traffic | 1 TB included, the second ~154 USD | included (20 TB in the EU) | |
Server | — | 35.99 EUR (CPX32) | |
Total | ≈ 271 USD | ≈ 36 EUR |
A Polish company is invoiced for these services in USD and EUR, not in złoty, so the bill moves with the exchange rate; the amounts are net, and VAT is settled by reverse charge.
Two numbers in that table deserve a comment, because they are the ones doing the work. First: traffic. Vercel Pro includes 1 TB and charges 0.15–0.35 USD per gigabyte above it, depending on the region. Hetzner includes 20 TB in European regions — twenty times more, as part of the price. Second: the database. Those 57 USD buy one M10 node, and a production database runs as a three-node replica set, so the real bill is closer to three times that before you add backups and traffic.
Traffic is worth counting past 2 TB, because it is the line that grows with your audience. At the lowest rate of 0.15 USD per gigabyte, every terabyte above the plan costs about 154 USD: at 3 TB the Vercel-plus-Atlas bill reaches about 424 USD a month, at 5 TB about 731 USD. The Coolify server costs the same 35.99 EUR in every one of those cases.
The Vercel-plus-Atlas bill grows with traffic; the Coolify server does not
Vercel, MongoDB Atlas and Hetzner Cloud price lists, read 5 Oct 2026; own calculation
There is also an item no table shows: on your own server every additional service is free. A queue worker, your own cron, Redis, a small helper service — same machine. Under per-execution billing each of them gets its own line on the invoice.
The difference in subscriptions is not a saving; it is a cost moved off an invoice and onto somebody's time. You take on three duties you did not have before: backups together with point-in-time restore, security updates and watching availability. They do not disappear because the bill got smaller — they change owner.
The honest conversion is this: if maintenance takes two hours a month, multiply them by the hourly rate you pay — at PLN 150 an hour that is PLN 300 a month — and subtract that from the difference in subscriptions before you call what is left a saving. Self-hosting pays off when those hours are in the team anyway and cover several projects at once, not one.
The monthly bill: Vercel with Atlas versus a VPS with Coolify
Vercel, MongoDB Atlas and Hetzner Cloud price lists, read 5 Oct 2026
Since version 3, Payload is not a separate application standing beside Next.js. It runs as a package inside it, sharing the same process, the same build and the same config file. That changes deployment architecture more than it sounds: the admin panel, the API and the frontend are one artefact, so one application container instead of two. What Payload can do as a CMS and when we choose it is covered in: Payload CMS; Next.js itself we take apart in: Next.js.
By default we go with one application container plus separate containers for the database, Redis and the proxy. Splitting the application into more processes only makes sense once one of them scales differently from the rest — a worker chewing through a job queue, say, which needs memory during a data import and does nothing for the rest of the day.
Service | Role | Why separate |
|---|---|---|
app | Next.js + Payload in one process | one build, one artefact, one restart |
database | MongoDB (or Postgres — Payload 3 supports both) | a different lifecycle from the code; it outlives every deployment |
redis | cache, sessions, shared state across instances | without it the cache is local to the container |
proxy | Traefik, managed by Coolify | routing and Let’s Encrypt certificates automatically |
Next.js can build a directory containing only what is needed at runtime — without the full tree of development dependencies. It is one line in the config (output: "standalone"), and the difference in image size is an order of magnitude: from roughly 1.5 GB down to about 150 MB. This is not cosmetic. A smaller image means a shorter transfer on every deployment and many times less disk eaten by the successive versions Docker keeps locally.
There is one condition and it is easy to miss: in a multi-stage build you have to copy the static asset and public directories by hand, because standalone does not take them. Skip it and you get a working application with no styles — a symptom that looks like a CSS problem and is a Dockerfile problem.
The core of the configuration is below. Coolify adds routing, the domain and the certificate on top, so the file says nothing about Traefik — that is precisely the part you use it for instead of bare Docker.
1services:2 app:3 build: .4 environment:5 DATABASE_URI: mongodb://mongo:27017/app6 PAYLOAD_SECRET: ${PAYLOAD_SECRET}7 REDIS_URL: redis://redis:63798 volumes:9 - media:/app/public/media # EVERY upload directory, separately10 depends_on: [mongo, redis]1112 mongo:13 image: mongo:714 volumes:15 - dbdata:/data/db1617 redis:18 image: redis:7-alpine19 command: redis-server --save 60 120 volumes:21 - redisdata:/data2223volumes:24 media:25 dbdata:26 redisdata:
How a Next.js and Payload deployment on a VPS with Coolify is put together
Digital Vantage, own diagram
A note on volumes, because this is the most common mistake. A container filesystem is ephemeral. Every directory the application writes files to must have its own entry under volumes — and “every” means every one, separately for each upload collection. Adding a new collection in Payload without adding its volume is not an error you will see in the logs. You will see it after a restart, when the files are gone.
The complete set — Dockerfile, compose, environment variables and a pre-launch checklist — sits in our public repository: nextjs-payload-starter.
Guides describe the deployment that worked. Below are the three mechanisms that break it most often — each with a real incident from our production as evidence rather than illustration.
Three things that break self-hosting — mechanism, symptom, our incident, fix
Digital Vantage, incidents from our own production
The next/image component processes images inside the application process by default, using the sharplibrary. On a managed platform a separate, scaled service does this and nobody notices. In a container on a VPS with 2–4 GB of memory, uploading a handful of product photos through the Payload panel can summon the OOM Killer — the kernel kills the process to save the system. The application vanishes with no entry in the application logs, because it never got to write one.
The business consequence is out of all proportion to the cause: the site stops responding in the middle of the day, and if a search engine crawler happens to arrive, the page drops out of the index for far longer than the outage itself lasted.
The fix has two steps. Files move off the server into S3-compatible storage — in Payload that is @payloadcms/plugin-cloud-storage with an adapter for Cloudflare R2, AWS S3 or a Hetzner Storage Box. Image processing goes outside too: either to a CDN that transforms on the fly (Cloudflare Images), or by turning off the built-in optimiser (images.unoptimized) and generating sizes on the Payload side at save time. The application then stops holding in memory something it has no reason to hold. More on choosing hosting, a CDN and what actually makes a site faster: Hosting, domains and CDN.
Our version of this bug was worse, because it was quieter. We did not run out of memory — we ran out of volume. One collection's directory had no persistent storage attached, so a container restart took every one of its files while the database records stayed behind, insisting the files existed. It happened twice: once with a media collection, once with the documents collection — eleven report and template files gone from disk while the site went on offering them for download.
On Vercel, static page revalidation and revalidateTag work at the level of a global CDN. In your own container the Next.js cache writes to .next/cache on that container's disk. Two things follow, both unpleasant: with two instances each has its own unsynchronised cache, and publishing content in the panel does not clear it by itself. This is, incidentally, one of the prices of headless architecture — more on that in: How headless architecture changes business strategy.
The fix. Your own CacheHandler pointed at from next.config, keeping entries in Redis — then the state is shared across all instances and survives a restart. Plus an afterChange hook in the Payload collections which, after a save, calls revalidatePath or revalidateTag for exactly what changed. The minimal variant — a persistent volume on .next/cache — solves the restart only, not horizontal scaling.
Our version: a disk filled to 89 GB, though nobody had uploaded anything. Bots were scanning addresses that did not exist, and on-demand rendering wrote every one of those responses as a cache file under .next/server/app. It grew for weeks, symptom-free, until the disk ran out. The fix turned out to be one line — a filter that sieves such requests out before they generate a file. The lesson, though, is not about the filter: on your own server the cache is your directory on your disk, and nobody tidies it for you.
If the database sits on the same machine as the application, then docker build takes practically the whole CPU and memory during a deployment. The effect: the site slows down exactly when you are shipping a fix — which is usually when something is already broken. With 8 GB of RAM the Next.js compile can also simply be killed.
Two traps that caught us specifically. First: the build server has no access to the database, so every function generating paths at build time has to account for that — otherwise the build passes locally and falls over in production. The second is sneakier: next build type-checks the whole project, so a directory excluded by .dockerignore can break a deployment while CI — building outside Docker — stays green. That cost us several deployments before we understood that green CI and a successful deploy are two different things.
Certificates. Coolify renews them automatically through Let’s Encrypt, but renewal requires the proxy to answer an HTTP challenge. If Cloudflare sits in front of the server in full proxy mode and the rules do not let everything through, renewal quietly fails and you find out ninety days after the deployment.
The decision is rarely technical. It comes down to whether anyone on the team will pick up the phone when the server stops answering on a Saturday.
Choose your own server if | Stay on a managed platform if |
|---|---|
traffic is predictable and grows gradually | traffic jumps by orders of magnitude (campaigns, seasons) |
you run several projects on the same machine | it is one project and one site |
someone on the team is comfortable with Docker | nobody wants to be a server administrator |
data has to stay in a specific jurisdiction | time to market matters more than the bill |
a fixed, predictable invoice has value in itself | you would rather pay more for not being on call |
If, after this arithmetic, your own server still looks sensible, the starting point is our public starter — Next.js 16, Payload 3, MongoDB and a Coolify deployment in one repository: nextjs-payload-starter.
The wider technology context — what the choice of stack does to the cost of a project — we take apart in: Comparing ways to build a website. And if you want the full maintenance bill, not just hosting: Recurring website fees.
We build deployments like this for ourselves and for clients — see how we build web applications.
The version you install on your own server is open source and free, with the full feature set — you pay only for the VPS. What costs money is Coolify Cloud: 5 USD a month for two connected servers and 3 USD for each further one. It is worth understanding what that buys: Cloud hosts the control panel, while your applications still run on your server (Coolify pricing).
For a content site, a sensible starting point is 4 vCPU and 8 GB of RAM — the Hetzner CPX32 tier at 35.99 EUR a month. Below 4 GB the problem is not running the site, it is building a new version and processing images. If you build the image off the server, in GitHub Actions, the requirements drop noticeably.
Yes, but not on its own. By default the cache goes to .next/cache inside the container, so every instance has its own and nothing clears it when content is published. The combination that works is your own CacheHandler backed by Redis plus an afterChange hook in Payload calling revalidatePath after a save.
No. Since version 3, Payload runs as a package inside the Next.js application — same process, same build, one container. The database, Redis and the proxy get their own containers, but the admin panel does not.
It is a duty you take on together with the server, and the most common place where the saving turns out to be illusory. A daily dump is enough only if you accept losing a day's work. If you do not, you need point-in-time restore — which means space for the write log and a restore procedure you have rehearsed. A backup you have never restored is a hypothesis, not a backup.
When deployment starts to be noticeable to users — that is, when building the image on the same machine slows the database's responses. If you build off production, that moment moves much further out and often never arrives.
Prices come from vendor price lists, read on 5 October 2026, excluding VAT: Vercel (20 USD per seat per month, 1 TB of traffic included, 0.15–0.35 USD per GB above that depending on the region, per the regional pricing page), Hetzner Cloud (CPX32: 4 vCPU, 8 GB RAM, 160 GB NVMe, 35.99 EUR a month including an IPv4 address, 20 TB of traffic in European regions; the CPX31 we priced earlier is now offered only in US locations) and MongoDB Atlas (M10 from 56.94 USD a month; a production database is a three-node replica set). The failures described here come from our own deployments, not from the literature.
The arithmetic in this article is an example, not a quote — it changes with traffic, headcount and how many maintenance hours you actually have. If you are wondering which side of that line you are on, we will go through it together: what you really pay today, what you take on yourself, and how long the difference takes to pay back.
Table of Contents · 6 sections · 11 minutes read
Rate this article
Back to the guide: Websites — a guide to the whole section

We found no independent Polish benchmark for SEO cost. How to turn a fee and its hours into an hourly rate, and what to ask before signing.

SMS marketing for online stores: GDPR and ePrivacy consent, what a campaign costs in PLN, and the Gmail, Yahoo and Outlook rules for email.

Ecommerce fulfillment: what the service covers, how providers in Poland price it, and when outsourcing your warehouse pays off instead of doing it in-house.

On premise, your own server: the full cost beyond hardware, the end of Windows Server 2016 support, and when the cloud or VPS wins instead.

SLA meaning: how much downtime fits in 99.9%, how AWS, Microsoft and Google SLAs compare, SLO, RPO, RTO and 10 things to check before signing.

Cloud computing by the NIST definition: five traits, IaaS, PaaS and SaaS, public, private and hybrid cloud, and how Polish businesses actually use the cloud.

What an ERP system is, how many Polish firms use one, when a small business needs it, what it costs beyond the price list and where it goes wrong.

A 502 Bad Gateway, 500, 503 or 504 error tells you which part failed: the application, the link between servers or an overload. And who to call.

A 404 Not Found on your own site is usually a page removed without a redirect. What 4xx status codes mean, what Google does and why our 404 returns 200.