A case for simplicity, SQLite, and not overthinking your infrastructure.
I run nine Rails applications on a single Hetzner VPS. Four vCPUs, 8 GB of RAM, 40 GB of local disk. The server costs about €20 a month. Everything else, the DNS, the object storage, the email sending, the error tracking, and the uptime monitoring, runs on free tiers. The total monthly cost of running all of my side projects is roughly the price of a couple of pints.
This isn’t a flex. It’s a deliberate set of choices that I’ve arrived at over the past year or so, and I think more side-project builders should consider something similar. The default advice for deploying Rails apps has drifted towards managed services, container orchestration, and separate database servers. For a lot of side projects, that’s overkill. You don’t need it. You need a box, a database file, and a deploy command.
The stack
Here’s what I actually use:
mindmap
My Tech Stack
App
Ruby on Rails
Web application
Database
SQLite
Litestream
Back ups
UUIDv7
sqlean
fts5
Postgres - limited
Frontend
HOTWire
StimulusJS
TailwindCSS
Devops
Kamal
Server provisioning
Website
Astro
UI
Content management
Services
Hetzner
Server
Cloudflare
Domain
SSL
Object Storage - R2
Hosting
Pages/Workers
Postmark
Emails
Sentry
Logging
Error handling
Uptime monitoring
AI
Anthropic
Claude Cowork
Claude Code
Google
Gemini
Stitch
Every Rails app starts the same way:
rails new . --css tailwind -j bun --skip-spring --skip-test --asset-pipeline=propshaft --database=sqlite3
Tailwind for styling. Bun for JavaScript. Propshaft for assets. SQLite for the database. Skip Spring because I’ve never found it worth the trouble. Skip the default test framework because I add RSpec afterwards. That one command gives me a working app in about thirty seconds.
Why SQLite
The biggest decision in this stack is probably SQLite over PostgreSQL. In my day job I use Postgres without question. For side projects, the calculus is different.
The practical reason is deployment simplicity. If you have nine apps sharing a single VPS, connecting them all to one PostgreSQL server adds a layer of configuration and management that you don’t need. Each SQLite database is just a file sitting next to the application. There’s nothing to connect to, nothing to manage separately, and nothing that can go down independently of the app itself.
The things I used to rely on Postgres for, I’ve replaced piece by piece:
Backups come from Litestream, which continuously replicates the SQLite database to Cloudflare R2. If the server disappears, I can restore from the last few seconds of data. It runs in the background and I don’t think about it.
UUIDs come from sqlean, a SQLite extension library. I use UUIDv7 as primary keys across all my apps. This gives me sortable, globally unique IDs without needing Postgres’s gen_random_uuid().
Full-text search comes from fts5, which is built into SQLite. For the scale of search I need on side projects, it does the job. I used to reach for pg_search or Elasticsearch. I don’t need either.
This setup took a bit of time to arrive at. It started with plain SQLite and grew as I hit problems that Postgres would have solved out of the box. The point isn’t that SQLite is better than Postgres. It’s that for low-traffic side projects, the operational simplicity of a single file database is hard to beat.
Deploys
I use Kamal, and I deploy from my laptop. There’s no GitHub Actions workflow, no automated CI. I run kamal deploy, it builds the Docker image, pushes it to the server, and swaps the container. A typical deploy takes a couple of minutes.
That doesn’t mean I skip checks entirely. Recent versions of Rails ship with a built-in CI script that runs linting, package auditing, and tests. I run that locally before every deploy to make sure everything passes. It’s the same checks a CI server would run, just on my machine instead of in a pipeline I have to configure and maintain.
For some people this would feel too manual. For me, it’s the right trade-off. A hosted CI pipeline is another thing to configure, another thing to maintain, and another thing that can break. On a side project where I’m the only developer, the local CI script and a deploy command cover it.
The static sites
Not everything is a Rails app. My personal site (dcyoung.dev) and the Fordyce Village community website both run on Astro, hosted on Cloudflare Pages for free.
Both are static sites that pull content from other sources at build time. dcyoung.dev pulls markdown files from a separate GitHub repository and fetches bookmarks from my Bookmarker app through an API. The Fordyce Village site connects to the Community Manager app I built, which acts as a content management system for the village committee. A GitHub Action triggers a rebuild every couple of hours, so the sites stay current without any server-side rendering.
The Fordyce project is probably the best example of what this stack makes possible. Last year, the village committee asked me to help with their website. They were using a drag-and-drop editor and struggling to keep events looking consistent across pages. What started as a simple Astro rebuild quickly turned into building them a proper content management app so that committee members could add events, news, and village information without needing to know anything about code or file systems. The Rails app runs on the same €20 server as everything else. The Astro site is free on Cloudflare. The village got a better website, and I got another project on the box.
What’s on the server
The nine apps cover a range of things:
Shipshape is a cleaning marketplace for the Moray coast, connecting domestic cleaners with self-catering and holiday accommodation owners who need changeover cleaning. Tessera is a child safeguarding tool for schools, tracking pupil behaviours, incidents, and context. TriggerDM automates Instagram DM responses triggered by comment keywords. Savourly is a digital cookbook with no life stories, just recipes, built around AI extraction so I can scrape a recipe from a web page or an uploaded image. Garden Genie is a garden planning and plant care assistant. Bookmarker saves and organises links and powers the bookmarks section of dcyoung.dev. Community Manager powers the Fordyce Village website. There are a couple more in development: Lodger for independent accommodation booking, and All Square for gift card tracking.
Some of these are live with real users. Some are experimental. All of them run on the same server, all use SQLite, and all deploy with the same kamal deploy command.
The one complication with running nine apps on a single box is Docker networking. Each app needs its own Docker network so they’re completely isolated from each other. I have to set that up manually for every new app. But I’ve done it so many times now that I know exactly what to do and how to do it. It takes a few minutes, and then I don’t think about it again.
The AI workflow
The other thing that makes running this many projects practical is AI. Specifically, Claude Code with the compound engineering plugin from Every.
My workflow varies depending on what I’m building. If it’s something new, a feature I haven’t thought through yet, or maybe just a one-line idea, I start with the compound engineering brainstorm skill. It creates a document and asks questions about what to build and how to build it. That usually surfaces decisions I hadn’t considered.
From there I move to the plan skill, which I review and tweak manually. I usually come to the plan with an idea of the data model and maybe some sketches of Ruby code that may or may not work. I pass those in at the start and outline any concepts I want the plan to follow. For instance, on one project recently I wanted records to be immutable, so I came up with an approach where a duplicate record gets created every time there’s a change. Explaining that kind of thing upfront in the plan makes a real difference to the output.
Then I run the work skill, either one deliverable at a time so I can check and tweak as I go, or if the scope is tight, I let it run through the whole plan. For well-defined tasks it’s genuinely fast. For anything ambiguous, the check-and-tweak approach works better.
I’ve also been using the compound skill, which documents the reasoning behind unusual approaches so the AI knows to follow them in future sessions. That immutable objects pattern I mentioned got compounded into a document, and now the AI applies it consistently without me having to explain it again. The DHH Rails style skill is another one I lean on for design decisions.
Beyond Claude Code, I use Gemini for image generation when I need a quick logo or visual to get ideas flowing. And Stitch for putting together UI layouts and experimenting with how things might flow before I commit to building them in the app.
The manifesto part
I think the default complexity of modern web development is too high for side projects. Not because the tools are bad, but because we carry habits from our day jobs into work that doesn’t need them.
When I started learning Ruby on Rails, it was the glory days of Heroku. You could write your Rails app, deploy it straight to Heroku without any configuration, and it all ran for free. I had countless applications on the free tier. That simplicity is what got me excited about Rails in the first place, and it’s what I’ve been trying to get back to ever since Heroku stopped being free.
A managed database service makes sense when you have a team, an SLA, and customers who will notice downtime. It doesn’t make sense when you’re the only developer and the app has twelve users. A CI pipeline makes sense when multiple people push code to the same repository. It doesn’t make sense when you deploy from your laptop on a Sunday evening.
The cost argument is the easy one. Nine apps for €20 a month, with everything else on free tiers. But the real benefit is cognitive. I don’t manage infrastructure. I don’t debug deployment pipelines. I don’t coordinate database migrations across a shared server. I build features, deploy them, and move on to the next project.
That low overhead is what makes it possible to run this many projects at all. If each app needed its own Heroku dyno, its own managed Postgres, and its own CI pipeline, I’d have maybe two or three projects, not nine. The stack isn’t just cheap. It’s what lets me say yes to the next idea.
Where this breaks
I should be honest about the limits. This setup works because all nine apps are low-traffic. If any of them grew to the point where a single VPS couldn’t handle the load, I’d need to rethink things. SQLite handles concurrent reads well, but heavy concurrent writes would push me back towards Postgres. Search performance on fts5 is fine for small datasets, but it’s not going to compete with a dedicated search service at scale.
If I were working with other developers, the no-CI approach wouldn’t fly. And if any of these apps handled sensitive financial data or had serious compliance requirements, the deployment story would need to be more rigorous.
That said, I think the simplicity actually makes scaling easier, not harder. If one of these apps took off tomorrow, I could spin up a new Hetzner server, deploy the app to it the exact same way I deploy now, restore the database from a Litestream backup, and switch the whole thing over in less than an afternoon. There’s no complex migration to plan. The deploy process is the same whether it’s a shared box or a dedicated one.
But the real limit isn’t technical. It’s about what you learn. I’ve been building side projects for about ten years now. I have too many on my laptop to even count. Most of them never went anywhere. The worst thing you can do with a side project isn’t over-engineering it. It’s not deploying it, not using it, and mostly not learning from it. This stack and this approach grew out of a decade of building things, shipping some, abandoning others, and figuring out what actually matters along the way.
Ship it
Nine apps. One server. SQLite. Kamal. €20 a month. No managed services, no container orchestration, no CI pipeline. Just a box in Falkenstein and a deploy command.
If you’ve been putting off a side project because the infrastructure feels like too much work, it probably is too much work, because you’re imagining too much infrastructure. Start simpler. You can always add complexity later. You almost certainly won’t need to.