Blog deployment via git-receive hooks

While quickly going through the options for hosting this blog I decided to just do it myself.. Serving some static content (for a small audience) isn’t that hard and I’m running a few (mostly idle) machines anyway.

My requirements were just to have IPv6, some way to tune the webserver config and some kind of “CI” that converts the website from markdown to HTML (preferably with Nix).

Actually pushing the content to a server is trivial if you already have SSH access. So I could use either rsync or SSH to do it. Since the blog is already in a git repo I wanted to treat the target server as a git remote. Git has a bunch of built-in hooks that can be used for many things. In this case I was interested in the post-receive hook.

Pushing the content (in it’s raw format) to a server is already trivial if you have SSH access. I would just commit and push the latest version to another remote.

For building the static assets I could be using git post-receive hooks that just call nix-build … -o $documentRoot.

And that is what I did.

All that was required to make this happen was adding a new file to the NixOS configuration of the server this is running on:

{ config, pkgs, ... }:
  home = "/var/lib/blog";

  env = pkgs.buildEnv {
    name = "post-receive-env";
    paths = [

  post-receive = pkgs.writeShellScript "post-receive" ''
    export PATH=${env}/bin
    set -ex

    GIT_DIR=$(git rev-parse --git-dir 2>/dev/null)
    if [ -z "$GIT_DIR" ]; then
            echo >&2 "fatal: post-receive: GIT_DIR not set"
            exit 1

    TMPDIR=$(mktemp -d)
    function cleanup() {
      rm -rf "$TMPDIR"
    trap cleanup EXIT

    git clone "$GIT_DIR" "$TMPDIR"
    unset GIT_DIR
    cd "$TMPDIR"
    exec nix-build "$TMPDIR" -A build -o ${home}/current

  config = {
    services.nginx.virtualHosts."" = {
      enableACME = true;
      forceSSL = true;
      root = "/var/lib/blog/current";
    }; = {
      inherit home;
      createHome = true;
      isNormalUser = true;
      openssh = config.users.users.andi.openssh;
    };"blog-prepare-git-repo" = {
      wantedBy = [ "" ];
      path = [
      script = ''
        set -ex
        cd ${home}
        chmod +rX ${home} # no secrets in that home dir, everyone can read my public blog
        test -e repo || git init --bare repo
        ln -nsf ${post-receive} repo/hooks/post-receive
      serviceConfig = {
        Kind = "one-shot";
        User = "blog";