Series · 2 parts · 3 min total
Self-hosting a blog on OCI free tier
Everything I broke getting this site onto a 1 GB box that was already busy.
Part 1 of 2
Reading the machine before writing any code
The tempting order is to pick a framework first and discover the constraints later. I did it the other way round, and it saved me from a stack that would not have survived its first week.
What the box actually had
The server is an Oracle Cloud free-tier instance. On paper that is 1 GB of RAM. In practice, after the operating system and the services already running on it, there was rather less:
total used free available
Mem: 956M 615M 97M 193M
Swap: 0B 0B 0B
193 MB available and no swap at all. Two other services were already living there: a FastAPI app holding 22% of memory, and a small bot holding another 9%.
Why that ruled things out
A server-rendered Node application typically wants somewhere between 150 and 250 MB resident. With 193 MB free and no swap, that is not a tight fit — it is an out-of-memory kill waiting to happen, and the kernel does not politely pick the process you would have chosen. It would likely have taken one of the existing services down with it.
The lesson I keep relearning: measure the target before designing for it.
What I changed
Two things, before writing a line of application code:
- Stopped a service that did not need to run continuously. The FastAPI app is used occasionally, not constantly. Disabling it from boot — while leaving it installed and startable on demand — returned about 210 MB.
- Added swap. Not as a substitute for real memory, but so that an unexpected spike degrades into slowness instead of a kill.
Available memory went from 193 MB to over 400 MB. Only then did the stack question become interesting rather than constrained.
Part 2 of 2
Static where it can be, dynamic where it must be
With memory freed up, every option fit. That made the decision harder, not easier — “it runs” stopped being the filter.
The question that settled it
What does a blog’s public page actually need at request time?
Nothing. The content does not change between visitors. There is no personalisation, no session, no query. Rendering it on demand means doing the same work repeatedly and paying for a runtime to do it.
So the public site compiles to static HTML at build time, and the server does nothing but hand over files.
Where dynamic behaviour is genuinely needed
Exactly one place: the admin page where I write. That does need a process — something to accept a form, write a file, and trigger a rebuild.
That process is small and deliberately boring. It does one job, holds almost no memory, and if it crashes the public site is entirely unaffected, because the public site is just files on disk.
The shape that falls out
- Build happens on my laptop, where memory is not scarce.
- The server holds static files plus one small binary.
- A crash in the admin cannot take the blog offline.
That last point is the one I care about most. The failure modes are separate, which is worth more than any framework feature.
Publishing
There is no admin page, and that was a deliberate subtraction. The repository is already the only place I can publish from, and it already has version history, diffs and rollback. An admin panel would have been a second, weaker way to do something git does better — plus a login form, a session cookie and a process running permanently on a server with under 1 GB of memory.
So publishing is git push. A workflow builds the site and copies the result
to the server, which never runs a build itself. The deploy key it uses can
only write to one directory: it cannot open a shell, read anything back, or
escape that path, so the worst a leaked key could do is overwrite pages that
are regenerated on every push anyway.