← Back to Blog

If you read my initial post, Hello World, you know this site started as a simple question: "How can I make a free website?", and I've actually had this question for a long time.

It started when I was a kid in 4th grade in computer class. In between learning how to type fast on a computer and goofing off by going to coolmathgames.com, I distinctly remember asking my teacher the incredibly innocent yet unfathomably naive question of: "How can I make a website?"

If I had maybe grown up in a more tech-centric city like San Fransisco, or if my computer teacher was a bit more knowledgable and excited to inspire a young mind, maybe that would've been the beginning of my illustrious software engineering career, and I could have learned what a HTML was, and coded an "if/else" statement at the age of 10.

But alas, I grew up in a rural town of 6,000, and my computer teacher was almost assuredly not a web developer, and so he did what he thought was answer my question, and showed me how to setup an account on Blogger.

That wasn't really what I was asking about, but I didn't really have the technical vocabulary to articulate that yet, so I thanked him, and moved on to learning how to type without looking at my fingers.

But that question never really went away. It stayed in the back of my mind as I learned more and more about computers and the internet through the years.

In middle school, I learned how to add mods to Minecraft, and I found out about how a video game, and literally everything on a computer, is really just a bunch of files in folders.

In high school, I joined the Robotics club and learned about gear-ratios, omni-directional wheels, and more importantly, how to copy code from the internet and upload it to a weird thing attached to the robot that could run electric motors when I moved a joystick (and I also learned that adding a negative sign means "spin the motor backwards", and it's a lot easier than unscrewing the entire motor and flipping it around).

And in college, while I didn't go down the Computer Science rabbit hole like many of my nerdy peers, I did end up taking some rudimentary courses in Python and Java to fulfill some credit requirements in my Mathematics and Physics majors (an admittedly prideful, masochistic, and socially crippling endeavor).

But because of all these courses, (and my experience as a Student IT Assistant I'm sure), I got a position at a tech company right out of college, and slowly but surely, absorbed software and developer terminology through osmosis. I learned the difference between On-Prem and Cloud-hosted, between Citrix and Cisco, and all the many different acronyms you could come up with to describe software problems while sounding like you knew what you were talking about.

When I left my first tech job, ChatGPT had just released, and I tested it first by asking it to write a poem about a frog on a lilypad, and my mind, like I'm sure everyone else's was at that time, was blown.

The second thing I asked it to do was write code using MUMPS to see if it even knew what that coding language was, and it did. It wasn't very long, or even that efficient, but it worked, and was probably better than what I could have written for the prompt I gave.

Once I got my new job, I inexplicably switched to using Gemini, and found out how to generate python code, and from that, create a janky, terminal-based tool to parse through JSON files and output pipe-delimited text that you could copy & paste into excel. It was also the last time that I was able to completely understand the entirety of an LLM-generated code file.

Finally, when I got my third tech job, I got access to Claude Code, and in one single prompt, it was able to turn my measly little python script into a full combo of HTML, CSS, and JavaScript files that could work in any browser and do the same thing as the python tool and more. It took a few more vibe-code sessions to work out the kinks and to add a darkmode toggle, but it was 1000x better than having to open up the terminal every time I needed to do my job.

When each version generated by a new Claude session would break, and my team-members kept asking for the latest version of the tool, I realized I could kill two birds with one stone by making a github account to track version updates, and actually host it on a free Github Page (you can still get to it here).

That's when that 4th-grade question, which had been buried so deep in the back of my mind it had almost been forgotten, found its way back into the front. I just made a website. It was mine and I owned it and people could go there to do stuff. Granted, it was void of any personality and served the noble and corporate purpose for making work more productive, but goddammit, it was my productive website.

The whole ordeal made me want to host a bigger, better, and more personal site, one that could do anything I came up with, and have no real limitations besides how fast it could run. And so, my (google) search began.

The problem I quickly found with all of the usual suspects of services that offer "Free" websites is that they're all really "Freemium", meaning that they get you hooked with their ease of setup, and a breadth of content, but there will always come a point where you will want another feature, or more space, and the only way to get it is to spend a certain amount every month for the rest of time.

I didn't want any of that. I wanted something that could exist and do anything I wanted, all without a subscription. I started asking not just the original question of "How can I make a website", but rather, "How far can I get building a fully functioning and capable website today, all while knowing almost zero about server infrastructure or website UI design, and spending zero dollars to do so?"

It turns out, further than I could have ever imagined.

The site you are reading this on isn't just a basic static landing page. It has a markdown blog, a photo gallery with camera metadata, custom browser tools (Photo Resizer, JSON Formatter, Word Counter), its own Content Management System admin backend, a SQLite voting database, and a background Git worker that auto-deploys updates every 60 seconds.

And the total cost to get it all up and running: $2

Here is the exact setup (as written by the AI that made the site, this whole preface was 100% me):


Finding a Free Server on Oracle Cloud

Before we start, one piece of vocabulary, because everything else depends on it.

A server is just a computer that stays turned on and answers requests from the internet. That's the whole idea. Your laptop could technically be one, but it sleeps, it moves around, and your home internet connection changes its address whenever it feels like it. So instead you rent a computer that lives in a data center and never sleeps. That rental is called a VPS — a Virtual Private Server. "Virtual" because you're not renting a whole physical machine, you're renting a slice of one, carved out and handed to you as if it were your own.

This whole project kicked off after watching a guide on getting one of those for free:

Source video: How to Run Your Own VPS for Free Forever (Oracle Cloud)

Oracle runs something called an Always Free tier. Not a trial — actually free, indefinitely. You get 2 ARM CPUs, 12 GB of RAM, 100 GB of storage, and 10 TB of monthly bandwidth.

For scale: that is more memory than most laptops sold today. For a personal website, it is comically overpowered. You could run this site fifty times over on that hardware.

The problem: you hit a wall about ten minutes in.

Oracle's free pool is almost always out of capacity. I tried to create an ARM instance for hours. Every single attempt came back with the same out-of-capacity error. Not a bug, and not something you did wrong — the free hardware is genuinely all claimed, and Oracle puts free-tier requests in a separate, permanently clogged queue.

If you stop here, you'll conclude the free tier is a myth. It isn't. You just need the workaround.


Step 1: Getting Oracle to Actually Give You the Server

The solution was mentioned almost in passing in the video transcript: upgrade your account to Pay As You Go (PAYG).

That sounds like exactly the thing we're trying to avoid. It isn't, and here's why it works:

  1. You add a real payment card to the account.
  2. Oracle runs a temporary $100 pre-authorization hold. This is an identity check, not a charge — the same thing a hotel or gas pump does. The money never leaves your account, and the hold releases on its own within a few days.
  3. Upgrading moves you out of the clogged free-tier provisioning queue and into the primary infrastructure pool — the one with actual capacity in it.
  4. As long as your instance stays inside the Always Free resource limits, Oracle bills you exactly $0.

The free tier limits don't change when you upgrade. You're not giving up the free allowance; you're just no longer stuck in the slow lane to get it. My instance was provisioned within minutes of the upgrade going through.

Here's the machine I ended up with:

  • Operating System: Ubuntu 24.04 LTS (Minimal, ARM aarch64)
  • CPU: 2 OCPUs (Ampere Altra, ARM)
  • Memory: 12 GB RAM
  • Storage: 100 GB

A note on Ubuntu: your server doesn't run Windows or macOS, it runs Linux, and Ubuntu is the friendliest and most widely documented flavor of it. There's no desktop, no mouse, no windows — you interact with it entirely by typing commands. That sounds intimidating and it is, for about a day. Then it just becomes normal.

Do this before you do anything else: go into Oracle Billing and create a $1.00 monthly budget alert. If your usage ever ticks above zero for any reason, you get an email immediately. This is the safety net that makes the whole "add a real credit card" step comfortable. Mine has sat at $0.00 the entire time.


Step 2: Getting Into the Server

Once the instance exists, Oracle gives you an IP address — four numbers separated by dots, like 123.45.67.89. That's the server's street address on the internet.

To actually get inside it, you use SSH (Secure Shell). SSH is a way of opening a text-only remote control session on a computer somewhere else. You type a command on your laptop, it runs on the server in the data center, and the output comes back to you. That's it. That's the entire relationship you'll have with this machine.

Oracle hands you a key file when you create the instance — download it and don't lose it, because it's the only way in. Unlike a password, you can't reset it later.

From that point on, every configuration step in this article happens inside that SSH session.


Step 3: Giving It a Real Web Address

An IP address works fine for SSH, but nobody is going to type 123.45.67.89 into a browser to read your blog.

The problem: you need a domain name, and domain names cost money every year, forever.

The solution: DNS, which is the internet's phone book. Its only job is translating a name a human can remember into an IP address a computer can route to. When someone types a domain into their browser, DNS is what quietly turns it into a number.

Most domains you pay a registrar for annually. But some services hand out free subdomains — a name that lives underneath a domain they already own. I registered one on DuckDNS:

  • Subdomain: droidzero.duckdns.org
  • Points to: the public static IPv4 address Oracle assigned my instance

Setup is about two minutes: sign in, pick a name, paste in your IP. It costs nothing and it never expires.

The tradeoff is honest: you get yourname.duckdns.org instead of yourname.com. If you want a bare domain later, you buy one and re-point it — nothing else in your setup has to change, because everything downstream only cares about the IP.


Step 4: Opening the Right Doors (Firewalls and Ports)

Here's a concept that trips up almost everyone at this stage.

A server doesn't just have one entrance. It has ports — numbered doors, 65,535 of them, and different kinds of traffic knock on different numbers. Web traffic goes to port 80, secure web traffic goes to 443, SSH goes to 22. A firewall is the bouncer deciding which doors are open at all.

The problem: by default, nearly all of them are shut. You will set your web server up perfectly, type your address into a browser, and get an infinite loading spinner with no error message and no clue why.

The extra problem, and this is the one that eats an afternoon: Oracle servers have two firewalls stacked in front of each other.

  1. Oracle's Virtual Cloud Network (VCN) rules — configured in the Oracle web console, out at the network edge.
  2. Ubuntu's own ufw firewall — configured over SSH, on the machine itself.

Traffic has to get past both. Open a port in one and forget the other and it fails exactly the same way as opening neither: silently. If something isn't reachable and you're sure the software is running, this is very likely the reason.

I opened four ports in both places:

  • Port 22 — SSH, so I can get in
  • Port 80 — regular HTTP web traffic
  • Port 443 — HTTPS, encrypted web traffic
  • Port 5000 — the internal Python service, covered below

Step 5: Serving the Website (Nginx and HTTPS)

Now the machine is reachable. It still isn't serving anything, because nothing on it is listening for visitors yet.

The problem: something has to receive an incoming request, figure out what's being asked for, and send back the right file.

The solution: a web server. I used Nginx (pronounced "engine-x"), which is the standard workhorse for this and is very good at it. Nginx sits on ports 80 and 443, waits for requests, and serves my HTML, CSS, and image files out of a folder — /var/www/html/.

That alone gets you a working website. But there are two more things worth understanding.

HTTPS, and why you can't skip it

Notice the padlock in your browser's address bar. That's HTTPS — the connection between the visitor and my server is encrypted, so nobody in between can read or tamper with it.

To get one, you need an SSL certificate: a file that proves your server really is the one that owns droidzero.duckdns.org. These used to cost money. Now Let's Encrypt issues them free, and a tool called Certbot fetches one for you, installs it into Nginx, and quietly renews it before it expires. It's one command, and then you never think about it again.

Skipping this isn't really an option anymore — modern browsers show a "Not Secure" warning on plain HTTP that will scare off every visitor you have.

Reverse proxying, which sounds harder than it is

Some parts of a site can't be plain files. A "like" button has to record something. A contact form has to store what was typed. That requires a program running and thinking, not a static file being handed over.

So I have a small Python program running separately, listening on port 5000. Nginx is configured to split incoming traffic:

  • Anything starting with /api/ → forwarded to the Python program on port 5000
  • Everything else → served straight from the folder as a static file

That forwarding arrangement is called a reverse proxy. From the visitor's side there's only one website at one address. Behind the curtain, two different programs are splitting the work, and Nginx is the one deciding who handles what.

This is the single most useful pattern in the entire build, and once it clicks, a lot of "how do real websites work" suddenly makes sense.


Step 6: Building It Without Writing Everything By Hand

I did not hand-write thousands of lines of boilerplate JavaScript, CSS layout logic, and SQL. I used AI pair programming (vibe-coding, if you want to be a stickler for terms) with Google Gemini, Gemini 3.5 Pro, and Claude Opus 4.6, inside the Antigravity IDE.

Plan first, then build

The most valuable thing I did was spend time talking through the architecture with Gemini before asking for any code. Three decisions came out of that, and each one saved me from a mess later:

  • How do I keep the front end light? Plain HTML, vanilla CSS, and vanilla JavaScript. No React, no build step, no frameworks. Fewer moving parts, nothing to update, and the pages load instantly.
  • How do I write posts without editing code every time? Use Decap CMS, backed by GitHub login. (More on what that means in Step 7.)
  • How do I store things like votes and form submissions? A local SQLite database — which is just a single file on disk — managed by a small Python backend. No separate database server to run, secure, or pay for.

If you're starting out, copy this order. Deciding what you're building is cheap. Rebuilding it because you skipped that part is not.

Hitting the wall: the 80s Soviet programmer method

The problem: building with Opus 4.6 and Gemini 3.5 Pro was fast, but as Uncle Ben almost said — with great power comes great usage. Within two hours, every week, I hit my free tier cap.

That left a choice: stop working for six days, or change how I worked.

The solution came from an odd place. I adapted my routine to match how programmers worked in the 1980s Soviet Union, where machine time was scarce enough to be measured in minutes. Sergey Aleynikov — who learned to program in the USSR and later wrote trading code at Goldman Sachs — put it plainly: "In Russia, time on the computer was measured in minutes. When you write a program, you are given a tiny time slot to make it work."

So the thinking happened first. Programmers wrote code and logic flowcharts out longhand in paper notebooks and traced them by hand, because debugging was the one thing you couldn't afford to do on the machine itself.

To be fair, none of this was uniquely Soviet — paper-first coding was simply how the punch-card era worked, West included. The USSR just kept at it long after everyone else had terminals.

I did the same thing in markdown:

  • During the six-day lockout, I wrote specifications, bug reports, and feature ideas in my notes app. No computer required — this is thinking work, not typing work.
  • When the weekly reset hit, I'd first tell the agent "continue" to finish whatever it was mid-way through.
  • Then I'd paste the entire note in at once. The agents built complete features without burning quota on back-and-forth debugging.

The counterintuitive result: the constraint made the output better. Unlimited access encourages vague prompting and lazy iteration. Rationed access forces you to actually know what you want before you ask for it.

Later I picked up an Antigravity promo tier at $2/month for three months, which is the entire cost of this project. But the bigger lesson stands: you do not need a paid agent plan for any of this. The free tier is fully capable. It's just slower, and slower turns out to be survivable.


Step 7: Publishing Without Touching the Server

Here's the last problem, and it's the one that decides whether a site stays alive or quietly dies after three posts.

The problem: if writing a blog post means SSH-ing into a server, editing a file by hand, and restarting something, you will stop writing blog posts. Friction always wins eventually.

The solution has three pieces, and together they're my favorite part of the build.

1. GitHub holds the truth. Git is a system that tracks every version of every file in a project; GitHub is a free website that stores those projects online. My entire site lives in a GitHub repository. That repo — not the server — is the official version of the site.

2. Decap CMS gives me an editor. A CMS (Content Management System) is just an admin screen for writing posts instead of editing raw files. Decap CMS is unusual in that it has no backend of its own — it runs in the browser, I log in with my GitHub account, and when I hit publish it commits the new post straight to the repository. I write in a normal text box; GitHub receives a commit.

3. The server pulls changes to itself. A background thread inside my Python program checks GitHub every 60 seconds. When it sees new commits, it pulls them down and resets the site files to match (git fetch && git reset --hard origin/main).

So the full loop is: I write a post in a browser → it lands in GitHub → within 60 seconds the server notices and updates itself → it's live. I never touch the server. There is no deploy button. It just happens.


What's Actually Running Behind the Curtain

If you're curious about the moving pieces, here's the whole thing in one place:

  1. Python Flask API (app.py) — runs as a systemd service (droidzero-api.service). Systemd is Linux's program manager: it starts the service on boot and restarts it automatically if it crashes, so the site survives reboots without me doing anything. The API handles GitHub OAuth logins for Decap CMS (/api/auth, /api/callback) and writes photo likes and contact form submissions to a local SQLite database (votes.db).
  2. The auto-sync Git daemon — the 60-second polling thread from Step 7, living inside app.py.
  3. Client-side tools — a canvas-based Photo Resizer, a JSON Formatter, and a live Word Counter. All of these run entirely in your browser with zero dependencies, which means they cost me no server resources at all no matter how many people use them.
  4. The blog and gallery — markdown files rendered on the fly with parsed frontmatter. The photo gallery reads EXIF metadata straight out of the image files (camera, lens, aperture, ISO) and links to the Darkroom store.

What It Costs Every Month

ComponentProvider / ToolMonthly Cost
Server computeOracle Cloud PAYG (2 OCPUs, 12 GB RAM, Ubuntu ARM)$0.00
Domain & DNSDuckDNS (droidzero.duckdns.org)$0.00
SSL certificateLet's Encrypt / Certbot$0.00
Web server & APINginx + Python Flask + SQLite$0.00
Content managementGitHub + Decap CMS$0.00
AI agent runnerAntigravity promo tier$2.00
Total$2.00 / month

And that $2.00 was optional. Every piece of infrastructure here is free. The only thing I paid for was going faster.


Wrapping Up

If you want to try this yourself, the honest order is:

  1. Get the Oracle instance (upgrade to PAYG so it'll actually provision), and set the budget alert.
  2. SSH in.
  3. Point a free DuckDNS subdomain at it.
  4. Open ports 22, 80, 443 in both firewalls.
  5. Install Nginx, run Certbot for HTTPS.
  6. Put your site in a GitHub repo and have the server pull from it.

Everything after that is decoration — and decoration is the fun part.

You don't need expensive cloud hosting, bloated frameworks, or huge monthly SaaS subscriptions to run a fast, functional website. Nearly every hard problem here already has a free, well-documented solution; the difficulty is almost never the technology, it's not knowing the solution exists yet.

You just need to be stubborn, keep asking the right questions, and be willing to learn.

Enjoyed this post?

If you found this write-up helpful, consider buying me a coffee to support future articles, experiments, and open-source tools.

Buy me a coffee