Now For Something A Little Different

Meet Python By Night V3

October 28, 2025

Now For Something A Little Different

Meet Python By Night V3

I would have written this post sooner if I wasn't, you know, busy redesigning my website. I guess I should do more Writing By Night... 😏 ...Better late than never, I suppose.

Why

The last time I redesigned my website, I kind of left a lot undone. I was still using FastAPI and a MongoDB backend, which was also what I used when I first started.

It was... fine. I enjoyed the ability to interact with my database without the hassle of migrations, and the dictionary/json-like structure of the documents was quite approachable.

What bogged me down, though, was getting lost in some sync/async details that I never quite fleshed out.

I also intended to add some features, like a TIL section, or a place to view some of my past conference slideshows (still considering this one). Those never materialized.

I also got stuck between the version changes of Pydantic, and the code got a little hacky and not too fun to maintain.

The design was also sort of ho-hum. I could never really get it quite how I envisioned it. While I appreciated the velocity of using Tailwind, it still felt a little constrained.

So, I decided to try again.

The Stack

Without going too much into detail, here is what my old stack looked like:

  • FastAPI
  • MongoDB (Beanie ODM)
  • TailwindCSS
  • Digital Ocean
  • App Platform (deployment)

Here is what my new stack looks like:

  • Litestar
  • SQLite (Litestream)
  • CSS by hand
  • Hetzner
  • Coolify
  • Docker Compose

So, pretty much everything from scratch.

I chose Litestar because I've been using it for a few other things (both hobby and at work) and it feels ergonomic, powerful, flexible, and it just fits my head. If you're curious, here's a pretty good write up about it.

I was curious to try SQLite based on some of the work Adam Hill has been doing on the Django side to make it a more viable option.

Sure, there are a lot of footguns that come with that choice, but I thought my blog would be a good enough (and serious enough) proof of concept.

Hetzner, generally because lower cost and powerful options. I was also curious to try Coolify because of some of the recent chatter around it, and also in case I want to keep experimenting with side projectsβ€”I want a good way to manage them.

And lastly, Docker Compose because it just seems like it'll simplify building projects side by side in a VPS (if needed).

I also have htmx enabled, though I'm not really using it much (yet), as well as a self hosted version of revealjs, which should allow me to share future slideshows (from my talks) if I go down that route.

Web Framework

I'm not going to give a huge rundown here, as this process had many moving parts.

As I mentioned before, using Litestar seemed like a no-brainer to me, based mostly on my experience using it in other situations.

With Advanced Alchemy baked in, there's no need to re-implement basic CRUD (create, read, update, delete) functionality, and adding specific business logic on top of pre-existing ORM (object relational mapper) methods is straightforward.

This is a somewhat pared down version of my project layout.

src/  
β”— app/  
  ┣ controllers/  
┃ ┃ ┣ api/  
┃ ┃ ┣ __init__.py  
┃ ┃ β”— web.py  
  ┣ db/  
  ┣ lib/  
  ┣ models/  
┃ ┃ ┣ content/  
┃ ┃ ┣ __init__.py  
┃ ┃ ┣ base.py  
┃ ┃ ┣ rss.py  
┃ ┃ β”— slug.py  
  ┣ services/  
┃ ┃ ┣ content/  
┃ ┃ ┣ __init__.py  
┃ ┃ ┣ deps.py  
┃ ┃ β”— serializer.py  
  ┣ static/  
  ┣ templates/  
  ┣ __init__.py  
  β”— main.py  

App settings and configuration live in the lib directory, along with anything security related (for my private API calls) or exception handling.

My database models are defined, naturally, in the models directory and are hooked up to a repository pattern using Advanced Alchemy (the database and migration configuration lives inside of the db directory).

Additional services outside of basic CRUD operations (i.e., create, get, update, delete, etc...) are defined within the services directory. (Things like list_published_posts).

The modules within models and services share same/similar names (i.e. blog.py, talk.py), which helps me keep track of each particular domain.

This then allows me to keep my routes/views fairly thin, which I like. For example, this is the view for my newly available Today I Learned page.

