Static Sites

Tech Stack of the urmaul.com Blog

At the time of writing this, urmaul.com is a static blog with privacy-friendly dynamic elements. Everything runs self-hosted on a single virtual server with an automatically updated Linux.

This is a description of every layer with some reasoning and configuration details.

Blog

No CMS is running on the server and generating pages when you request them. All the pages are pre-generated, now it's just static files served by nginx.

Benefits of static sites:

  • Fast: It's hard to serve requests faster than nginx serves static files.
  • Simple: configuring the server is easy because nothing smart is running there. No databases, and no script interpreters.
  • Secure: No one can hack the admin panel if there's no admin panel.

The downside is that every new post requires you to edit files and redeploy everything. That s fine for me but might be a blocker for non-developers.

Engine

Having a static blog means you need a tool to generate it. It would take posts saved as Markdown files and generate all the HTML files for deployment.

There are many static site generators and picking one is more a matter of taste. When creating this blog, I picked Sculpin because it's written in PHP and uses Twig templates.

This day I would probably pick something fancier, but now I don't want to migrate to another engine. When your result is a bunch of HTML files, it doesn't matter, which tool generated them.

Process

I write blog posts in Markdown files using Typora as an editor. Typora is not open source and not free but it's worth it.

I store blog post contents in the same git repository as the generator configuration.

When the blog post is ready, I run one of the two scripts:

  • preview.sh runs Sculpin in preview mode. It serves the blog on localhost and regenerates it whenever the source files change. Useful for checking the post before publishing.
  • publish.sh runs Sculpin to generate the site into the directory. It creates separate pages for each post, creates list pages, and copies resources (like images) as is. Then rsync uploads it all to the server.

Dynamic parts

Some parts of the blog can't be static. I use separate tools for that.

Statistics

Umami is a privacy-friendly self-hosted analytics tool. It collects aggregated website usage statistics but doesn't track users separately. It doesn't even use cookies. But at the same time, I can see the number of visitors, visits per page, and even visits per country.

And since Umami is self-hosted, this blog doesn't send your behavior data to the tech giants.

Comments

Remark42 is a privacy-friendly self-hosted comment engine. It doesn't track you when you're just viewing and stores the bare minimum of information when you log in to comment.

As a nice feature, it also provides an RSS feed for all the comments on the website. It's useful if the blog gets new comments even more rarely than it gets new posts.

Bringing it together

You could count three services running on the same server: the blog webserver, the analytics tool, and the comments tool. To be honest, there are even more web servers running on this instance. Each one of them is running in a Docker container and each of them is serving HTTP requests. So for each new HTTP request, the server has to decide which docker container will handle it.

Traefik as a reverse proxy

We can solve the task of routing HTTP requests with reverse proxies. I use Traefik proxy for that. It can forward requests to Docker containers and it doesn't require hardcoded configuration for that. Instead, it discovers running Docker containers and updates routing rules automatically.

That means we can add and remove applications on the same server and Traefik would make sure everything just works.

To enable this, you need to add this to the traefik.toml:

[providers.docker]
endpoint = "unix:///var/run/docker.sock"
# Auto-discover new containers
watch = true
# Containers need to opt-in to be exposed
exposedbydefault = false

When we configure specific applications, it's easier to use docker-compose even for single-container ones. We mark the container as discoverable, add a domain label, and add the container to the traefik network so Traefik can see it. Together it looks like this:

version: '3'

services:
    myapplication:
        # ...
        networks: [default, traefik]
        labels:
            # Make container discoverable
            traefik.enable: true
            # The port exposed by the application
            traefik.port: 8080
            traefik.docker.network: traefik
            traefik.passHostHeader: true
            # Routing rule like "give me everything with this hostname"
            traefik.http.routers.myapplication.rule: "Host(`${VHOST}`)"
            # Enable HTTPS
            traefik.http.routers.myapplication.tls: true
            traefik.http.routers.myapplication.tls.certresolver: myresolver

networks:
  traefik:
    external: true

The tls labels enable HTTPS for this application. Let's look at it closer.

HTTPS with Let's Encrypt

Traefik doesn't only forward requests. It can also obtain and renew HTTPS certificates using Let's Encrypt and even redirect from http:// to https://.

We need to set up the certificate manager once in the traefik.toml and then we can connect specific applications with a couple of labels on the Docker container.

The certificate manager configuration looks like this:

[entryPoints]
  [entryPoints.web]
    address = ":80"

    # Enable redirection from http:// to https://
    [entryPoints.web.http]
      [entryPoints.web.http.redirections]
        [entryPoints.web.http.redirections.entryPoint]
          to = "websecure"
          scheme = "https"

  [entryPoints.websecure]
    address = ":443"
    [entryPoints.websecure.http.tls]
      certResolver = "myresolver"

# Enable ACME (Let's Encrypt): automatic SSL.
[certificatesResolvers.myresolver.acme]
  email = "my-email@domain.com"
  storage = "my-certificate-storage.json"
  [certificatesResolvers.myresolver.acme.httpChallenge]
    entryPoint = "web"

Server

The server is a usual $6 DigitalOcean VPS. It hosts several tiny pet projects.

The operating system on that server is Flatcar Container Linux. That's a Linux distro with no package manager and major parts of configuration made immutable. The only thing it can do is run Docker containers. Sounds horrible at a first glance but it lets Flatcar install updates automatically without the risk of breaking the system. And that turns Flatcar into an operating system that doesn't require maintenance. So no five-year-old Ubuntu on pet servers anymore.

What's next

This stack feels like a good setup for lazy blogging. Perfect when the average blog post publish rate is around once per year. And it feels appropriate for libre software folks.

There's a downside, though. It's hard to follow updates on such a blog. You won't check it every day to see if there's something new. And it's not a part of a blog platform like Medium that takes care of notifying subscribers.

There is an obligatory Atom feed but the audience of people using RSS and Atom is pretty small.

Right now I'm announcing every new post on my Fediverse and Twitter accounts but that feels too manual.

I'm looking into ActivityPub. Would be nice if people could see and comment on new posts right in their Mastodon clients. But I'm not sure it's worth moving away from static files.

Comment if you have ideas on how to implement it.


Tags: , , , ,

Comments