Making A Stateful Webapp on a $7.68 per Month Budget

Backends Are Useful, But Scary

A backend is part of a website that handles user data. Most websites need a backend because they need to be dynamic—it's not enough to show the same page to every visitor. Visitors need to interact with the website: whether it's checking out an order on Amazon or posting a hot take on Twitter.

This blog, however, has no backend. It shows the same page to every visitor, and will not handle any user data. It is static. This reduces my hosting costs because the entire blog can be cached in a free CDN (Content Delivery Network) 1 like Cloudflare, and needs nothing else to run.

Some of my other projects taking this approach include:

These only rely on static content (mainly javascript) to provide complex functionality.

Historically, I've avoided making webapps with backends for my hobby projects. A backend comes with all sorts of baggage: databases, managing user data, authentication, backups, and increased server load. This complexity adds cost to a project: both in time and hosting costs. So I've avoided them when possible.

Normally, when I couldn't get away with providing just a static frontend, I've avoided the issue by writing software meant to be run by the user on their own computer, rather than a website. But this limits the accessibility of my projects to technical users who are familiar with linux and a command line interface. A webapp, however, is usable to anyone with a phone or computer.

When my girlfriend pitched me an idea for a webapp to help her organize her life and help compete with the digital distractions that weren't bringing her value, I decided to face my apprehension of backends.

We decided to call this project Make Me Touch Grass.

Make Me Touch Grass activities page

Tools

To get the project up and running quickly, I reached for tools that were familiar to me: Python, NextJS, PostgreSQL, and Docker.

Backend

To further accelerate development, I relied on the Python web framework FastAPI 2, which provides most of the base functionality needed for setting up a backend. I had no prior experience working with this framework, but found it quite beneficial.

The FastAPI OAuth2 tutorial 3 was especially helpful for quickly setting up authentication for my backend. There's no room for error in this part of the system because even a minor flaw could expose private user data. By following this guide, I quickly set up API endpoints for registering new users, logging in, and retrieving user data. With FastAPI, this was done in only 100 lines of Python.

Frontend

NextJS 4 provides an opinionated framework built on top of React, allowing the frontend codebase to scale with added complexity. I prefer using NextJS over Create React App (another library for setting up React projects), because it has better performance for static deployments.

Database

While developing, I started out just using in-memory data structures for the API at first, then I moved to SQLite, a single-file relational database. Using a relational database allows the backend to safely modify data concurrently, and also stores the data more persistently than in-memory data structures.

Then I moved to PostgreSQL after I became concerned over concurrency constraints of using SQLite. I haven't used SQLite much professionally, but my understanding is that because it relies on a single database file, it is better suited to single-user applications than something with many concurrent users making writes to the database. Some people claim "SQLite is the only database you will ever need in most cases" 5, but I was skeptical of this claim when it comes to creating a highly concurrent webapp backend.

Reverse Proxy

I'm using nginx as a reverse proxy for the system so that the frontend and backend can be accessed at the same domain. This helps mitigate CORS (Cross Origin Resource Sharing) issues, because there is only one origin used.

The config for nginx is fairly simple, with just a few location definitions under one server:

worker_processes 1;
 
events { worker_connections 1024; }
 
http {
    index    index.html;
    root /usr/share/nginx/html/;
    include mime.types;

    sendfile on;
 
    server {
        resolver 127.0.0.11;
        listen 8080 default_server;

        location /api/  {
            proxy_pass      http://backend:4000/;
        }

        location / {
            proxy_pass      http://frontend:80/;
        }
    }
}

This tells nginx to forward all requests to the frontend docker container except requests starting with /api, which are sent to the backend.

Docker

Because of the numerous dependencies I'm using, Docker was useful for tying the whole thing together and manage dependencies. I'm also using docker compose to manage the networking between services. By default docker-compose exposes all services under the same network, allowing them to communicate. This is why the nginx can reference the services as http://backend, where backend is the name of the service.

The docker compose file to support this is also minimalist:

services:
    database:
        build: database
        restart: always
        environment:
            - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
            - POSTGRES_USER=postgres
            - POSTGRES_DB=postgres
        volumes:
            - database-volume:/var/lib/postgresql/data

    reverseproxy:
        build: reverseproxy
        ports:
            - 8080:8080
        restart: always

    frontend:
        build: web
        depends_on:
            - reverseproxy
        restart: always

    backend:
        depends_on:
            - reverseproxy
            - database
        environment:
            - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
        build: backend
        restart: always

volumes:
  database-volume:
    driver: local
    driver_opts:
      type: 'none'
      o: 'bind'
      device: '${PWD}/postgres_data'

Code

I removed all the application-specific parts of the repo and uploaded them here, so anyone wanting to build a full stack webapp powered by the same tools can get a head start following the same approach I did. I haven't decided what license to release Make Me Touch Grass under, but for now, the project is private. I may release it as open source, like most of my projects, but I'll leave that option open for now.

Deployment

To actually push this project out into the world, I had to set up some infrastructure. I chose a $7/month Digital Ocean droplet that bought me just 1 GB of RAM and a single virtual intel CPU. I'm uncertain if this will be enough to run the project with a real user base, but for now, this is sufficient and within my budget.

I also bought a domain name (makemetouchgrass.com) for $0.68/month from NameCheap.

To decrease latency and increase reliability for the website, I'm using Cloudflare to cache the static assets on a CDN. Cloudflare quite generously provides a free tier, so this isn't included in my total costs.

The base load on my server has been fairly minimal, but I also don't have many real users yet. I'll have to see how this project scales with more users, and possibly re-evaluate hosting options.

So far the only issue I've had with this limited server is running builds. Generally the CPU usage hovers at less than 5%, but during the build step, it spikes.

Digital Ocean CPU Usage

Active memory usage generally stays under 50%, but also uses a good chunk for cache. I suspect this is the bottleneck for my project.

htop memory usage

As a result of these constraints, I have to take the server down when making updates, causing downtime for users. This is the biggest limitation I've faced with such a tight budget. Alternatively, I could pre-build the frontend in a CI/CD pipeline and rsync it to my server, but this is a bit tedious with my docker setup. Then again, I don't have many users to begin with, so a little downtime can't hurt.

Generally, the build takes around 1-2 minutes and is noticeably slower than when running on my local desktop. Running NextJS's export command next export is the culprit here.

Cloud Hybrid?

I had another idea to mitigate the limited hardware available on my budget: run the actual server on prem (my home office) and forward requests from my cloud server to this desktop. This would involve running the webapp at a subdomain of my personal wcedmisten.dev domain. Then I would configure nginx running on my DigitalOcean droplet to rewrite all traffic to this subdomain.

This would help solve my memory constraints because this desktop has 128 GB of RAM. Comparatively, even the cheapest 4 GB DigitalOcean droplet is $24/month, which is more than I want to spend on this project.

This would also have the downside of limiting availability to depend on my local network and power situation, but that's probably acceptable for a hobby project.

Conclusions and future work

So far, despite my prior hesitation to make a project with a backend, the project has gone smoothly so far. The real issue is now a need to acquire users and get some feedback. If you want to check out the app, please go to makemetouchgrass.com and make a free account. I'd love to get your feedback at [email protected]. Thanks!

Footnotes

  1. https://en.wikipedia.org/wiki/Content_delivery_network

  2. https://fastapi.tiangolo.com/

  3. https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/

  4. https://nextjs.org/

  5. https://unixsheikh.com/articles/sqlite-the-only-database-you-will-ever-need-in-most-cases.html