@get("/til", name="tils")
    async def tils(
        self,
        til_service: s.TilService,
    ) -> htmx_template:
        tils = await til_service.list_published_tils(limit=None)

        return htmx_template(
            template_name="til.html",
            context={
                "meta_title": "TILs",
                "meta_description": "TIL posts on PBN",
                "tils": tils,
            },
        )

The list_published_tils method is a custom service I added on top of the already available Advanced Alchemy list method. (Hmm, I should probably put a limit on how many objects I render on the page πŸ˜…).

Database

As mentioned earlier, I switched to using SQLite from MongoDB, which meant I needed to export my existing data (not too difficult), and have it fit into my newly defined models.

That was only mildly difficult. Since my models are defined using attrs, I was able to use cattrs for serialization/validation. It worked pretty well!

Initially, I thought I would likely spin up a Postgres instance once I was ready to deploy, but I got more and more interested in using SQLite (again, thanks in no small part to Adam Hill).

One challenging aspect of depending on SQLite, specifically in a containerized app, is the question of persistence.

If not careful, your database file could be just gone.

For replicating, I decided to go with Litestream, which copies the database file to a GCS (Google Cloud) bucket. This took a bit of trial and error, especially because the latest version of Litestream was failing with GCS. So I went with the previous version and everything worked!

But as of this writing, apparently that has been fixed!

Hetzner and Coolify

I already had an account with Hetzner, but hadn't really given it a proper go.

I wasn't necessarily unhappy with Digital Ocean. I had been using the managed app platform-as-a-service offering, which handles much of the deployment details for you, instead of starting fresh on a single droplet.

With Hetzner, I wanted to start with a fresh VPS and manage the deployment myself. You know, for practice.

Since I was already going that route, I thought to throw in Coolify, as mentioned above, and it proved to be about as challenging as expected (read: yes, challenging).

I may write a little bit more in depth about the deployment process at a later time. But needless to say, my biggest hiccup was DNS.

A Few More Thoughts

Leaving Tailwind behind was not necessarily where I started, but the allure of dropping a little bloat in exchange for skilling up on CSS by hand was enough to entice me.

At the end of the day, I feel like doing it this way opened up my design and allowed me to be a little bit more creative.

With Tailwind, I felt like I could iterate quickly and arrive at a design rather fastβ€”at the expense of something that looks a little more average.

And speaking of averages, any AI usage consisted of seldomly asking a chatbot about a deployment detail I seemed to be missing, and at least it felt like a net neutral for me (in terms of time saved).

Sometimes it would lead me astray, and I'd end up searching for a solution myself, or I'd end up reading documentation anyway to try to make sure that what it was telling me was indeed true.

I am not claiming that using AI tooling is useless or that it can't be very powerful. But I am clearly wary of the marketing and hype, positioning it as a way to solve all sorts of problems, while in turn, many more are being created.

So my dependence on those tools is minimal, if at all.

Lastly

In the old version of my blog, I had some CLI commands baked in to the app in order to publish content written in Markdown (and Frontmatter).

This was sort of fine, except I never really wanted to find a way to exec into my hosted application. So this meant I needed to spin up a local version of my blog, make sure it was connected to my production database, and then run commands to publish my content.

It was also kind of half-baked.

This time around, I created an entirely different CLI application that is more streamlined and straightforward.

It hits a protected endpoint on my live application and writes directly to the SQLite database (which is then backed up/replicated by Litestream after every write operation).

This should allow me to work on the CLI app separately if I want to introduce different interactions with the database, but without having to worry about redeploying or branching from my main application.

Oh, Also

Coolify currently only supports automated deployments if using the included GitHub integration. However, I started my blog repository over on Codeberg, because reasons.

I noticed that you can also connect private repositories from other sources (i.e., Forgejo), but you also have to include some SSH and Private/Public Key shenanigans.

I was trying to get that hooked up at some point, but I got tired of trying, so instead [I made my repository public] and called it a day. 😁

With a public repo, Coolify is able to build straight from that, using my included Docker Compose file. It's actually quite extraordinary everything that it is doing in the background.

So yeah, in case you want to poke in to see what kind of folly I've gotten myself into, have at it (and sorry for the abysmal documentation so far)!

Feel free to message me on Mastodon if you have any specific questions.