How to Build Your Own Vercel (Self-Hosted PaaS on a VPS)
You connect a repository, you run git push, and a few seconds later there is a URL. Not a build log you have to babysit, not a server you had to rent first. A URL, already live, already on HTTPS. Push again and there is a second URL, and the first one still works. Break production and a button puts it back in about a second, with no rebuild. It has the texture of a magic trick: you did one ordinary thing and a global machine did the rest.
I run my own VPS, and I have already written about turning it into a small platform. So I came to Vercel with a narrow, slightly jealous question. How much of that machine is genuinely hyperscale, the kind of thing only a company with a global network can run, and how much is a handful of good design decisions I could reproduce on a box I own?
The short answer is that the magic is mechanical. Underneath, a modern PaaS is two planes, one core primitive, one split, and a lot of patient automation around routing and certificates. The parts you truly have to rent turn out to be fewer than they look, and none of them are the parts that make the thing feel magic.
Two planes#
The cleanest way to hold the whole system in your head is to split it in two. There is a control plane that decides what should be served where, and a data plane that actually serves it. Almost every design choice falls out of keeping those two apart.
The control plane is stateful and correctness-first: it is where the databases of record live, the projects and deployments and domains and env vars and certs. The data plane is availability-first: it is the CDN and the compute fleet, and it mostly just reads config the control plane hands it. Certificates are stored in the control-plane database and cached in edge memory so TLS can terminate close to the user. Once you see this line, the rest of the architecture stops looking clever and starts looking inevitable.
Immutability is the whole trick#
Here is the single decision that everything else rests on: every deployment is immutable and preserved. A build runs once, produces a fixed set of artifacts, and that deployment gets a permanent, unique URL. It is never mutated again.
Your production domain, then, is not a deployment. It is an alias, a pointer at one deployment. Promoting to production and rolling back are the same operation: you move the pointer. Nothing rebuilds. This is why a rollback on Vercel takes about a second instead of the length of a build, and why only deployments that were previously live in production are eligible to roll back to.
Once deployments are immutable and production is just an alias, a surprising number of features stop being features and become consequences. Preview URLs for every branch: a deployment already has its own permanent URL, so a preview is just not being aliased to production. Atomic promotion: flip a pointer. Instant rollback: flip it back. Even skew protection, keeping a user who loaded an old version talking to an API compatible with that version, is natural when old deployments never disappear. One design choice, a whole product surface for free.
The build splits your app in two#
So what does a build actually produce? This is where the static-versus-server distinction, the real spine of the whole field, shows up. Vercel formalises the output as the Build Output API, and reading its file layout teaches you more about how a PaaS works than any diagram of theirs.
Each .func directory is a self-contained function with a .vc-config.json that declares its runtime, nodejs or edge. Static assets land in static/ and get served straight from the CDN. The interesting middle case is Incremental Static Regeneration: a "prerender" is a function plus a .prerender-config.json with an expiration and an optional static fallback. It is served from cache like a static file, but a function runs out of band to regenerate it on a timer or on demand (a request carrying an x-prerender-revalidate header). ISR is the bridge between the two piles: static speed, eventually-fresh content.
The history is worth one sentence, because it is the contract you would target if you cloned this. The Build Output API went from static-plus-Node-functions, to mirroring Next.js output and adding edge functions, to a v3 that is deliberately framework-agnostic. It is the clean, stable interface a build can compile any framework down to.
Static is easy, server is the whole problem#
The two piles are not equally hard. Static assets are fingerprinted files on a CDN, so scaling them is almost free: copy them to more edge locations, and the vast majority of traffic becomes cache hits served from the nearest point of presence. You will never sweat the static half.
The server half is the entire engineering problem, because it means running your code, per request, somewhere, fast. The industry's answer for a decade was serverless: spin up an isolated instance per request, scale by adding instances, pay by wall-clock duration. Elastic and simple to reason about, but it bought that elasticity with a specific tax.
Why serverless got cold#
The tax is the cold start. When no warm instance exists, the platform has to build a sandbox, load the runtime and your dependencies, run your top-level initialisation, and only then call your handler. The work you actually wanted done is the last and often smallest part.
For bursty, stateless APIs this is a fine trade. But there are two problems. One is latency: a cold start can add hundreds of milliseconds to a request whose real work is a few. The other is cost, and it is subtler. A classic serverless instance handles one request at a time and bills for the whole wall-clock duration, including the long stretches where your code is doing nothing but waiting on a database or an API. For anything I/O-bound, and modern apps calling databases and LLMs are almost all I/O-bound, you pay full rate to sit idle.
Serverless servers#
Vercel's answer to both problems is Fluid compute, which they describe, accurately, as "serverless servers." It became the default for new projects on April 23, 2025, and by their account now powers over 45 billion weekly requests, with more than 75% of all Vercel Function invocations running on it.
The core is a component they call the Functions router. Instead of exposing each instance to the internet directly, every instance opens a persistent, secure TCP tunnel back to the router, and the router multiplexes requests onto warm instances. Because one instance can now handle many requests concurrently, and because an I/O-bound request leaves the CPU idle while it waits, the instance serves other requests during that wait. This is in-function concurrency, and it is the whole idea.
Two more moves finish the job. Warm reuse plus "scale to one" keep at least one instance alive so most requests never hit a cold start on sustained traffic, and Node's bytecode caching reuses compiled code across invocations. And the billing changes to match: under Active CPU pricing, Vercel charges CPU only while your code is actually running (currently $0.128 per active CPU-hour) and a much cheaper rate for provisioned memory ($0.0106 per GB-hour) during the idle waits. Their worked example: a standard function at full CPU costs about $0.149 per hour under the new model versus $0.31842 under the old one, and they claim savings of up to 85% from concurrency and up to 90% more for idle-heavy workloads like LLM calls. Vendor figures, but the mechanism is real and reproducible.
It is worth laying the compute models side by side, because "serverless" is really four different things:
The edge row is the one exception to "you can build this." A V8 isolate at a point of presence has a near-zero cold start, but it pays for that with a restricted runtime: Web APIs only, a small code-size limit, no native Node APIs. It is superb for middleware and redirects and hopeless for a full app. Fluid is the row that matters for most work, and it is essentially a persistent server that scales to zero. Hold onto that, because it is the row a solo dev can approximate.
A URL and a certificate, for free#
The last piece of the front end is how a hostname finds the right deployment and gets HTTPS without anyone touching a certificate. Every deployment gets a unique hostname, and a single wildcard certificate covers all of them. Custom domains point in with a CNAME (or an A record to an anycast IP for an apex), and the moment a domain is added, a certificate is issued.
The issuance is fully automated over ACME, the protocol behind Let's Encrypt. An HTTP-01 challenge proves control of a normal host; a DNS-01 challenge (a TXT record) proves control of a wildcard. Certificates renew themselves roughly every ninety days. This is not proprietary magic, it is the exact pattern any of us can run, and I will come back to that in a moment.
What of this can you actually build?#
Here is the accounting I came for. Lay the whole machine out, and most of it maps cleanly onto open components you can run on a couple of Hetzner nodes and Cloudflare R2. The parts that do not map are specific, and they are the parts you would rent anyway.
Walk it in the same order as the request. Postgres is the control-plane source of truth, and you enforce the one rule that matters: a deployments row, once READY, is never mutated. A domains row holds host → target_deployment_id, and promotion and rollback are a one-row update of that pointer, exactly Vercel's alias trick, O(1) and instant. The API inserts the row, reserves a deterministic hostname like dep-<sha>.apps.you.dev, enqueues the build, and returns the URL before the build has even started.
Kafka is the durable event backbone (deploy.requested, build.*, deploy.ready, logs.*), so build workers, the scheduler, and the log loaders are decoupled consumers you can replay. Redis is the low-latency layer next to it: the work queue, build heartbeats and locks, caches, and rate limits. Kafka for durable ordered events, Redis for fast ephemeral coordination. They are not redundant.
The build worker is the one place isolation genuinely matters, because you are running code from a repo. Three honest options, from most to least isolation. Firecracker microVMs give VM-grade isolation at container speed: the NSDI '20 paper reports under 5 MB of memory overhead per microVM, boot to application code in under 125 ms, up to 150 microVMs per second per host, and about 50k lines of Rust (it is what AWS Lambda and Fargate run on). Managed Fargate gives you that isolation without operating the VMM, at the cost of AWS lock-in and egress to move artifacts to R2. And unprivileged BuildKit or Kaniko pods build an OCI image with no Docker daemon, right inside your own cluster. For a solo dev the pragmatic default is a Kaniko or BuildKit build pod as a Kubernetes Job with a per-build namespace and tight limits; reach for Firecracker only when you open builds to genuinely untrusted code.
The server half runs on Kubernetes: a Deployment and Service per deployment, a namespace per project, the immutable image tag mapping to an immutable ReplicaSet. Steady apps autoscale on HPA; the interesting one is KEDA, which adds event-driven autoscaling and, crucially, scale-to-zero. KEDA taking a Deployment from zero to one on the first request and back to zero when idle is the closest open analog to Fluid's economics: you stop paying for idle apps without giving up the ability to serve them. Traefik is the edge: it watches Kubernetes and adds a per-host router rule for each new deployment with no restart, and its ACME resolver issues and renews certificates automatically, including a DNS-01 wildcard on *.apps.you.dev that covers every deploy URL at once, Vercel's *.vercel.app trick with off-the-shelf software.
The tail of the system is observability. Kafka feeds ClickHouse, whose columnar storage and vectorized execution are built exactly for high-cardinality request and build logs: sort by (project_id, deployment_id, timestamp), compress with ZSTD, tier cold partitions out to R2, and per-deployment p95 latency and error rates come back in sub-second time where a row store would choke. Meilisearch, fed off the same event stream, gives the dashboard instant typo-tolerant search over projects and deployments. It is not on the request path; it is there so the UI feels fast.
So here is the split, honestly drawn. The green items are what a determined solo dev can actually build; the empty ones are the scale you rent or defer.
- Included: Immutable deployments as append-only Postgres rows
- Included: Alias-based promotion and about-one-second rollback
- Included: Static assets on object storage behind a CDN (R2)
- Included: Isolated build workers (BuildKit or Kaniko pods)
- Included: Server apps as OCI images on Kubernetes
- Included: Per-deployment hostname routing (Traefik)
- Included: Automatic TLS with ACME and a wildcard certificate
- Included: HPA autoscaling for steady traffic
- Included: KEDA scale-to-zero, the Fluid-economics analog
- Included: A durable event pipeline (Kafka to ClickHouse)
- Included: Per-deployment logs and analytics (ClickHouse)
- Included: Typo-tolerant dashboard search (Meilisearch)
- Not included: A global anycast edge network on every continent
- Not included: V8-isolate compute at hundreds of points of presence
- Not included: A Functions router doing 100K RPS at sub-millisecond p99.99
- Not included: A Firecracker microVM build fleet for untrusted code at scale
- Not included: Multi-region failover and geo-routing
The pattern is the same one I found when I looked at anti-bot systems: the expensive, unbeatable-looking parts are the global-scale ones, and they are precisely the parts you should not try to copy. Everything above the line is a design decision, not a data center.
Where it breaks#
None of this is free, and the seams are worth naming.
The self-hosted version is deliberately not hyperscale. It reproduces the techniques, immutability, the static-versus-server split, edge caching, event-driven builds, scale-to-zero, at indie scale. Global anycast, a V8-isolate edge, and a router resolving in sub-millisecond time at 100K requests per second are genuinely different engineering, and pretending otherwise would be the hype the whole piece is trying to avoid.
TLS automation has one classic operational trap: Let's Encrypt has rate limits, so you persist Traefik's acme.json across restarts, test against the staging CA, and prefer a single DNS-01 wildcard over per-hostname issuance. Lose that file and re-request a hundred certs and you will get throttled at the worst moment.
There are limits on the borrowed pieces too. Even Vercel's own container support runs on top of Fluid and inherits function limits: capped size, memory, and duration, scale-to-zero after idle, statelessness, no durable attached storage yet. A "full server" on a PaaS is still not an always-on VM, which is exactly why stateful things (WebSockets, long jobs, anything holding local disk) still want a real container or machine. And one claim I deliberately left out: it is well documented that Lambda and Fargate run on Firecracker, but I could find no primary source that AWS CodeBuild does, so I do not assert it.
Most of the specific numbers here, the 45 billion weekly requests, the 85 to 90 percent cost savings, the sub-millisecond router, come from Vercel's own blog and changelog. They are precise and technically detailed, but they are vendor figures measured on sustained production traffic, not independent benchmarks, and Vercel notes the concurrency gains do not apply the same way to bursty preview or CI traffic. Treat them as directional, not audited.
The part you can actually own#
The thing I keep coming back to is that the magic-trick feeling is, again, the opposite of the truth. A PaaS is not smart in the way it looks smart. It is not doing something no one else can do; it is making a few disciplined choices and then automating around them relentlessly. Immutability turns rollback into a pointer move. The static-versus-server split turns most of your traffic into cache hits. ACME turns HTTPS into a thing you never think about. Fluid turns idle waiting into someone else's request.
That is encouraging if you are building rather than buying. The global brain, the anycast network and the edge isolates and the router at planetary scale, is the part you will never own, and you do not need to. The structural part, immutable deployments behind an alias, a build that sorts static from server, a wildcard cert and a reverse proxy that routes by Host, and scale-to-zero compute, is most of what makes the experience feel like magic, and it fits on a VPS. For most projects, most of the time, that is the machine worth building.
References
- Vercel: Build Output API (v3) reference
- Vercel: Fluid compute documentation
- Vercel: Introducing Fluid compute, the power of servers in serverless form
- Vercel: Fluid compute, how we built serverless servers
- Vercel: serverless servers, efficient Node.js with in-function concurrency
- Vercel: Fluid compute is now the default for new projects (changelog)
- Vercel: Introducing Active CPU pricing for Fluid compute
- Vercel: how requests flow through Vercel (infrastructure)
- Vercel: run any Dockerfile on Vercel
- Agache et al.: Firecracker, Lightweight Virtualization for Serverless Applications (NSDI '20)
- Firecracker: the microVM project repository
- Kaniko: build container images in Kubernetes without a Docker daemon
- BuildKit: efficient, repeatable image builds
- KEDA: Kubernetes event-driven autoscaling with scale-to-zero
- Kubernetes: Horizontal Pod Autoscaler
- Traefik: automatic HTTPS with ACME
- RFC 8555: Automatic Certificate Management Environment (ACME)
- Let's Encrypt: how it works
- ClickHouse: the columnar database for analytics and observability
- Meilisearch: fast, typo-tolerant search
- Cloudflare R2: object storage with no egress fees
- Apache Kafka: the distributed event streaming platform
Related articles
A defender's-eye tour of how modern anti-bot systems actually work in 2026, from passive TLS and HTTP fingerprints to an obfuscated client that probes your browser, server-side ML scoring, and tokens that expire in seconds. Plus an honest split of what a small team can build and what it has to rent.
The request that takes a payment is four lines. Everything that makes it safe to build a business on, so a retry never double-charges and a dropped webhook never loses an order, is the actual work. How I shaped Paybit's crypto payments API on the pattern Stripe proved.