[{"data":1,"prerenderedAt":1294},["ShallowReactive",2],{"blog-en-\u002Fen\u002Fblog\u002Fself-hosted-vercel-alternative":3,"blog-en-surround-\u002Fen\u002Fblog\u002Fself-hosted-vercel-alternative":1281},{"id":4,"title":5,"author":6,"body":7,"category":1263,"cover":1264,"date":1265,"description":1266,"draft":1267,"extension":1268,"lastReviewed":1264,"meta":1269,"navigation":766,"path":1270,"readingTime":1271,"seo":1272,"sitemap":1273,"stem":1274,"tags":1275,"__hash__":1280},"blog_en\u002Fen\u002Fblog\u002Fself-hosted-vercel-alternative.md","A self-hosted alternative to Vercel: hosting Next.js without lock-in","HeroCtl team",{"type":8,"value":9,"toc":1236},"minimark",[10,14,17,22,25,60,63,66,70,75,78,81,84,87,91,94,97,109,120,139,150,154,157,160,184,187,191,194,198,201,207,213,219,222,226,229,232,242,245,248,252,255,258,265,268,271,275,278,518,521,525,528,534,540,546,552,555,559,562,566,607,610,614,630,694,701,836,839,843,846,886,889,893,896,933,937,940,947,950,954,957,988,991,995,998,1019,1022,1026,1029,1034,1054,1059,1075,1081,1084,1088,1094,1103,1117,1127,1153,1168,1174,1178,1181,1184,1187,1212,1215,1229,1232],[11,12,13],"p",{},"Vercel is the best DX product out there for Next.js. Automatic build on every push, preview deploys with a unique URL per commit, edge runtime running close to the user, ISR working with one line of config, analytics and Web Vitals integrated without installing a thing. For someone starting a Next.js application solo, it is objectively the best technical choice.",[11,15,16],{},"The bill starts to hurt at three predictable points. This post maps those points with real numbers, defends Vercel where it gets things right, and shows three exit routes — each with its trade-off. At the end, a step-by-step technical migration and a concrete calculation of how much a Brazilian team saves by leaving.",[18,19,21],"h2",{"id":20},"where-vercel-gets-it-right","Where Vercel gets it right",[11,23,24],{},"Worth starting with the hard-to-admit part. Vercel solves real problems that other providers don't solve with the same elegance:",[26,27,28,36,42,48,54],"ul",{},[29,30,31,35],"li",{},[32,33,34],"strong",{},"Zero-config for Next.js."," The Vercel team maintains the framework. Each new release has tested support on the provider before it goes GA. You don't need to configure adapter, runtime, build cache, anything.",[29,37,38,41],{},[32,39,40],{},"Preview deploys per commit."," Each pull request opens an isolated public URL with that commit's build. Designer reviews, PM approves, QA tests — without bringing up shared staging.",[29,43,44,47],{},[32,45,46],{},"Global edge functions."," Code runs in over 30 regions simultaneously. For a user in São Paulo, cold start latency is lower than many dedicated servers in GRU.",[29,49,50,53],{},[32,51,52],{},"ISR and SSG out-of-the-box."," A static page with scheduled revalidation works without configuring an external CDN, without manual invalidation.",[29,55,56,59],{},[32,57,58],{},"Native analytics and Web Vitals."," No third-party script to install, no extra weight on the bundle, real Core Web Vitals metrics in production.",[11,61,62],{},"For a Brazilian solo dev with a SaaS at US$5k MRR, with a small app and controlled traffic, Vercel costs US$20\u002Fmonth and frees the developer to write product. It is the right choice. There's no irony in that sentence.",[11,64,65],{},"The problem is what happens after the product grows.",[18,67,69],{"id":68},"the-three-points-where-it-hurts","The three points where it hurts",[71,72,74],"h3",{"id":73},"point-1-cost-scaled-in-usd","Point 1 — Cost scaled in USD",[11,76,77],{},"The Pro plan costs US$20 per developer per month as a floor. In real, with the exchange rate around R$5, that becomes roughly R$100 per dev per month. A team of five starts at R$500\u002Fmonth in licenses alone, before any traffic or compute.",[11,79,80],{},"From there, cost is by usage. Serverless functions charge per GB-second of execution plus invocation per request. Egress traffic charges per GB. Vercel KV, Vercel Postgres, Vercel Blob — each managed service has its pricing table, all in USD.",[11,82,83],{},"The operational consequence is an unpredictable bill. Seasonal traffic — Black Friday, feature launch, press mention — multiplies that month's cost tenfold. In USD, with the exchange rate varying 5% in a bad month, you find out you budgeted at R$5.00 and closed at R$5.30. The math is clear: 10x traffic and 6% additional FX becomes a bill 10.6x larger in real.",[11,85,86],{},"For a Brazilian startup with revenue in real and cost in dollars, the margin spread vanishes first. For an agency that bills clients in real and pays infra in dollars, margin vanishes second.",[71,88,90],{"id":89},"point-2-lock-in-of-primitives","Point 2 — Lock-in of primitives",[11,92,93],{},"This is the point nobody sees until they need to leave.",[11,95,96],{},"ISR (Incremental Static Regeneration) is a Next.js feature, but Vercel's optimized implementation uses a proprietary CDN with global tag invalidation. Self-hosted, ISR works locally — each node has its own cache copy on disk. To invalidate a tag across three nodes you need explicit orchestration.",[11,98,99,100,104,105,108],{},"Edge runtime uses Cloudflare Workers-style primitives — global ",[101,102,103],"code",{},"fetch",", no ",[101,106,107],{},"fs"," access, no native Node modules. Code written for the edge doesn't run directly on traditional Node without refactoring.",[11,110,111,112,115,116,119],{},"Image Optimization runs on Vercel's infra. You ship ",[101,113,114],{},"\u003CImage src=\"\u002Ffoo.jpg\" \u002F>"," and the provider delivers a resized WebP, with global cache. Self-hosted, you need to run ",[101,117,118],{},"sharp"," at build time, or use a dedicated image proxy, or disable the feature.",[11,121,122,123,126,127,130,131,134,135,138],{},"Vercel KV, Postgres and Blob are wrappers around Upstash Redis, Neon Postgres and S3-compatible storage with their own SDK. Migrating from ",[101,124,125],{},"@vercel\u002Fkv"," to ",[101,128,129],{},"ioredis"," directly is an afternoon of refactoring. Migrating from ",[101,132,133],{},"@vercel\u002Fpostgres"," to a standard client is another afternoon. Migrating from ",[101,136,137],{},"@vercel\u002Fblob"," to S3 means revisiting every upload in the app.",[11,140,141,142,145,146,149],{},"None of these barriers is insurmountable. But leaving Vercel isn't ",[101,143,144],{},"git remote set-url"," followed by ",[101,147,148],{},"vercel logout",". It's a refactoring sprint for a medium-sized app.",[71,151,153],{"id":152},"point-3-bandwidth-and-functions-out-of-control","Point 3 — Bandwidth and functions out of control",[11,155,156],{},"Vercel has no hard cap by default on the Pro plan. You define budget alerts, but the application keeps serving until the limit is reached — and the limit is configurable upward, not downward.",[11,158,159],{},"The three bad scenarios are predictable:",[26,161,162,168,174],{},[29,163,164,167],{},[32,165,166],{},"Light DDoS."," A thousand requests per second for an hour hit a volume of bandwidth and invocation that routinely exceeds US$200 on a normal plan. Vercel has protection against massive attacks, but the threshold to trigger the defense is high.",[29,169,170,173],{},[32,171,172],{},"Viral post."," Your page hits Hacker News or the Reddit front, and five hundred thousand people access it in a day. The cost falls on you, not on the advertiser.",[29,175,176,179,180,183],{},[32,177,178],{},"Bug in a loop."," A function in production with a ",[101,181,182],{},"setInterval"," that forgot to clear, or a route that calls itself recursively in SSR — discovered on the bill.",[11,185,186],{},"You discover the damage when the credit card statement arrives. The appeals path exists, but it is case-by-case and depends on provider goodwill. It is not a contractual cap guarantee.",[18,188,190],{"id":189},"the-three-exit-routes","The three exit routes",[11,192,193],{},"Leaving Vercel doesn't mean jumping to Kubernetes. There's a spectrum, and each step trades one thing for another.",[71,195,197],{"id":196},"route-a-hosted-more-predictable","Route A — Hosted, more predictable",[11,199,200],{},"Render, Railway and Fly.io occupy this band. You still pay per instance in USD, still trust the provider for availability, still have a web panel and integrated CI\u002FCD. The difference is the billing model.",[11,202,203,206],{},[32,204,205],{},"Render."," Fixed price per instance per month. Basic web service US$7\u002Fmonth, larger instance US$25-85\u002Fmonth. Decent latency to São Paulo via Render-Cloud in the eastern US region. Has a limited free tier for personal projects. Direct support for Next.js standalone, without a custom adapter.",[11,208,209,212],{},[32,210,211],{},"Railway."," Usage-based model (CPU + memory + bandwidth) with configurable cap. Predictable pricing because you set the ceiling. Good for an MVP running cheap, scales up when needed. The console UX is excellent.",[11,214,215,218],{},[32,216,217],{},"Fly.io."," Multi-region edge without separate per-function billing. You ship the application and it runs in N regions at the same price. For an app that needs global presence but doesn't want to pay a function table, it is the most obvious choice.",[11,220,221],{},"Trade-off of Route A: you still pay in USD, still depend on a single provider for availability, still have to accept their pricing table when it changes. But you left the serverless-per-invocation model and gained cost predictability. For many teams, that's enough.",[71,223,225],{"id":224},"route-b-simple-self-hosted","Route B — Simple self-hosted",[11,227,228],{},"Modern orchestration panels became a category in the past two years. Coolify, Dokploy, Kamal — each with its philosophy, all sharing the same model: install the panel on a single server, connect a repo, deploy the application.",[11,230,231],{},"The numbers shift regime in this route. A cloud server on Hetzner costs around €5\u002Fmonth — close to R$30. That single server comfortably hosts five medium-sized Next.js applications, plus a small Postgres database, plus a Redis. The DX drops to \"install panel, connect repo, choose domain, deploy.\" There's no global build cache, no automatic preview deploy on every commit (depends on extra configuration), no global edge.",[11,233,234,235,126,238,241],{},"The technical detail that enables the route is Next.js standalone build. Adding ",[101,236,237],{},"output: 'standalone'",[101,239,240],{},"next.config.js",", the build generates a compact Node server with all required dependencies copied in. The resulting Docker image lands around 150 MB. Each instance of the application consumes roughly 100 MB of RAM at idle, scaling with traffic. Five Next.js apps on a 4 GB server have memory to spare.",[11,243,244],{},"Trade-off of Route B: you lose global edge. A user in Tokyo accessing your app in SP will feel latency. You lose automatic preview deploy per commit (need to configure manually, or use a panel that supports it). You gain total cost predictability: the server bill is the same every month, regardless of traffic.",[11,246,247],{},"For a Brazilian team with Brazilian clients, the loss of global edge is, in practice, irrelevant. Latency São Paulo → São Paulo is lower than latency São Paulo → Vercel-edge-São Paulo, in most measurements.",[71,249,251],{"id":250},"route-c-self-hosted-with-high-availability","Route C — Self-hosted with high availability",[11,253,254],{},"This is where HeroCtl lives. The difference from Route B is the kind of guarantee you can give the client.",[11,256,257],{},"Simple self-hosted panels are, by construction, single-server. When that server goes down, the client goes down with it. For a personal app or MVP, that's acceptable. For a B2B contract with a written SLA, it isn't.",[11,259,260,261,264],{},"Route C removes that single point of failure by placing 3 or 4 servers in the same cluster, with the control plane replicated across them. If one server dies, the others keep serving — and the cluster automatically reschedules containers from the dead node onto other healthy nodes. New coordinator election takes about 7 seconds after a ",[101,262,263],{},"kill -9"," on the leading server.",[11,266,267],{},"The integrated router issues Let's Encrypt certificates automatically, performs rolling deploys without a maintenance window, and runs health checks on every container. You don't assemble five products to get ingress + TLS + metrics + logs — it all comes in the same binary.",[11,269,270],{},"The application range is specific: when a startup needs a written SLA from a client (usually above US$10k MRR or in a serious B2B contract), Route B starts to get risky. A single server, even a reliable one, is a hard narrative to defend when the client asks \"and what if that server falls?\". Route C solves that without becoming Kubernetes.",[18,272,274],{"id":273},"side-by-side","Side by side",[11,276,277],{},"The table is the honest version of the decision. There's no column without caveats.",[279,280,281,306],"table",{},[282,283,284],"thead",{},[285,286,287,291,294,297,300,303],"tr",{},[288,289,290],"th",{},"Criterion",[288,292,293],{},"Vercel",[288,295,296],{},"Render",[288,298,299],{},"Railway",[288,301,302],{},"Coolify",[288,304,305],{},"HeroCtl",[307,308,309,330,348,364,381,398,416,431,448,468,483,500],"tbody",{},[285,310,311,315,318,321,324,327],{},[312,313,314],"td",{},"Minimum BRL\u002Fmonth cost",[312,316,317],{},"~R$100\u002Fdev",[312,319,320],{},"~R$35",[312,322,323],{},"~R$25",[312,325,326],{},"~R$30 (1 VPS)",[312,328,329],{},"~R$120 (3-4 VPS)",[285,331,332,335,338,341,344,346],{},[312,333,334],{},"Predictable cost",[312,336,337],{},"No",[312,339,340],{},"Yes",[312,342,343],{},"Yes (with cap)",[312,345,340],{},[312,347,340],{},[285,349,350,353,356,358,360,362],{},[312,351,352],{},"Global edge",[312,354,355],{},"Yes (30+ regions)",[312,357,337],{},[312,359,337],{},[312,361,337],{},[312,363,337],{},[285,365,366,369,372,375,377,379],{},[312,367,368],{},"Next.js ISR",[312,370,371],{},"Native, optimized",[312,373,374],{},"Works locally",[312,376,374],{},[312,378,374],{},[312,380,374],{},[285,382,383,386,389,392,394,396],{},[312,384,385],{},"Image Optimization",[312,387,388],{},"Hosted",[312,390,391],{},"Build\u002Fproxy",[312,393,391],{},[312,395,391],{},[312,397,391],{},[285,399,400,403,406,409,411,414],{},[312,401,402],{},"Preview deploys",[312,404,405],{},"Automatic per commit",[312,407,408],{},"Manual\u002Fbranch",[312,410,408],{},[312,412,413],{},"Manual",[312,415,413],{},[285,417,418,421,423,425,427,429],{},[312,419,420],{},"Automatic TLS",[312,422,340],{},[312,424,340],{},[312,426,340],{},[312,428,340],{},[312,430,340],{},[285,432,433,436,438,441,443,445],{},[312,434,435],{},"Multi-region",[312,437,340],{},[312,439,440],{},"Limited",[312,442,440],{},[312,444,337],{},[312,446,447],{},"Configurable",[285,449,450,453,456,459,462,465],{},[312,451,452],{},"Contractual SLA",[312,454,455],{},"99.99% (Enterprise)",[312,457,458],{},"99.95%",[312,460,461],{},"99.9%",[312,463,464],{},"Best-effort",[312,466,467],{},"Configurable (you operate)",[285,469,470,473,475,477,479,481],{},[312,471,472],{},"Real high availability",[312,474,340],{},[312,476,340],{},[312,478,340],{},[312,480,337],{},[312,482,340],{},[285,484,485,488,490,492,494,497],{},[312,486,487],{},"Support in PT",[312,489,337],{},[312,491,337],{},[312,493,337],{},[312,495,496],{},"Community",[312,498,499],{},"Yes (Business)",[285,501,502,505,508,511,513,516],{},[312,503,504],{},"Lock-in of primitives",[312,506,507],{},"High (KV\u002FPostgres\u002FBlob\u002FEdge)",[312,509,510],{},"Low",[312,512,510],{},[312,514,515],{},"None",[312,517,515],{},[11,519,520],{},"The column that matters changes by stage. Solo dev looks at the first row. A growing team looks at \"predictable cost\". A startup with a B2B client looks at \"contractual SLA\" and \"real high availability\".",[18,522,524],{"id":523},"when-to-stay-on-vercel","When to stay on Vercel",[11,526,527],{},"Honesty is the defense mechanism of any comparison. Four scenarios where leaving is a loss:",[11,529,530,533],{},[32,531,532],{},"Solo dev running a small SaaS in USD with healthy revenue."," If the app already bills in dollars and revenue passes US$30k MRR, US$100-300\u002Fmonth of Vercel is accounting noise. The time spent migrating is worth more than the savings.",[11,535,536,539],{},[32,537,538],{},"Low-complexity Next.js marketing site."," Static page with a contact form. Vercel does it for free on the Hobby plan, and the free tier has no hard limit for that traffic profile. Switching to self-hosted is moving a problem instead of solving it.",[11,541,542,545],{},[32,543,544],{},"Small team without anyone to look after infra, with revenue justifying it."," Vercel is, ultimately, outsourced operations. If your margin supports the price, and your only senior dev needs to be writing product, keeping Vercel is a time-allocation decision, not a technology one.",[11,547,548,551],{},[32,549,550],{},"Global edge critical for UX."," Application with users on three continents where sub-50ms latency globally is part of the product. Self-hosted with global presence is expensive and operationally complicated. Vercel solves it.",[11,553,554],{},"If you are in any of these four profiles, close this tab and go back to code. The rest of the post isn't for you yet.",[18,556,558],{"id":557},"technical-migration-from-vercel-to-self-hosted","Technical migration from Vercel to self-hosted",[11,560,561],{},"For those who decided to leave, the path has seven steps. Each takes between an afternoon and two days, depending on the size of the app.",[71,563,565],{"id":564},"_1-inventory","1. Inventory",[11,567,568,569,572,573,576,577,579,580,579,582,579,584,579,587,590,591,594,595,598,599,602,603,606],{},"Before moving anything, map what's in use. List of environment variables in the Vercel project — copy everything into a versioned ",[101,570,571],{},".env.example"," file. List of Vercel-only dependencies that appear in ",[101,574,575],{},"package.json",": ",[101,578,125],{},", ",[101,581,133],{},[101,583,137],{},[101,585,586],{},"@vercel\u002Fanalytics",[101,588,589],{},"@vercel\u002Fspeed-insights",". List of Next.js features that depend on a specific runtime: ISR (search for ",[101,592,593],{},"revalidate"," in the code), middleware (does ",[101,596,597],{},"middleware.ts"," exist at root?), edge runtime (",[101,600,601],{},"export const runtime = 'edge'","), Image Optimization (",[101,604,605],{},"\u003CImage \u002F>"," on how many routes?).",[11,608,609],{},"The inventory changes nothing. But it decides the order of the next steps.",[71,611,613],{"id":612},"_2-standalone-build","2. Standalone build",[11,615,616,617,126,619,621,622,625,626,629],{},"Add ",[101,618,237],{},[101,620,240],{},". This mode makes the build copy to ",[101,623,624],{},".next\u002Fstandalone\u002F"," only the production dependencies actually used, plus a minimal Node server (",[101,627,628],{},"server.js",").",[631,632,637],"pre",{"className":633,"code":634,"language":635,"meta":636,"style":636},"language-js shiki shiki-themes github-dark-default","\u002F\u002F next.config.js\nmodule.exports = {\n  output: 'standalone',\n  \u002F\u002F demais opções\n}\n","js","",[101,638,639,648,669,682,688],{"__ignoreMap":636},[640,641,644],"span",{"class":642,"line":643},"line",1,[640,645,647],{"class":646},"sH3jZ","\u002F\u002F next.config.js\n",[640,649,651,655,659,662,666],{"class":642,"line":650},2,[640,652,654],{"class":653},"sFSAA","module",[640,656,658],{"class":657},"sZEs4",".",[640,660,661],{"class":653},"exports",[640,663,665],{"class":664},"suJrU"," =",[640,667,668],{"class":657}," {\n",[640,670,672,675,679],{"class":642,"line":671},3,[640,673,674],{"class":657},"  output: ",[640,676,678],{"class":677},"s9uIt","'standalone'",[640,680,681],{"class":657},",\n",[640,683,685],{"class":642,"line":684},4,[640,686,687],{"class":646},"  \u002F\u002F demais opções\n",[640,689,691],{"class":642,"line":690},5,[640,692,693],{"class":657},"}\n",[11,695,696,697,700],{},"Local build with ",[101,698,699],{},"next build"," produces a folder of about 150 MB. Dockerfile is short:",[631,702,706],{"className":703,"code":704,"language":705,"meta":636,"style":636},"language-dockerfile shiki shiki-themes github-dark-default","FROM node:20-alpine AS builder\nWORKDIR \u002Fapp\nCOPY package.json pnpm-lock.yaml .\u002F\nRUN corepack enable && pnpm install --frozen-lockfile\nCOPY . .\nRUN pnpm build\n\nFROM node:20-alpine\nWORKDIR \u002Fapp\nCOPY --from=builder \u002Fapp\u002F.next\u002Fstandalone .\u002F\nCOPY --from=builder \u002Fapp\u002F.next\u002Fstatic .\u002F.next\u002Fstatic\nCOPY --from=builder \u002Fapp\u002Fpublic .\u002Fpublic\nEXPOSE 3000\nCMD [\"node\", \"server.js\"]\n","dockerfile",[101,707,708,722,730,738,746,753,761,768,776,783,791,799,807,816],{"__ignoreMap":636},[640,709,710,713,716,719],{"class":642,"line":643},[640,711,712],{"class":664},"FROM",[640,714,715],{"class":657}," node:20-alpine ",[640,717,718],{"class":664},"AS",[640,720,721],{"class":657}," builder\n",[640,723,724,727],{"class":642,"line":650},[640,725,726],{"class":664},"WORKDIR",[640,728,729],{"class":657}," \u002Fapp\n",[640,731,732,735],{"class":642,"line":671},[640,733,734],{"class":664},"COPY",[640,736,737],{"class":657}," package.json pnpm-lock.yaml .\u002F\n",[640,739,740,743],{"class":642,"line":684},[640,741,742],{"class":664},"RUN",[640,744,745],{"class":657}," corepack enable && pnpm install --frozen-lockfile\n",[640,747,748,750],{"class":642,"line":690},[640,749,734],{"class":664},[640,751,752],{"class":657}," . .\n",[640,754,756,758],{"class":642,"line":755},6,[640,757,742],{"class":664},[640,759,760],{"class":657}," pnpm build\n",[640,762,764],{"class":642,"line":763},7,[640,765,767],{"emptyLinePlaceholder":766},true,"\n",[640,769,771,773],{"class":642,"line":770},8,[640,772,712],{"class":664},[640,774,775],{"class":657}," node:20-alpine\n",[640,777,779,781],{"class":642,"line":778},9,[640,780,726],{"class":664},[640,782,729],{"class":657},[640,784,786,788],{"class":642,"line":785},10,[640,787,734],{"class":664},[640,789,790],{"class":657}," --from=builder \u002Fapp\u002F.next\u002Fstandalone .\u002F\n",[640,792,794,796],{"class":642,"line":793},11,[640,795,734],{"class":664},[640,797,798],{"class":657}," --from=builder \u002Fapp\u002F.next\u002Fstatic .\u002F.next\u002Fstatic\n",[640,800,802,804],{"class":642,"line":801},12,[640,803,734],{"class":664},[640,805,806],{"class":657}," --from=builder \u002Fapp\u002Fpublic .\u002Fpublic\n",[640,808,810,813],{"class":642,"line":809},13,[640,811,812],{"class":664},"EXPOSE",[640,814,815],{"class":657}," 3000\n",[640,817,819,822,825,828,830,833],{"class":642,"line":818},14,[640,820,821],{"class":664},"CMD",[640,823,824],{"class":657}," [",[640,826,827],{"class":677},"\"node\"",[640,829,579],{"class":657},[640,831,832],{"class":677},"\"server.js\"",[640,834,835],{"class":657},"]\n",[11,837,838],{},"Final image around 180 MB. Runs the same in any environment that supports containers.",[71,840,842],{"id":841},"_3-storage-substitution","3. Storage substitution",[11,844,845],{},"Each Vercel managed service has a direct alternative:",[26,847,848,859,874],{},[29,849,850,853,854,126,856,858],{},[32,851,852],{},"Vercel KV → Redis."," You bring up a Redis in the cluster (HeroCtl runs it as a regular job) or use hosted Upstash. Client switches from ",[101,855,125],{},[101,857,129],{},". The API is similar; the adapter can be hidden behind a function.",[29,860,861,864,865,126,867,870,871,658],{},[32,862,863],{},"Vercel Postgres → Postgres."," Postgres in the cluster (regular job) or hosted Supabase\u002FNeon. Migration scripts stay the same. Client switches from ",[101,866,133],{},[101,868,869],{},"pg"," or ",[101,872,873],{},"postgres.js",[29,875,876,879,880,126,882,885],{},[32,877,878],{},"Vercel Blob → S3-compatible."," Cloudflare R2 (no egress charge), Backblaze B2, or MinIO in the cluster itself. Client switches from ",[101,881,137],{},[101,883,884],{},"@aws-sdk\u002Fclient-s3"," pointing to the custom endpoint.",[11,887,888],{},"General rule: do the substitution on a separate branch, with integration tests running against the new service, before touching production.",[71,890,892],{"id":891},"_4-image-optimization","4. Image Optimization",[11,894,895],{},"Three paths, pick one:",[26,897,898,909,917],{},[29,899,900,905,906,908],{},[32,901,902,904],{},[101,903,118],{}," directly on the server."," Next.js detects ",[101,907,118],{}," installed and uses it for local Image Optimization. Works, but consumes CPU from the same process serving the application.",[29,910,911,916],{},[32,912,913,658],{},[101,914,915],{},"next-image-export-optimizer"," Pre-optimizes all images at build time. Good for a blog or site with static images. Unfeasible for an app with user upload.",[29,918,919,922,923,870,926,929,930,932],{},[32,920,921],{},"Dedicated image proxy."," ",[101,924,925],{},"imgproxy",[101,927,928],{},"imageflow"," running as a separate service. The ",[101,931,605],{}," URL points to that proxy. Solves any use case, costs one extra job in the cluster.",[71,934,936],{"id":935},"_5-isr","5. ISR",[11,938,939],{},"Self-hosted, ISR works — Next.js standalone implements the local cache on disk. The fragile point is multi-region invalidation.",[11,941,942,943,946],{},"A 3-node cluster means 3 disk cache copies, each with its own expiration. For a blog or site whose content changes a few times a day, that's acceptable: a few-second inconsistency between nodes is invisible to the user. For an e-commerce dashboard with prices updating every minute, you need coordinated invalidation — usually via a webhook calling ",[101,944,945],{},"revalidatePath"," on all nodes simultaneously.",[11,948,949],{},"Most cases fall in the first profile. It doesn't become the problem it seems at first glance.",[71,951,953],{"id":952},"_6-cicd","6. CI\u002FCD",[11,955,956],{},"Replace Vercel auto-deploy with your own pipeline:",[26,958,959,972,978],{},[29,960,961,964,965,579,968,971],{},[32,962,963],{},"Build:"," GitHub Actions (or GitLab CI, or Jenkins) runs on each push. ",[101,966,967],{},"pnpm install",[101,969,970],{},"pnpm build",", generates Docker image.",[29,973,974,977],{},[32,975,976],{},"Push:"," image registry (ECR, Docker Hub, GHCR). Tag by commit SHA or date.",[29,979,980,983,984,987],{},[32,981,982],{},"Deploy:"," API call against the orchestrator (",[101,985,986],{},"heroctl deploy job.json"," or equivalent). Rolling update without downtime.",[11,989,990],{},"Pipeline time for a medium app sits around 4-6 minutes. Vercel does it in 2-3 minutes. The difference is real, but not catastrophic.",[71,992,994],{"id":993},"_7-cutover","7. Cutover",[11,996,997],{},"Last step, and the most delicate:",[26,999,1000,1007,1010,1013,1016],{},[29,1001,1002,1003,1006],{},"Bring up the self-hosted version pointing to a temporary domain (",[101,1004,1005],{},"new.yourapp.com",", for example).",[29,1008,1009],{},"Run in parallel for 7 days. Internal users test. Canary traffic directed by flag.",[29,1011,1012],{},"Compare metrics: error rate, p95 latency, projected infra cost.",[29,1014,1015],{},"If parity is OK, switch the main DNS to point to the new backend. Low TTL (60s) helps with quick rollback.",[29,1017,1018],{},"Keep Vercel on for another 7 days. Only deactivate the project after confirming nobody is on the old DNS.",[11,1020,1021],{},"Total migration for a medium app takes 2-3 weeks with a dedicated dev. For a small app, one week. For a giant Next.js monolith with 50 routes and complex middleware, a quarter.",[18,1023,1025],{"id":1024},"concrete-calculation-for-a-brazilian-team","Concrete calculation for a Brazilian team",[11,1027,1028],{},"Number to close the argument. Five Brazilian devs with a medium-sized Next.js app (50 routes, Postgres database, image storage, traffic of 2 million requests\u002Fmonth).",[11,1030,1031],{},[32,1032,1033],{},"Vercel scenario:",[26,1035,1036,1039,1042,1045,1048],{},[29,1037,1038],{},"5 × Pro (US$20\u002Fdev\u002Fmonth) = US$100\u002Fmonth",[29,1040,1041],{},"Bandwidth and function invocations (estimate with given traffic): US$50-200\u002Fmonth",[29,1043,1044],{},"Vercel Postgres (small production instance): US$30\u002Fmonth",[29,1046,1047],{},"Vercel Blob (50 GB stored, 100 GB transfer): US$20\u002Fmonth",[29,1049,1050,1053],{},[32,1051,1052],{},"Total: US$200-400\u002Fmonth = R$1,000 to R$2,000\u002Fmonth"," at the current FX.",[11,1055,1056],{},[32,1057,1058],{},"HeroCtl Community scenario on 4 Hetzner servers:",[26,1060,1061,1064,1067,1070],{},[29,1062,1063],{},"4 × CX22 (€5.18\u002Fmonth each) = €21\u002Fmonth",[29,1065,1066],{},"Cloudflare R2 (50 GB stored, no egress charge): ~€5\u002Fmonth",[29,1068,1069],{},"Postgres running as a job in the cluster itself: zero additional",[29,1071,1072,1053],{},[32,1073,1074],{},"Total: €25-30\u002Fmonth = R$150-180\u002Fmonth",[11,1076,1077,1080],{},[32,1078,1079],{},"Difference: R$850 to R$1,850\u002Fmonth."," Over 12 months, R$10,000 to R$22,000 in savings. Equivalent to one month of mid-level developer salary in the intermediate band of the Brazilian market.",[11,1082,1083],{},"The savings pay for a migration done in four weeks in the first year, and remain available as operational margin in the years that follow. Over 36 months, R$30k-66k of difference. It is a budget line that deserves to show up in the finance meeting.",[18,1085,1087],{"id":1086},"questions-we-get","Questions we get",[11,1089,1090,1093],{},[32,1091,1092],{},"Does HeroCtl run Next.js directly?","\nYes. Standalone build generates a Docker image, and HeroCtl orchestrates any image. There's no custom adapter, no specific template — the Dockerfile shown above works without modification.",[11,1095,1096,1099,1100,1102],{},[32,1097,1098],{},"And ISR without global CDN?","\nWorks locally on each node of the cluster. A 3-node cluster means 3 independent caches with their own expiration. For coordinated multi-node invalidation, you use ",[101,1101,945],{}," called via webhook on all nodes. For most cases (blog, institutional site, dashboard with revalidation every minute), the transient inconsistency is invisible.",[11,1104,1105,1108,1109,1112,1113,1116],{},[32,1106,1107],{},"How do I do preview deploys?","\nHeroCtl doesn't have a native automatic preview deploy per commit, but it supports multiple versions of the same job running side by side. Common setup: pipeline creates a job with the branch suffix (",[101,1110,1111],{},"my-app-feature-x","), with a temporary domain (",[101,1114,1115],{},"feature-x.preview.yourapp.com","), automatic TLS by the integrated router. When the branch is merged and deleted, the job is demoted. Whoever wants exactly Vercel's DX assembles that in 100-200 lines of pipeline.",[11,1118,1119,1122,1123,1126],{},[32,1120,1121],{},"Do edge functions survive?","\nEdge functions use Cloudflare Workers-style primitives and don't run on traditional Node. Self-hosted, you convert them to normal server-side routes (",[101,1124,1125],{},"export const runtime = 'nodejs'",") or split into their own services. Refactoring is per file, usually between 10 minutes and 2 hours depending on the code.",[11,1128,1129,1132,1134,1135,1138,1139,1141,1142,1144,1145,1148,1149,1152],{},[32,1130,1131],{},"What if I use Vercel Postgres?",[101,1133,133],{}," is a wrapper around Neon Postgres. You switch to ",[101,1136,1137],{},"@neondatabase\u002Fserverless"," (keeping Neon hosted), or ",[101,1140,869],{},"\u002F",[101,1143,873],{}," pointing to a Postgres in the cluster. Schema migrates directly via ",[101,1146,1147],{},"pg_dump"," + ",[101,1150,1151],{},"pg_restore",". For 95% of apps, it is an afternoon of work.",[11,1154,1155,1158,1159,1161,1162,1164,1165,1167],{},[32,1156,1157],{},"Is there a substitute for Vercel Image Optimization?","\nThree options: ",[101,1160,118],{}," directly on the server (works, consumes local CPU), ",[101,1163,915],{}," at build (good for static images), dedicated proxy like ",[101,1166,925],{}," running as a separate service (handles any case). For an app with user upload, the third option is the best choice.",[11,1169,1170,1173],{},[32,1171,1172],{},"How long does migration take for a medium app?","\nA Next.js application with 50 routes, Postgres, storage and middleware: 2-3 weeks with a dedicated dev following this post's step-by-step. Small application (10-15 routes, no managed storage): one week. Giant monolith with complex middleware and strong dependence on edge runtime: a full quarter.",[18,1175,1177],{"id":1176},"closing","Closing",[11,1179,1180],{},"Vercel is a good choice. For many cases, it is the right choice. The point of this post is not \"Vercel is bad\" — it is \"Vercel is not the only choice\". Most Brazilian teams looking at the monthly bill and sighing aren't looking elsewhere because their product is worse. They are looking because the savings in real, at company scale, are large enough to pay for a calm migration with cash to spare.",[11,1182,1183],{},"The choice between the three routes depends on where you are. Render and Railway solve the predictability problem without changing the operational model much. Coolify and Dokploy solve cost radically, in exchange for a single server. HeroCtl solves cost and keeps real high availability, in exchange for operating 3-4 servers.",[11,1185,1186],{},"If you want to test Route C through the shortest path:",[631,1188,1192],{"className":1189,"code":1190,"language":1191,"meta":636,"style":636},"language-bash shiki shiki-themes github-dark-default","curl -sSL get.heroctl.com\u002Finstall.sh | sh\n","bash",[101,1193,1194],{"__ignoreMap":636},[640,1195,1196,1200,1203,1206,1209],{"class":642,"line":643},[640,1197,1199],{"class":1198},"sQhOw","curl",[640,1201,1202],{"class":653}," -sSL",[640,1204,1205],{"class":677}," get.heroctl.com\u002Finstall.sh",[640,1207,1208],{"class":664}," |",[640,1210,1211],{"class":1198}," sh\n",[11,1213,1214],{},"Bring up 3 small servers, install on each, point the domain. Bring up the Next.js application as a job. Verify that the integrated router issued a certificate, that the rolling deploy worked, that killing a server didn't take the site down. Then decide if the savings are worth it.",[11,1216,1217,1218,1223,1224,1228],{},"For more reading: ",[1219,1220,1222],"a",{"href":1221},"\u002Fen\u002Fblog\u002Fwhy-we-built-heroctl","Why we built HeroCtl"," explains the general thesis behind the product, and ",[1219,1225,1227],{"href":1226},"\u002Fen\u002Fblog\u002Fself-hosted-heroku-2026","Self-hosted Heroku in 2026"," covers the adjacent use range — when you want Heroku-like DX running on your infra, without needing HeroCtl's HA level.",[11,1230,1231],{},"The intent, as always, is the same: container orchestration, without ceremony.",[1233,1234,1235],"style",{},"html pre.shiki code .sH3jZ, html code.shiki .sH3jZ{--shiki-default:#8B949E}html pre.shiki code .sFSAA, html code.shiki .sFSAA{--shiki-default:#79C0FF}html pre.shiki code .sZEs4, html code.shiki .sZEs4{--shiki-default:#E6EDF3}html pre.shiki code .suJrU, html code.shiki .suJrU{--shiki-default:#FF7B72}html pre.shiki code .s9uIt, html code.shiki .s9uIt{--shiki-default:#A5D6FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sQhOw, html code.shiki .sQhOw{--shiki-default:#FFA657}",{"title":636,"searchDepth":650,"depth":650,"links":1237},[1238,1239,1244,1249,1250,1251,1260,1261,1262],{"id":20,"depth":650,"text":21},{"id":68,"depth":650,"text":69,"children":1240},[1241,1242,1243],{"id":73,"depth":671,"text":74},{"id":89,"depth":671,"text":90},{"id":152,"depth":671,"text":153},{"id":189,"depth":650,"text":190,"children":1245},[1246,1247,1248],{"id":196,"depth":671,"text":197},{"id":224,"depth":671,"text":225},{"id":250,"depth":671,"text":251},{"id":273,"depth":650,"text":274},{"id":523,"depth":650,"text":524},{"id":557,"depth":650,"text":558,"children":1252},[1253,1254,1255,1256,1257,1258,1259],{"id":564,"depth":671,"text":565},{"id":612,"depth":671,"text":613},{"id":841,"depth":671,"text":842},{"id":891,"depth":671,"text":892},{"id":935,"depth":671,"text":936},{"id":952,"depth":671,"text":953},{"id":993,"depth":671,"text":994},{"id":1024,"depth":650,"text":1025},{"id":1086,"depth":650,"text":1087},{"id":1176,"depth":650,"text":1177},"comparison",null,"2026-02-04","Vercel charges in USD, scales serverless cost per request, and pulls you into its primitives. For Brazilian teams, the bill turns ugly fast. How to run Next.js elsewhere.",false,"md",{},"\u002Fen\u002Fblog\u002Fself-hosted-vercel-alternative","13 min",{"title":5,"description":1266},{"loc":1270},"en\u002Fblog\u002Fself-hosted-vercel-alternative",[1276,1277,1278,1263,1279],"vercel","next-js","self-hosted","lock-in","5OLinVuLSTvNUtES7uRenVP71Dob-Vr9jU8QXwltovY",[1282,1287],{"title":1283,"path":1226,"stem":1284,"description":1285,"date":1286,"category":1263,"children":-1},"Self-hosted Heroku in 2026: the state of the segment","en\u002Fblog\u002Fself-hosted-heroku-2026","Since Salesforce killed the Heroku free plan in November\u002F2022, dozens of self-hosted alternatives emerged. An honest map of the segment and how to choose.","2025-11-19",{"title":1288,"path":1289,"stem":1290,"description":1291,"date":1292,"category":1293,"children":-1},"Sentry self-hosted vs SaaS: how much you really save for a Brazilian startup","\u002Fen\u002Fblog\u002Fsentry-self-hosted-vs-saas-cost-comparison","en\u002Fblog\u002Fsentry-self-hosted-vs-saas-cost-comparison","Sentry SaaS starts at US$26\u002Fmonth, scaling fast with volume. Self-hosted is 'free' — but runs Postgres + Redis + Kafka + ClickHouse. Honest analysis of when self-hosting is worth it.","2026-05-05","engineering",1777362215232]