[{"data":1,"prerenderedAt":1828},["ShallowReactive",2],{"site":3,"series-page-auth-is-more-than-login":16,"series-posts-auth-is-more-than-login":29,"series-listing":1824},{"id":4,"calendlyUrl":5,"copyright":6,"email":7,"extension":8,"meta":9,"socials":10,"stem":14,"__hash__":15},"site\u002Fdata\u002Fsite.yml","https:\u002F\u002Fcalendly.com\u002Fherdeybayor\u002Fdiscovery-call","All rights reserved, ©2026","hello@0xsherif.dev","yml",{},{"linkedin":11,"twitter":12,"github":13},"https:\u002F\u002Flinkedin.com\u002Fin\u002F0xsherifdev\u002F","https:\u002F\u002Fx.com\u002F0xsherifdev","https:\u002F\u002Fgithub.com\u002F0xsherifdev","data\u002Fsite","-S03CjOfpfnvoKqIM6EvytqEogbFk4013kamLvbviCQ",{"id":17,"title":18,"description":19,"extension":8,"meta":20,"posts":21,"stem":27,"__hash__":28},"series\u002Fseries\u002Fauth-is-more-than-login.yml","Auth Is More Than Login","A practical series on XSS, CSRF, JWTs, sessions, refresh tokens, and the auth rules people skip.",{},[22,23,24,25,26],"your-auth-is-not-safer-because-you-used-jwts","access-token-in-localstorage-is-not-an-auth-strategy","csrf-did-not-die-when-spas-became-popular","sessions-jwts-and-the-auth-checklist-people-skip","the-refresh-token-endpoint-is-where-auth-gets-real","series\u002Fauth-is-more-than-login","QbuFaKxgaIeDD0Wso9RhxnzuisjDG2yjHOKvtb7D3BA",[30,562,1000,1278],{"id":31,"title":32,"author":33,"body":34,"canonical":545,"category":545,"cover":546,"coverAlt":547,"date":548,"description":549,"extension":550,"meta":551,"navigation":223,"ogImage":545,"path":552,"published":223,"readingTime":300,"seo":553,"series":554,"stem":555,"tags":556,"updated":545,"__hash__":561},"blog\u002Fblog\u002Fyour-auth-is-not-safer-because-you-used-jwts.md","Your Auth Is Not Safer Because You Used JWTs","Sherifdeen Adebayo",{"type":35,"value":36,"toc":537},"minimark",[37,41,44,47,52,55,58,61,166,169,172,176,179,186,383,386,397,411,418,422,425,428,431,434,437,443,447,450,453,456,459,475,478,482,485,488,491,494,498,501,504,521,524,527,530,533],[38,39,40],"p",{},"A lot of teams replace sessions with JWTs and somehow become more confident without becoming more secure.",[38,42,43],{},"The same application still has weak logout, credentials that live for days, no useful revocation story, and authorization checks scattered across controllers. But now the credentials are longer and contain three dots, so the architecture diagram looks serious.",[38,45,46],{},"I like JWTs. I use them. I just do not confuse a token format with an authentication system.",[48,49,51],"h2",{"id":50},"the-token-is-not-the-system","The Token Is Not the System",[38,53,54],{},"A JWT is a compact way to carry claims. In the common signed form, it lets an API verify that the token was signed by the holder of a trusted signing key and was not changed afterwards.",[38,56,57],{},"That is useful. It can also be useful to put a random session ID in a cookie and look it up in Redis. The right choice depends on the system, not on which option sounds more modern.",[38,59,60],{},"Here is the boundary I wish more auth discussions started with:",[62,63,64,81],"table",{},[65,66,67],"thead",{},[68,69,70,75,78],"tr",{},[71,72,74],"th",{"align":73},"left","Concern",[71,76,77],{"align":73},"What a JWT gives you",[71,79,80],{"align":73},"What the system still needs",[82,83,84,96,119,134,145,155],"tbody",{},[68,85,86,90,93],{},[87,88,89],"td",{"align":73},"Integrity",[87,91,92],{"align":73},"A signature over the token",[87,94,95],{"align":73},"Trusted keys, algorithm restrictions, and rotation",[68,97,98,101,116],{},[87,99,100],{"align":73},"Identity context",[87,102,103,104,108,109,112,113],{"align":73},"Claims such as ",[105,106,107],"code",{},"sub",", ",[105,110,111],{},"iss",", and ",[105,114,115],{},"aud",[87,117,118],{"align":73},"Account status and tenant boundaries",[68,120,121,124,131],{},[87,122,123],{"align":73},"Expiry",[87,125,126,127,130],{"align":73},"An ",[105,128,129],{},"exp"," claim the verifier can reject",[87,132,133],{"align":73},"Sensible lifetimes and a renewal strategy",[68,135,136,139,142],{},[87,137,138],{"align":73},"Theft",[87,140,141],{"align":73},"Nothing by default",[87,143,144],{"align":73},"Storage decisions and incident response",[68,146,147,150,152],{},[87,148,149],{"align":73},"Logout",[87,151,141],{"align":73},[87,153,154],{"align":73},"A way to end active access",[68,156,157,160,163],{},[87,158,159],{"align":73},"Authorization",[87,161,162],{"align":73},"Data the policy can consider",[87,164,165],{"align":73},"The actual policy for this resource and action",[38,167,168],{},"A signed JWT is also usually encoded, not encrypted. Anyone who gets one can typically decode its header and payload. Do not put passwords, secrets, or private profile data in there and hope Base64 develops morals overnight.",[38,170,171],{},"JWTs solve a narrow problem well. Trouble starts when we promote that narrow solution into proof that the whole auth design is sound.",[48,173,175],{"id":174},"verification-is-the-useful-part","Verification Is the Useful Part",[38,177,178],{},"If an API accepts JWTs, it has to verify the exact kind of token it expects. Checking that the payload parses is not verification. Checking a signature without constraining the algorithm, issuer, and audience is not enough either.",[38,180,181,182,185],{},"With ",[105,183,184],{},"jose",", a focused verifier can look like this:",[187,188,193],"pre",{"className":189,"code":190,"language":191,"meta":192,"style":192},"language-ts shiki shiki-themes github-dark","import { importSPKI, jwtVerify } from \"jose\";\n\nconst publicKey = await importSPKI(process.env.JWT_PUBLIC_KEY!, \"RS256\");\n\nexport async function verifyAccessToken(token: string) {\n  return jwtVerify(token, publicKey, {\n    algorithms: [\"RS256\"],\n    issuer: \"https:\u002F\u002Fauth.example.com\",\n    audience: \"https:\u002F\u002Fapi.example.com\",\n    requiredClaims: [\"exp\", \"sub\"],\n    clockTolerance: \"5s\",\n  });\n}\n","ts","",[105,194,195,218,225,262,267,298,310,321,333,344,360,371,377],{"__ignoreMap":192},[196,197,200,204,208,211,215],"span",{"class":198,"line":199},"line",1,[196,201,203],{"class":202},"snl16","import",[196,205,207],{"class":206},"s95oV"," { importSPKI, jwtVerify } ",[196,209,210],{"class":202},"from",[196,212,214],{"class":213},"sU2Wk"," \"jose\"",[196,216,217],{"class":206},";\n",[196,219,221],{"class":198,"line":220},2,[196,222,224],{"emptyLinePlaceholder":223},true,"\n",[196,226,228,231,235,238,241,245,248,251,254,256,259],{"class":198,"line":227},3,[196,229,230],{"class":202},"const",[196,232,234],{"class":233},"sDLfK"," publicKey",[196,236,237],{"class":202}," =",[196,239,240],{"class":202}," await",[196,242,244],{"class":243},"svObZ"," importSPKI",[196,246,247],{"class":206},"(process.env.",[196,249,250],{"class":233},"JWT_PUBLIC_KEY",[196,252,253],{"class":202},"!",[196,255,108],{"class":206},[196,257,258],{"class":213},"\"RS256\"",[196,260,261],{"class":206},");\n",[196,263,265],{"class":198,"line":264},4,[196,266,224],{"emptyLinePlaceholder":223},[196,268,270,273,276,279,282,285,289,292,295],{"class":198,"line":269},5,[196,271,272],{"class":202},"export",[196,274,275],{"class":202}," async",[196,277,278],{"class":202}," function",[196,280,281],{"class":243}," verifyAccessToken",[196,283,284],{"class":206},"(",[196,286,288],{"class":287},"s9osk","token",[196,290,291],{"class":202},":",[196,293,294],{"class":233}," string",[196,296,297],{"class":206},") {\n",[196,299,301,304,307],{"class":198,"line":300},6,[196,302,303],{"class":202},"  return",[196,305,306],{"class":243}," jwtVerify",[196,308,309],{"class":206},"(token, publicKey, {\n",[196,311,313,316,318],{"class":198,"line":312},7,[196,314,315],{"class":206},"    algorithms: [",[196,317,258],{"class":213},[196,319,320],{"class":206},"],\n",[196,322,324,327,330],{"class":198,"line":323},8,[196,325,326],{"class":206},"    issuer: ",[196,328,329],{"class":213},"\"https:\u002F\u002Fauth.example.com\"",[196,331,332],{"class":206},",\n",[196,334,336,339,342],{"class":198,"line":335},9,[196,337,338],{"class":206},"    audience: ",[196,340,341],{"class":213},"\"https:\u002F\u002Fapi.example.com\"",[196,343,332],{"class":206},[196,345,347,350,353,355,358],{"class":198,"line":346},10,[196,348,349],{"class":206},"    requiredClaims: [",[196,351,352],{"class":213},"\"exp\"",[196,354,108],{"class":206},[196,356,357],{"class":213},"\"sub\"",[196,359,320],{"class":206},[196,361,363,366,369],{"class":198,"line":362},11,[196,364,365],{"class":206},"    clockTolerance: ",[196,367,368],{"class":213},"\"5s\"",[196,370,332],{"class":206},[196,372,374],{"class":198,"line":373},12,[196,375,376],{"class":206},"  });\n",[196,378,380],{"class":198,"line":379},13,[196,381,382],{"class":206},"}\n",[38,384,385],{},"The example uses a static public key to stay focused. An identity provider will usually publish a JWKS so keys can rotate. Resolve it from a fixed, trusted URL. Do not construct that URL from an unverified token unless you enjoy letting attackers help with configuration.",[38,387,388,389,396],{},"The ",[390,391,395],"a",{"href":392,"rel":393},"https:\u002F\u002Fdatatracker.ietf.org\u002Fdoc\u002Fhtml\u002Frfc8725",[394],"nofollow","JWT Best Current Practices RFC"," goes deeper on algorithm confusion, cross-JWT confusion, issuer validation, audience validation, and other ways a technically valid token can still be wrong for your application.",[38,398,399,400,402,403,406,407,410],{},"Even perfect verification only establishes identity context. It does not prove that ",[105,401,107],{}," may edit invoice ",[105,404,405],{},"123",", cross into tenant ",[105,408,409],{},"acme",", or promote another user. Those are authorization decisions, and the token cannot make them for you.",[38,412,413],{},[414,415],"img",{"alt":416,"src":417},"JWT passing signature, algorithm, issuer, audience, and expiry checks before reaching a separate authorization decision","\u002Fimages\u002Fblog\u002Fjwt-verification-vs-authorization.webp",[48,419,421],{"id":420},"the-format-argument-is-a-distraction","The Format Argument Is a Distraction",[38,423,424],{},"Most session-versus-JWT debates start too late. They compare credential formats before agreeing on the system's actual requirements.",[38,426,427],{},"An opaque session ID normally sends the server to a trusted store for the current session state. A JWT lets a service verify signed claims without performing that lookup on every request. That can matter when several independent services need the same identity context.",[38,429,430],{},"It matters much less when one web application and one backend already share a database or cache.",[38,432,433],{},"For a server-rendered application or a monolith, I often choose an opaque session cookie. It is boring, revocable, and supported by mature frameworks. For distributed APIs, mobile clients, or services that need independently verifiable claims, JWTs can be a strong fit.",[38,435,436],{},"Neither choice answers the uncomfortable questions around it.",[38,438,439],{},[414,440],{"alt":441,"src":442},"Opaque sessions and JWTs using different verification models before converging on shared authorization and revocation responsibilities","\u002Fimages\u002Fblog\u002Fsessions-vs-jwt-verification.webp",[48,444,446],{"id":445},"the-questions-jwts-do-not-answer","The Questions JWTs Do Not Answer",[38,448,449],{},"Imagine an application issues one valid bearer token for seven days and stores it somewhere malicious JavaScript can read. The signature can be flawless. The issuer and audience can be correct. One successful theft still gives the attacker a working credential.",[38,451,452],{},"Changing the storage mechanism creates different trade-offs. Adding refresh tokens creates another credential lifecycle. Moving credentials into cookies changes which browser protections matter. Putting roles into claims raises questions about stale permissions and account disablement.",[38,454,455],{},"Those are not edge cases. They are the auth system.",[38,457,458],{},"The rest of this series will take them one at a time:",[460,461,462,466,469,472],"ul",{},[463,464,465],"li",{},"What token storage changes when XSS enters the threat model.",[463,467,468],{},"Why cookie-authenticated routes still need a CSRF policy.",[463,470,471],{},"How sessions and JWTs fit into the same enforcement checklist.",[463,473,474],{},"Why refresh rotation, reuse detection, device sessions, and revocation need server-side design.",[38,476,477],{},"This post is the boundary marker. A JWT can carry trustworthy claims. Everything that determines how safely those claims are issued, stored, renewed, rejected, and used belongs to the wider system.",[48,479,481],{"id":480},"the-honest-part","The Honest Part",[38,483,484],{},"JWTs can remove a database lookup from a request path and make identity context portable across services. Those are real advantages. They also create operational work around signing keys, claim contracts, clock skew, and token lifetime.",[38,486,487],{},"They can make revocation less immediate. Claims can become stale. Teams often reintroduce state for logout, device sessions, refresh-token families, or incident response after describing the architecture as stateless.",[38,489,490],{},"That is not hypocrisy. Security requirements are allowed to beat conference-slide aesthetics.",[38,492,493],{},"The honest answer is that neither JWTs nor opaque sessions are universally safer. Safety comes from choosing a design that matches the threats, then enforcing the boring rules consistently.",[48,495,497],{"id":496},"my-recommendation","My Recommendation",[38,499,500],{},"Use JWTs when independent verification solves a real architectural problem. Do not use them as a certificate that your auth is modern.",[38,502,503],{},"If you are building a normal web application, start with the simplest mature session architecture your framework supports. If you genuinely need JWTs, write down the rest of the system before shipping the login screen:",[460,505,506,509,512,515,518],{},[463,507,508],{},"Where do credentials live?",[463,510,511],{},"How long does each credential remain useful?",[463,513,514],{},"What happens on logout, password reset, and account disablement?",[463,516,517],{},"Which browser threats apply to every authenticated route?",[463,519,520],{},"Where are authorization rules enforced?",[38,522,523],{},"If those answers are vague, changing the token format will not rescue the design.",[38,525,526],{},"The token is one component. The enforcement around it is the auth system.",[528,529],"hr",{},[38,531,532],{},"What is the most confident JWT setup you have seen that fell apart after one uncomfortable security question? I have a few candidates, including some I wrote myself 😅",[534,535,536],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}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);}",{"title":192,"searchDepth":227,"depth":227,"links":538},[539,540,541,542,543,544],{"id":50,"depth":220,"text":51},{"id":174,"depth":220,"text":175},{"id":420,"depth":220,"text":421},{"id":445,"depth":220,"text":446},{"id":480,"depth":220,"text":481},{"id":496,"depth":220,"text":497},null,"\u002Fimages\u002Fblog\u002Fyour-auth-is-not-safer-because-you-used-jwts.webp","JWT surrounded by the security controls required to build a complete authentication system","2026-07-20","JWTs can prove that claims were signed. They cannot decide where credentials live, how logout works, or what a user may do. A token format is not an auth system.","md",{},"\u002Fblog\u002Fyour-auth-is-not-safer-because-you-used-jwts",{"title":32,"description":549},"auth-is-more-than-login","blog\u002Fyour-auth-is-not-safer-because-you-used-jwts",[557,558,559,560],"security","authentication","web-security","opinion","douQyUcF5ldqfO2317VGzYYhCGsWoY1JpEmmjdfw1yM",{"id":563,"title":564,"author":33,"body":565,"canonical":545,"category":545,"cover":986,"coverAlt":987,"date":988,"description":989,"extension":550,"meta":990,"navigation":223,"ogImage":991,"path":992,"published":223,"readingTime":335,"seo":993,"series":545,"stem":994,"tags":995,"updated":545,"__hash__":999},"blog\u002Fblog\u002Fi-tried-4-cmses-before-landing-on-nuxt-studio.md","I Tried 4 CMSes Before Landing on Nuxt Studio",{"type":35,"value":566,"toc":975},[567,570,573,576,580,588,591,594,615,618,622,625,628,631,634,637,641,644,647,650,656,662,668,671,675,678,681,684,687,690,694,697,700,708,715,718,722,725,728,731,734,737,743,747,872,879,883,889,895,901,907,913,917,922,939,944,958,964,966,969,972],[38,568,569],{},"$15\u002Fmonth. That's what Framer was charging me to host a portfolio site that gets a few hundred visits a month. Not a SaaS app. Not an e-commerce store. A portfolio. A glorified list of projects and a blog.",[38,571,572],{},"I've since tried Sanity, self-hosted Directus, and finally landed on Nuxt Content v3 with Nuxt Studio. My CMS bill is now $0. My content lives in Git. And I can move the whole thing to a different host in about 20 minutes.",[38,574,575],{},"Here's how I got here, and why I'm not moving again.",[48,577,579],{"id":578},"the-framer-exit","The Framer Exit",[38,581,582,583,587],{},"Let me be clear: Framer is a beautiful tool. The editor is smooth, the templates are gorgeous, and the animations are ",[584,585,586],"em",{},"chef's kiss",". If you're a designer who wants a portfolio up in an afternoon, it's genuinely great.",[38,589,590],{},"But I'm a developer. And $15\u002Fmonth for a static portfolio started feeling like a subscription to a gym I wasn't using properly.",[38,592,593],{},"The problems weren't just the price:",[460,595,596,603,609],{},[463,597,598,602],{},[599,600,601],"strong",{},"No real code ownership."," Your site lives in Framer's editor. Export options are limited. If Framer doubles their pricing tomorrow, you're stuck.",[463,604,605,608],{},[599,606,607],{},"Custom functionality?"," Good luck. Want a custom RSS feed? A dynamic sitemap? Code highlighting in blog posts? You're fighting the platform instead of building.",[463,610,611,614],{},[599,612,613],{},"Performance."," Framer sites are fast-ish, but you're loading their runtime whether you need it or not.",[38,616,617],{},"I liked Framer for what it was. But for what I needed, it was the wrong tool at the wrong price.",[48,619,621],{"id":620},"sanity-great-until-the-bill-arrives","Sanity: Great Until the Bill Arrives",[38,623,624],{},"After Framer, I went the \"proper\" headless CMS route. Sanity has a generous free tier, a fantastic query language (GROQ), and a studio that's genuinely nice to work with.",[38,626,627],{},"For about three months, I was happy. Then I started paying attention to the pricing page.",[38,629,630],{},"Sanity's free tier works fine for a solo portfolio. But the moment you start thinking bigger, the numbers get uncomfortable. API CDN requests, dataset storage, bandwidth. It all adds up. And the real cost isn't even monetary. It's architectural.",[38,632,633],{},"Your content lives in Sanity's cloud. Your schemas are defined in their format. Your queries use GROQ, which is Sanity-only. Every piece of content you create deepens the lock-in.",[38,635,636],{},"When I wanted to migrate my blog posts out, I had to write a custom export script, transform the block content format back into markdown, and manually fix all the image references. For a portfolio blog with maybe 10 posts, it took me an entire evening. Imagine doing that with 200 posts.",[48,638,640],{"id":639},"directus-self-hosting-your-way-into-more-problems","Directus: Self-Hosting Your Way Into More Problems",[38,642,643],{},"\"I'll self-host my CMS,\" I said, already running 30+ Docker containers on my Contabo VPS. \"What's one more?\" 😅",[38,645,646],{},"Directus is genuinely good open-source software. It gives you a beautiful admin panel, auto-generates REST and GraphQL APIs from your database, and lets you own everything. On paper, it was perfect.",[38,648,649],{},"In practice? Three problems.",[38,651,652,655],{},[599,653,654],{},"1. Another container on an already packed VPS."," Directus needs PostgreSQL (or MySQL), plus its own Node.js process. My VPS was already running at ~80% RAM. Adding Directus meant something else had to shrink, or I'd need a bigger server. More money, more complexity.",[38,657,658,661],{},[599,659,660],{},"2. Maintenance never stops."," Self-hosting a CMS means self-hosting its database, its migrations, its updates. When Directus pushes a major version, you're the one debugging why the upgrade script failed at 11pm on a Tuesday.",[38,663,664,667],{},[599,665,666],{},"3. The lock-in problem, again."," Your content is in a PostgreSQL database, structured however Directus schemas define it. Want to move to a different CMS? Cool, write another migration script. Convert all your relational data back into flat files. Re-map your media. Again.",[38,669,670],{},"I was solving the pricing problem by self-hosting, but I wasn't solving the lock-in problem at all. I just moved the lock from a SaaS company's servers to a database on my own server.",[48,672,674],{"id":673},"the-realization","The Realization",[38,676,677],{},"Every CMS I tried had the same fundamental issue: my content didn't belong to me in any portable way.",[38,679,680],{},"Framer owned the rendering. Sanity owned the storage format. Directus owned the database schema. Moving between any of them meant a migration project.",[38,682,683],{},"Then I looked at my content and asked a simple question: what is a portfolio, really?",[38,685,686],{},"It's a list of projects (titles, descriptions, tags, links). A few testimonials. Some service descriptions. Blog posts. An about section. That's it. It's just data. Simple, flat, structured data.",[38,688,689],{},"Why was I running a database for this?",[48,691,693],{"id":692},"nuxt-content-v3-files-are-the-cms","Nuxt Content v3: Files Are the CMS",[38,695,696],{},"Nuxt Content v3 treats your file system as the content layer. Blog posts are Markdown files. Portfolio data (projects, testimonials, services) are YAML files. Everything lives in your Git repo, versioned alongside your code.",[38,698,699],{},"Here's what my content directory looks like:",[187,701,706],{"className":702,"code":704,"language":705,"meta":192},[703],"language-text","content\u002F\n├── blog\u002F\n│   ├── hello-world.md\n│   └── self-hosting-30-docker-containers.md\n├── projects\u002F\n│   ├── kora-payments.yml\n│   └── fincra-dashboard.yml\n├── testimonials\u002F\n│   ├── john-doe.yml\n│   └── jane-smith.yml\n├── services\u002F\n│   └── web-development.yml\n└── data\u002F\n    └── site.yml\n","text",[105,707,704],{"__ignoreMap":192},[38,709,710,711,714],{},"No database. No API keys. No hosted backend. You define your schemas in ",[105,712,713],{},"content.config.ts"," with Zod, and Nuxt Content handles querying, rendering, and hot-reloading in dev.",[38,716,717],{},"The migration from Directus? I exported my data, saved each item as a YAML file, and committed to Git. Took about an hour, including writing the schemas.",[48,719,721],{"id":720},"where-nuxt-studio-fits-in","Where Nuxt Studio Fits In",[38,723,724],{},"\"But Sherif, do you really want to write YAML by hand every time you update a project?\"",[38,726,727],{},"No. That's where Nuxt Studio comes in.",[38,729,730],{},"Nuxt Studio is a visual editor that sits on top of your Nuxt Content project. It connects to your Git repo and gives you a nice UI to edit content, preview changes live, and commit when you're happy.",[38,732,733],{},"It's not a CMS in the traditional sense. There's no database and no separate hosted platform. It's a Nuxt module that runs as part of your app, with a built-in editor route you can lock down with GitHub, GitLab, or even Google OAuth. That last one is interesting - it means you can hand content editing to someone who doesn't have a GitHub account. When you save, it commits directly to your Git repo. That's it.",[38,735,736],{},"For blog posts, I still prefer VS Code. But for updating a project description, adding a testimonial, or fixing a typo? Studio is perfect. Open the browser, make the change, save, done. No terminal required.",[38,738,739],{},[414,740],{"alt":741,"src":742},"Nuxt Studio editor showing visual content editing with live preview","\u002Fimages\u002Fblog\u002Fnuxt-studio-editor.webp",[48,744,746],{"id":745},"the-numbers","The Numbers 💰",[62,748,749,767],{},[65,750,751],{},[68,752,753,755,758,761,764],{},[71,754],{},[71,756,757],{},"Framer",[71,759,760],{},"Sanity",[71,762,763],{},"Directus (self-hosted)",[71,765,766],{},"Nuxt + Cloudflare",[82,768,769,788,806,823,841,857],{},[68,770,771,776,779,782,785],{},[87,772,773],{},[599,774,775],{},"Monthly cost",[87,777,778],{},"$15",[87,780,781],{},"$0-99",[87,783,784],{},"~$5 (VPS share)",[87,786,787],{},"$0",[68,789,790,795,798,801,803],{},[87,791,792],{},[599,793,794],{},"Content portability",[87,796,797],{},"Low",[87,799,800],{},"Medium",[87,802,800],{},[87,804,805],{},"High (it's files)",[68,807,808,813,816,818,820],{},[87,809,810],{},[599,811,812],{},"Vendor lock-in",[87,814,815],{},"High",[87,817,815],{},[87,819,800],{},[87,821,822],{},"None",[68,824,825,830,833,836,838],{},[87,826,827],{},[599,828,829],{},"Custom functionality",[87,831,832],{},"Limited",[87,834,835],{},"Good",[87,837,835],{},[87,839,840],{},"Full (it's Nuxt)",[68,842,843,848,850,852,854],{},[87,844,845],{},[599,846,847],{},"Migration effort",[87,849,815],{},[87,851,800],{},[87,853,800],{},[87,855,856],{},"Copy the files",[68,858,859,864,866,868,870],{},[87,860,861],{},[599,862,863],{},"Maintenance",[87,865,822],{},[87,867,797],{},[87,869,815],{},[87,871,822],{},[38,873,874,875,878],{},"My portfolio now runs on Cloudflare Pages. Free tier. My domain is already on Cloudflare, so deployment is ",[105,876,877],{},"git push"," and done. Build time is under a minute.",[48,880,882],{"id":881},"the-trade-offs","The Trade-Offs",[38,884,885,888],{},[599,886,887],{},"No visual page builder."," Nuxt Studio lets you edit content, not drag-and-drop layouts. If you want Framer-style design flexibility without code, this isn't it.",[38,890,891,894],{},[599,892,893],{},"You need to know Nuxt."," This stack assumes you can build a Nuxt app. If you're a designer who just wants a portfolio online, Framer is still the better choice for you.",[38,896,897,900],{},[599,898,899],{},"Content v3 is still maturing."," I've hit quirks with data collections (each item needs its own file, no arrays in single files). The docs are improving but there are gaps. You'll spend some time in GitHub issues.",[38,902,903,906],{},[599,904,905],{},"Studio isn't as polished as Sanity's studio."," Sanity's editing experience is genuinely best-in-class. Nuxt Studio is good and getting better, but it's not there yet.",[38,908,909,912],{},[599,910,911],{},"You're still the ops team."," No database to maintain, sure. But you still need to handle build configs, deployment, and debugging when the Nuxt build fails because of a malformed frontmatter field at midnight.",[48,914,916],{"id":915},"should-you-make-this-switch","Should You Make This Switch?",[38,918,919],{},[599,920,921],{},"Yes, if you:",[460,923,924,927,930,933,936],{},[463,925,926],{},"Are a developer comfortable with Nuxt\u002FVue",[463,928,929],{},"Want $0 hosting with Cloudflare Pages or similar",[463,931,932],{},"Care about owning your content in portable formats",[463,934,935],{},"Are tired of CMS pricing games",[463,937,938],{},"Want your content versioned in Git alongside your code",[38,940,941],{},[599,942,943],{},"No, if you:",[460,945,946,949,952,955],{},[463,947,948],{},"Need a visual page builder (stick with Framer or Webflow)",[463,950,951],{},"Want a CMS that non-technical clients can use independently",[463,953,954],{},"Need complex relational content (use a real database)",[463,956,957],{},"Don't know Vue\u002FNuxt and don't want to learn it for a portfolio",[38,959,960,961,963],{},"If you're in the \"yes\" camp, the setup is straightforward: scaffold a Nuxt 4 app, add the Content module, define your collections in ",[105,962,713],{},", drop in your Markdown and YAML files, and deploy to Cloudflare Pages. The whole thing takes an afternoon.",[528,965],{},[38,967,968],{},"Four CMSes, probably 60+ hours of migrations, and one hard lesson: the best CMS for a portfolio is the one that doesn't exist. Just files, in a repo, deployed for free.",[38,970,971],{},"My content is markdown and YAML now. If I want to move to Astro next year, I copy the files. If Cloudflare Pages shuts down tomorrow, I deploy somewhere else in 10 minutes. No export scripts. No migration hell. No $15\u002Fmonth for the privilege of hosting a list of my own projects.",[38,973,974],{},"What's the most you've ever paid monthly for a portfolio or personal site? I'm genuinely curious if anyone's topped my Framer + Sanity combo.",{"title":192,"searchDepth":227,"depth":227,"links":976},[977,978,979,980,981,982,983,984,985],{"id":578,"depth":220,"text":579},{"id":620,"depth":220,"text":621},{"id":639,"depth":220,"text":640},{"id":673,"depth":220,"text":674},{"id":692,"depth":220,"text":693},{"id":720,"depth":220,"text":721},{"id":745,"depth":220,"text":746},{"id":881,"depth":220,"text":882},{"id":915,"depth":220,"text":916},"\u002Fimages\u002Fblog\u002Fcms-migration-journey.webp","Migration path from Framer to Sanity to Directus to Nuxt, showing each CMS crossed out with price labels and Nuxt glowing green at $0","2026-05-15","Framer, Sanity, Directus, and a lot of frustration. Here's why my portfolio content now lives in Git files and costs me $0\u002Fmonth to manage.",{},"\u002Fimages\u002Fblog\u002Fcms-migration-journey-og.png","\u002Fblog\u002Fi-tried-4-cmses-before-landing-on-nuxt-studio",{"title":564,"description":989},"blog\u002Fi-tried-4-cmses-before-landing-on-nuxt-studio",[996,997,560,998],"nuxt","cms","case-study","X3mcrhxvR4mJQSDikxhltr0yJFeeSybStTt1IYoKeZw",{"id":1001,"title":1002,"author":33,"body":1003,"canonical":545,"category":1267,"cover":545,"coverAlt":545,"date":1268,"description":1269,"extension":550,"meta":1270,"navigation":223,"ogImage":545,"path":1271,"published":223,"readingTime":545,"seo":1272,"series":545,"stem":1273,"tags":1274,"updated":545,"__hash__":1277},"blog\u002Fblog\u002Fhello-world.md","Hello World",{"type":35,"value":1004,"toc":1260},[1005,1009,1012,1015,1019,1022,1025,1029,1032,1049,1052,1056,1059,1228,1232,1254,1257],[48,1006,1008],{"id":1007},"so-a-blog","So, a blog.",[38,1010,1011],{},"I've been building software for a while now. Fintech platforms, enterprise systems, mobile apps, no-code sites. I've shipped a lot of stuff. But I never really wrote about any of it.",[38,1013,1014],{},"That changes today.",[48,1016,1018],{"id":1017},"why-now","Why now?",[38,1020,1021],{},"Honestly? I kept running into the same problems, googling the same things, explaining the same concepts to junior devs on my team. At some point it just made sense to write things down properly. If I'm going to explain how to set up a CI\u002FCD pipeline for the third time this year, might as well put it somewhere useful.",[38,1023,1024],{},"Also, I recently rebuilt this entire portfolio from a Framer site into a Nuxt 4 app with a headless CMS. That process alone gave me enough material for like five posts.",[48,1026,1028],{"id":1027},"what-ill-write-about","What I'll write about",[38,1030,1031],{},"Mostly things I actually deal with day to day:",[460,1033,1034,1037,1040,1043,1046],{},[463,1035,1036],{},"Building fullstack apps with TypeScript, React, Next.js, Vue, Node.js",[463,1038,1039],{},"Cloud stuff. AWS, Docker, Kubernetes, CI\u002FCD pipelines",[463,1041,1042],{},"Mobile dev with React Native and Flutter",[463,1044,1045],{},"No-code tools like Framer, Webflow, n8n (yes, I use these too)",[463,1047,1048],{},"Lessons from working across fintech, edtech, and enterprise",[38,1050,1051],{},"No fluff. No \"10 reasons why React is amazing\" listicles. Just real problems, real solutions, and the occasional opinion.",[48,1053,1055],{"id":1054},"a-quick-hello-in-code","A quick hello in code",[38,1057,1058],{},"Since this is a dev blog, here's how I'd introduce myself in TypeScript:",[187,1060,1062],{"className":189,"code":1061,"language":191,"meta":192,"style":192},"interface Developer {\n  name: string;\n  skills: string[];\n  currentRole: string;\n}\n\nconst sheriff: Developer = {\n  name: \"Sherifdeen Adebayo\",\n  skills: [\"TypeScript\", \"React\", \"React Native\", \"Vue\", \"Node.js\", \"AWS\", \"Docker\"],\n  currentRole: \"Senior Full-Stack Developer\",\n};\n\nconsole.log(`Hello from ${sheriff.name}!`);\n",[105,1063,1064,1075,1086,1098,1109,1113,1117,1132,1142,1182,1192,1197,1201],{"__ignoreMap":192},[196,1065,1066,1069,1072],{"class":198,"line":199},[196,1067,1068],{"class":202},"interface",[196,1070,1071],{"class":243}," Developer",[196,1073,1074],{"class":206}," {\n",[196,1076,1077,1080,1082,1084],{"class":198,"line":220},[196,1078,1079],{"class":287},"  name",[196,1081,291],{"class":202},[196,1083,294],{"class":233},[196,1085,217],{"class":206},[196,1087,1088,1091,1093,1095],{"class":198,"line":227},[196,1089,1090],{"class":287},"  skills",[196,1092,291],{"class":202},[196,1094,294],{"class":233},[196,1096,1097],{"class":206},"[];\n",[196,1099,1100,1103,1105,1107],{"class":198,"line":264},[196,1101,1102],{"class":287},"  currentRole",[196,1104,291],{"class":202},[196,1106,294],{"class":233},[196,1108,217],{"class":206},[196,1110,1111],{"class":198,"line":269},[196,1112,382],{"class":206},[196,1114,1115],{"class":198,"line":300},[196,1116,224],{"emptyLinePlaceholder":223},[196,1118,1119,1121,1124,1126,1128,1130],{"class":198,"line":312},[196,1120,230],{"class":202},[196,1122,1123],{"class":233}," sheriff",[196,1125,291],{"class":202},[196,1127,1071],{"class":243},[196,1129,237],{"class":202},[196,1131,1074],{"class":206},[196,1133,1134,1137,1140],{"class":198,"line":323},[196,1135,1136],{"class":206},"  name: ",[196,1138,1139],{"class":213},"\"Sherifdeen Adebayo\"",[196,1141,332],{"class":206},[196,1143,1144,1147,1150,1152,1155,1157,1160,1162,1165,1167,1170,1172,1175,1177,1180],{"class":198,"line":335},[196,1145,1146],{"class":206},"  skills: [",[196,1148,1149],{"class":213},"\"TypeScript\"",[196,1151,108],{"class":206},[196,1153,1154],{"class":213},"\"React\"",[196,1156,108],{"class":206},[196,1158,1159],{"class":213},"\"React Native\"",[196,1161,108],{"class":206},[196,1163,1164],{"class":213},"\"Vue\"",[196,1166,108],{"class":206},[196,1168,1169],{"class":213},"\"Node.js\"",[196,1171,108],{"class":206},[196,1173,1174],{"class":213},"\"AWS\"",[196,1176,108],{"class":206},[196,1178,1179],{"class":213},"\"Docker\"",[196,1181,320],{"class":206},[196,1183,1184,1187,1190],{"class":198,"line":346},[196,1185,1186],{"class":206},"  currentRole: ",[196,1188,1189],{"class":213},"\"Senior Full-Stack Developer\"",[196,1191,332],{"class":206},[196,1193,1194],{"class":198,"line":362},[196,1195,1196],{"class":206},"};\n",[196,1198,1199],{"class":198,"line":373},[196,1200,224],{"emptyLinePlaceholder":223},[196,1202,1203,1206,1209,1211,1214,1217,1220,1223,1226],{"class":198,"line":379},[196,1204,1205],{"class":206},"console.",[196,1207,1208],{"class":243},"log",[196,1210,284],{"class":206},[196,1212,1213],{"class":213},"`Hello from ${",[196,1215,1216],{"class":206},"sheriff",[196,1218,1219],{"class":213},".",[196,1221,1222],{"class":206},"name",[196,1224,1225],{"class":213},"}!`",[196,1227,261],{"class":206},[48,1229,1231],{"id":1230},"lets-talk","Let's talk",[38,1233,1234,1235,1239,1240,108,1245,1249,1250,1219],{},"If you want to work together or just nerd out about tech, ",[390,1236,1238],{"href":5,"rel":1237},[394],"book a call"," or find me on ",[390,1241,1244],{"href":1242,"rel":1243},"https:\u002F\u002Flinkedin.com\u002Fin\u002F0xsherif",[394],"LinkedIn",[390,1246,1248],{"href":12,"rel":1247},[394],"X",", or ",[390,1251,1253],{"href":13,"rel":1252},[394],"GitHub",[38,1255,1256],{},"More posts coming soon. For real this time.",[534,1258,1259],{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}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);}",{"title":192,"searchDepth":227,"depth":227,"links":1261},[1262,1263,1264,1265,1266],{"id":1007,"depth":220,"text":1008},{"id":1017,"depth":220,"text":1018},{"id":1027,"depth":220,"text":1028},{"id":1054,"depth":220,"text":1055},{"id":1230,"depth":220,"text":1231},"General","2026-05-14","First post. Why I'm starting a blog after years of writing code.",{},"\u002Fblog\u002Fhello-world",{"title":1002,"description":1269},"blog\u002Fhello-world",[1275,1276],"announcement","blog","N79PddJDvMpck756feHX6mhNLsCo-eCwLdH0WEFjmHA",{"id":1279,"title":1280,"author":33,"body":1281,"canonical":545,"category":545,"cover":1810,"coverAlt":1811,"date":1268,"description":1812,"extension":550,"meta":1813,"navigation":223,"ogImage":1814,"path":1815,"published":223,"readingTime":545,"seo":1816,"series":545,"stem":1817,"tags":1818,"updated":1268,"__hash__":1823},"blog\u002Fblog\u002Fself-hosting-30-docker-containers-on-a-single-vps.md","Self-Hosting 30+ Docker Containers on a Single VPS",{"type":35,"value":1282,"toc":1799},[1283,1286,1290,1293,1296,1299,1303,1309,1326,1329,1333,1336,1470,1473,1477,1484,1487,1508,1511,1515,1522,1525,1537,1540,1544,1547,1674,1677,1680,1684,1690,1696,1702,1712,1718,1721,1725,1729,1746,1750,1764,1768,1788,1791,1793,1796],[38,1284,1285],{},"Let me explain how I got here, because trust me, this wasn't the plan.",[48,1287,1289],{"id":1288},"how-it-started","How It Started",[38,1291,1292],{},"It started innocently. I needed n8n for some workflow automation. The cloud plan? $20\u002Fmonth for limited executions. \"I'll just self-host it,\" I said. Famous last words 😅",[38,1294,1295],{},"Then I needed a search engine. Then file storage. Then a database tool. Then monitoring. Then a document processing API. Then... you get the idea.",[38,1297,1298],{},"Before I knew it, I was running a mini data center from a $15\u002Fmonth VPS. My friends think I have a problem. They might be right.",[48,1300,1302],{"id":1301},"the-server","The Server",[38,1304,1305,1306,291],{},"I'm running a ",[599,1307,1308],{},"Contabo Cloud VPS 30",[460,1310,1311,1314,1317,1320,1323],{},[463,1312,1313],{},"12 CPU cores (AMD EPYC)",[463,1315,1316],{},"24 GB RAM",[463,1318,1319],{},"200 GB NVMe disk",[463,1321,1322],{},"Unlimited traffic",[463,1324,1325],{},"~$15\u002Fmonth",[38,1327,1328],{},"Is Contabo the fastest? No. Is the support great? Also no. But for $15\u002Fmonth with those specs? Nothing comes close. It's the Honda Civic of VPS providers. Reliable, affordable, gets the job done. Nobody's taking Instagram photos of it, but it works 🚗",[48,1330,1332],{"id":1331},"the-stack-aka-what-are-you-running","The Stack (aka \"What Are You Running?!\")",[38,1334,1335],{},"Here's what's actually running on this thing:",[62,1337,1338,1351],{},[65,1339,1340],{},[68,1341,1342,1345,1348],{},[71,1343,1344],{},"Service",[71,1346,1347],{},"What It Does",[71,1349,1350],{},"Why Not SaaS?",[82,1352,1353,1366,1379,1392,1405,1418,1431,1444,1457],{},[68,1354,1355,1360,1363],{},[87,1356,1357],{},[599,1358,1359],{},"Coolify",[87,1361,1362],{},"Self-hosted PaaS",[87,1364,1365],{},"Replaces Vercel\u002FRailway. Free.",[68,1367,1368,1373,1376],{},[87,1369,1370],{},[599,1371,1372],{},"n8n",[87,1374,1375],{},"Workflow automation",[87,1377,1378],{},"Unlimited executions. No per-run billing.",[68,1380,1381,1386,1389],{},[87,1382,1383],{},[599,1384,1385],{},"SearXNG",[87,1387,1388],{},"Meta search engine",[87,1390,1391],{},"Private, no tracking. Try doing that with Google 👀",[68,1393,1394,1399,1402],{},[87,1395,1396],{},[599,1397,1398],{},"NocoDB",[87,1400,1401],{},"Spreadsheet database",[87,1403,1404],{},"Airtable but free and unlimited",[68,1406,1407,1412,1415],{},[87,1408,1409],{},[599,1410,1411],{},"Nextcloud",[87,1413,1414],{},"File storage & sync",[87,1416,1417],{},"Google Drive but I own my data",[68,1419,1420,1425,1428],{},[87,1421,1422],{},[599,1423,1424],{},"Docling",[87,1426,1427],{},"Document processing API",[87,1429,1430],{},"PDF\u002Fimage parsing for AI pipelines",[68,1432,1433,1438,1441],{},[87,1434,1435],{},[599,1436,1437],{},"Uptime Kuma",[87,1439,1440],{},"Monitoring",[87,1442,1443],{},"Knows when things break before my clients do",[68,1445,1446,1451,1454],{},[87,1447,1448],{},[599,1449,1450],{},"PostgreSQL",[87,1452,1453],{},"Database",[87,1455,1456],{},"Shared across multiple services",[68,1458,1459,1464,1467],{},[87,1460,1461],{},[599,1462,1463],{},"Redis",[87,1465,1466],{},"Caching",[87,1468,1469],{},"Makes Nextcloud not feel like it's running on a toaster",[38,1471,1472],{},"Plus Tailscale for private networking, Let's Encrypt for HTTPS, and about 15 other app deployments. It adds up.",[48,1474,1476],{"id":1475},"coolify-the-thing-that-makes-it-all-possible","Coolify: The Thing That Makes It All Possible",[38,1478,1479,1480,1483],{},"Before Coolify, I was SSH-ing into the server and running ",[105,1481,1482],{},"docker compose up -d"," like some kind of caveman 🦕",[38,1485,1486],{},"Now I have a nice web UI where I can:",[460,1488,1489,1492,1499,1502,1505],{},[463,1490,1491],{},"Deploy from GitHub with one click",[463,1493,1494,1495,1498],{},"Manage environment variables without touching ",[105,1496,1497],{},".env"," files",[463,1500,1501],{},"Get automatic HTTPS on every service",[463,1503,1504],{},"Monitor which container is eating all my RAM (it's always Nextcloud, by the way)",[463,1506,1507],{},"Roll back when I inevitably break something",[38,1509,1510],{},"The learning curve? About a weekend. If you've used Vercel or Railway before, you'll feel right at home. Except this time, you're not paying per seat, per build, or per breath.",[48,1512,1514],{"id":1513},"tailscale-because-not-everything-needs-to-be-public","Tailscale: Because Not Everything Needs to Be Public",[38,1516,1517,1518,1521],{},"Some of my services are... let's say \"for my eyes only.\" My search engine, database tools, internal dashboards. These are only accessible through ",[599,1519,1520],{},"Tailscale",", a mesh VPN.",[38,1523,1524],{},"The setup is embarrassingly simple:",[1526,1527,1528,1531,1534],"ol",{},[463,1529,1530],{},"Install Tailscale on server",[463,1532,1533],{},"Install on your devices",[463,1535,1536],{},"Private services bind to Tailscale IP only",[38,1538,1539],{},"That's it. No VPN configs. No firewall rules. No \"why can't I connect\" debugging at 2am. Tailscale just works. And the free tier covers 100 devices, which is about 97 more than I need.",[48,1541,1543],{"id":1542},"lets-talk-money","Let's Talk Money 💰",[38,1545,1546],{},"This is the part that makes me smile every time:",[62,1548,1549,1561],{},[65,1550,1551],{},[68,1552,1553,1555,1558],{},[71,1554,1344],{},[71,1556,1557],{},"SaaS Cost (monthly)",[71,1559,1560],{},"My Cost",[82,1562,1563,1573,1583,1592,1602,1611,1621,1630,1643,1657],{},[68,1564,1565,1568,1571],{},[87,1566,1567],{},"n8n Cloud",[87,1569,1570],{},"$20+",[87,1572,787],{},[68,1574,1575,1578,1581],{},[87,1576,1577],{},"Nextcloud \u002F Google One",[87,1579,1580],{},"$10+",[87,1582,787],{},[68,1584,1585,1588,1590],{},[87,1586,1587],{},"NocoDB \u002F Airtable",[87,1589,1570],{},[87,1591,787],{},[68,1593,1594,1597,1600],{},[87,1595,1596],{},"Search API",[87,1598,1599],{},"$50+",[87,1601,787],{},[68,1603,1604,1606,1609],{},[87,1605,1440],{},[87,1607,1608],{},"$7+",[87,1610,787],{},[68,1612,1613,1616,1619],{},[87,1614,1615],{},"Vercel\u002FRailway (multiple apps)",[87,1617,1618],{},"$40+",[87,1620,787],{},[68,1622,1623,1625,1628],{},[87,1624,1427],{},[87,1626,1627],{},"$30+",[87,1629,787],{},[68,1631,1632,1637,1639],{},[87,1633,1634],{},[599,1635,1636],{},"My VPS",[87,1638],{},[87,1640,1641],{},[599,1642,778],{},[68,1644,1645,1650,1652],{},[87,1646,1647],{},[599,1648,1649],{},"Domain",[87,1651],{},[87,1653,1654],{},[599,1655,1656],{},"~$1",[68,1658,1659,1664,1669],{},[87,1660,1661],{},[599,1662,1663],{},"Total",[87,1665,1666],{},[599,1667,1668],{},"$187+",[87,1670,1671],{},[599,1672,1673],{},"~$16",[38,1675,1676],{},"That's a conservative estimate. Some of these SaaS tools charge per seat, per execution, or per GB stored. The real gap is probably wider.",[38,1678,1679],{},"But before you go canceling all your subscriptions, let me be honest about the other side...",[48,1681,1683],{"id":1682},"the-stuff-nobody-tells-you","The Stuff Nobody Tells You 😬",[38,1685,1686,1689],{},[599,1687,1688],{},"It's not set-and-forget."," Things break. Docker runs out of disk space at 3am. A container decides to eat all 24GB of RAM for breakfast. PostgreSQL needs vacuuming (yes, databases need housekeeping too).",[38,1691,1692,1695],{},[599,1693,1694],{},"Backups are YOUR problem."," When you use Vercel, they handle backups. When you self-host, if your disk dies and you didn't set up backups... that's on you. I learned this the scary way. Set up automated backups early. Do it now. I'll wait.",[38,1697,1698,1701],{},[599,1699,1700],{},"Updates never stop."," Security patches, version upgrades, dependency conflicts. It's not hard, but it's constant. Think of it like having a garden. Rewarding, but you can't just plant things and walk away.",[38,1703,1704,1707,1708,1711],{},[599,1705,1706],{},"RAM is never enough."," 24 GB sounds like a lot until PostgreSQL, Redis, Nextcloud, and 10 other services are all fighting over it. I've become very familiar with ",[105,1709,1710],{},"docker stats"," and very good at setting memory limits.",[38,1713,1714,1717],{},[599,1715,1716],{},"Start small."," I didn't deploy 30 containers on day one. I started with Coolify and one app. Then added services as I needed them. Each new service taught me something about Docker networking, reverse proxies, or why you should always read the docs before running random commands from Stack Overflow 📚",[38,1719,1720],{},"The biggest lesson? Self-hosting is a skill. The first service takes a weekend. The tenth takes 20 minutes.",[48,1722,1724],{"id":1723},"should-you-do-this","Should You Do This?",[38,1726,1727],{},[599,1728,921],{},[460,1730,1731,1734,1737,1740,1743],{},[463,1732,1733],{},"Run multiple side projects",[463,1735,1736],{},"Know basic Linux and Docker",[463,1738,1739],{},"Like owning your stuff",[463,1741,1742],{},"Want to learn DevOps by actually doing it",[463,1744,1745],{},"Are tired of surprise SaaS invoices",[38,1747,1748],{},[599,1749,943],{},[460,1751,1752,1755,1758,1761],{},[463,1753,1754],{},"Need five-nines uptime for production",[463,1756,1757],{},"Don't want to think about servers ever",[463,1759,1760],{},"Haven't used Docker before (learn that first, then come back)",[463,1762,1763],{},"Value your weekends too much 😂",[48,1765,1767],{"id":1766},"getting-started-the-quick-version","Getting Started (The Quick Version)",[1526,1769,1770,1773,1776,1779,1782,1785],{},[463,1771,1772],{},"Get a VPS (Contabo or Hetzner)",[463,1774,1775],{},"Install Coolify (literally one command)",[463,1777,1778],{},"Deploy the service that costs you the most",[463,1780,1781],{},"Add Tailscale for private access",[463,1783,1784],{},"Add more services as you go",[463,1786,1787],{},"Try not to become addicted",[38,1789,1790],{},"You don't need to replace everything overnight. Start with one service and build from there.",[528,1792],{},[38,1794,1795],{},"I've been running this setup for months now and I keep finding new things to self-host. At this point, I think I might have an actual problem 😂",[38,1797,1798],{},"What would be the first service you'd move off SaaS? Drop it in the comments, I'm curious!",{"title":192,"searchDepth":227,"depth":227,"links":1800},[1801,1802,1803,1804,1805,1806,1807,1808,1809],{"id":1288,"depth":220,"text":1289},{"id":1301,"depth":220,"text":1302},{"id":1331,"depth":220,"text":1332},{"id":1475,"depth":220,"text":1476},{"id":1513,"depth":220,"text":1514},{"id":1542,"depth":220,"text":1543},{"id":1682,"depth":220,"text":1683},{"id":1723,"depth":220,"text":1724},{"id":1766,"depth":220,"text":1767},"\u002Fimages\u002Fblog\u002Fselfhosted-stack.webp","Self-hosted Docker stack architecture diagram","I run 30+ Docker containers on a single VPS. Yes, thirty. On one box. And no, my server hasn't caught fire yet 🔥 (I check every morning though, just in case).",{},"\u002Fimages\u002Fblog\u002Fselfhosted-stack-og.png","\u002Fblog\u002Fself-hosting-30-docker-containers-on-a-single-vps",{"title":1280,"description":1812},"blog\u002Fself-hosting-30-docker-containers-on-a-single-vps",[1819,1820,1821,1822],"self-hosting","docker","coolify","devops","TtzMuUNcW1dMc4Vd_kgSdJcek0Say8mUA80BoFRI210",[1825],{"id":17,"title":18,"description":19,"extension":8,"meta":1826,"posts":1827,"stem":27,"__hash__":28},{},[22,23,24,25,26],1784540810629]