Declarative Deployment of OwnTracks Frontend with NixOS

Posted on Wed 16 April 2025 in self-hosting

This a followup to my previous post about setting up OwnTracks on a home server running NixOS. One step of that process was to download the OwnTracks Frontend single page application zip archive and copy it into /var/www/html/owntracks to be served by Nginx. Now I'd like to look at replacing this procedure with a declarative approach by defining the resource in the server's configuration.nix.

An Aside: Google Drops the Ball

It turns out my concerns about Google's Map Timeline service were prescient. Last month, a "technical issue" resulted in the loss of users' Timeline data. Luckily, I had the back-up-to-cloud option turned on and was able to recover my Timeline history. Once I had done that, I exported the data from the app for safekeeping. At some point I might try converting and importing that data into OwnTracks.

Writing a Nix Derivation

The first step to the declarative deployment is to write a Nix derivation that packages the Frontend application. The principled thing to do would probably be referencing the source repo and building the application as part of the derivation. But since this is a Javascript project built using Vite, I was afraid doing that might be complicated. Also, I am primarily interested in a reproducible server deployment rather than the reproducibility of the package itself. By that logic I decided to base the derivation on the prebuilt artifact from the repo's releases.

Here the goal will be to write a owntracks-frontend.nix derivation and a default.nix to invoke it so that executing nix-build -A owntrack-frontend generates a result link to a Nix store path containing the contents of the OwnTracks Frontend package. To do this I followed the nix.dev tutorial on packaging existing software.

In the first iteration I created two files with the following contents.

# owntracks-frontend.nix
{
  stdenv,
  fetchzip
}:
stdenv.mkDerivation {
  pname = "owntracks-frontend";
  version = "v2.15.3";
  src = fetchzip {
    url = "https://github.com/owntracks/frontend/releases/download/v2.15.3/v2.15.3-dist.zip";
    sha256 = "";
  };
}

And,

# default.nix
let
  pkgs = import <nixpkgs> { config = {}; overlays = []; };
in
{
  owntracks-frontend = pkgs.callPackage ./owntracks-frontend.nix { };
}

Then executing nix-build -A owntracks-frontend produces the expected error regarding the incorrect hash.

error: hash mismatch in fixed-output derivation '/nix/store/im2lmhh4a2h7x87plz9i1fsc5fw8vhyf-source.drv':
         specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
            got:    sha256-iy+yISPcOD/2lTyJUb1eI3wufLku1mKfVDm0+Dy8OKk=
error: 1 dependencies of derivation '/nix/store/bm08ivlk0nqc61kz6vallfndbq2xn5ly-owntracks-frontend-v2.15.3.drv' failed to build

This error gives the correct hash so it can be put into the derivation.

  src = fetchzip {
    url = "https://github.com/owntracks/frontend/releases/download/v2.15.3/v2.15.3-dist.zip";
    sha256 = "iy+yISPcOD/2lTyJUb1eI3wufLku1mKfVDm0+Dy8OKk=";
  };

Now, Nix will accept the source, and the build proceeds up to the next error.

error: builder for
'/nix/store/csw1kq323p6lipcwbs9q109rf85vbx1n-owntracks-frontend-v2.15.3.drv'
failed to produce output path for output 'out' at 
'/nix/store/csw1kq323p6lipcwbs9q109rf85vbx1n-owntracks-frontend-v2.15.3.drv.chroot/root/nix/store/br9k7nxj067cmza64s5r92f2zchrj5vx-owntracks-frontend-v2.15.3'

This means that the derivation is not creating an output directory which should contain the result. That's because this package isn't a normal Autotools build. All that really needs to happen, with one caveat I'll get to, is to copy the files that were downloaded as the source into the output directory. That can be accomplished by overriding the install phase as described in the tutorial.

# owntracks-frontend.nix
{
  stdenv,
  fetchzip
}:
stdenv.mkDerivation {
  pname = "owntracks-frontend";
  version = "v2.15.3";
  src = fetchzip {
    url = "https://github.com/owntracks/frontend/releases/download/v2.15.3/v2.15.3-dist.zip";
    sha256 = "iy+yISPcOD/2lTyJUb1eI3wufLku1mKfVDm0+Dy8OKk=";
  };

  installPhase = ''
    runHook preInstall
    cp -r . $out
    runHook postInstall
  '';
}

Now, nix-build -A owntrack-frontend is able to succeed, and the result link points to a Nix store path containing the expected files.

$ nix-build -A owntracks-frontend
this derivation will be built:
  /nix/store/n07d9lsy8g4h8q7b69i51511ddfnqpq7-owntracks-frontend-v2.15.3.drv
building '/nix/store/n07d9lsy8g4h8q7b69i51511ddfnqpq7-owntracks-frontend-v2.15.3.drv'...
Running phase: unpackPhase
unpacking source archive /nix/store/nwca1gys4hl68cnslmiakcjaqkl8v612-source
source root is source
Running phase: patchPhase
Running phase: updateAutotoolsGnuConfigScriptsPhase
Running phase: configurePhase
no configure script, doing nothing
Running phase: buildPhase
no Makefile or custom buildPhase, doing nothing
Running phase: installPhase
Running phase: fixupPhase
shrinking RPATHs of ELF executables and libraries in /nix/store/4z9iardq8amf6f54azfznphy2dbb76fh-owntracks-frontend-v2.15.3
checking for references to /build/ in /nix/store/4z9iardq8amf6f54azfznphy2dbb76fh-owntracks-frontend-v2.15.3...
patching script interpreter paths in /nix/store/4z9iardq8amf6f54azfznphy2dbb76fh-owntracks-frontend-v2.15.3
/nix/store/4z9iardq8amf6f54azfznphy2dbb76fh-owntracks-frontend-v2.15.3

$ ls -lL result
total 60
dr-xr-xr-x 2 root root  4096 Dec 31  1969 assets
dr-xr-xr-x 2 root root  4096 Dec 31  1969 config
-r--r--r-- 1 root root  1150 Dec 31  1969 favicon.ico
-r--r--r-- 1 root root  3647 Dec 31  1969 icon-180x180.png
-r--r--r-- 1 root root   718 Dec 31  1969 index.html
-r--r--r-- 1 root root   377 Dec 31  1969 manifest.json
-r--r--r-- 1 root root 36117 Dec 31  1969 OwnTracks.svg

The caveat I mentioned is that the config directory needs to include a config.js file to set up the frontend app. As described in the previous post, for my purposes it needs to contain the following:

window.owntracks = window.owntracks || {};
window.owntracks.config = {
  api: {
    baseUrl: "http://wimpy.bleak-moth.ts.net:8083"
  },
  router: {
    basePath: "owntracks"
  }
};

This can be accomplished using the pkgs.writeText library function which puts some arbitrary text into a file in the Nix store. That file can then be referenced later in the derivation. So, I added such an attribute to the mkDerivation call and then, in the install phase, copied the generated file into the appropriate place in the $out directory. Notice, this entails adding writeText to the argument list of the function.

# owntracks-frontend.nix
{
  stdenv,
  writeText,
  fetchzip
}:
stdenv.mkDerivation {
  pname = "owntracks-frontend";
  version = "v2.15.3";
  src = fetchzip {
    url = "https://github.com/owntracks/frontend/releases/download/v2.15.3/v2.15.3-dist.zip";
    sha256 = "iy+yISPcOD/2lTyJUb1eI3wufLku1mKfVDm0+Dy8OKk=";
  };

  config = writeText "config.js" ''
    window.owntracks = window.owntracks || {};
    window.owntracks.config = {
      api: {
        baseUrl: "http://wimpy.bleak-moth.ts.net:8083"
      },
      router: {
        basePath: "owntracks"
      }
    };
  '';

  installPhase = ''
    runHook preInstall
    cp -r . $out
    cp $config $out/config/config.js
    runHook postInstall
  '';
}

Rebuilding the package and checking its contents shows the config.js file is present and has the right content.

$ nix-build -A owntracks-frontend
these 2 derivations will be built:
  /nix/store/dacc8ma93fl60qvp359nqb0b9liv5v57-config.js.drv
  /nix/store/adpvald53kxzhcfmm840xdvfydbn88yw-owntracks-frontend-v2.15.3.drv
.
.
.

/nix/store/dbml7xvlqv2lgcjfb3s0y17m5nj9bdmi-owntracks-frontend-v2.15.3

$ cat result/config/config.js 
window.owntracks = window.owntracks || {};
window.owntracks.config = {
  api: {
    baseUrl: "http://wimpy.bleak-moth.ts.net:8083"
  },
  router: {
    basePath: "owntracks"
  }
};

Making Use of the Derivation

At this point I'm able to build a directory in the Nix store which contains the SPA that I want to serve with Nginx. If done manually this would involve updating the Nginx configuration with a location block pointing to the path in the Nix store. Obviously, I want to accomplish this declaratively through configuration.nix in my NixOS server.

I wasn't immediately sure how to go about this. I knew that I could refer to derivations in pkgs using string interpolation and get the path to a package in the Nix store. I knew Nix would make sure the package was present in the store and build or download it if necessary. So if a pkgs.owntracks-frontend existed, I could do something like this:

  services.nginx = {
      enable = true;
      virtualHosts."wimpy" = {
        root = "/var/www/html";
        locations."/owntracks/" = {
          alias = "${pkgs.owntracks-frontend}/";
        };
      };
    };

Obviously, there is no pkgs.owntracks-fronted. But, I do now have owntracks-frontend.nix which contains a function that will produce the same kind of derivation as those in pkgs if invoked correctly. I reasoned that that is what default.nix is doing with the pkgs.callPackage invocation, and so maybe I could just do the same thing in my configuration.nix.

I copied owntracks-frontend.nix to be next to configuration.nix in my NixOS config repo and updated the latter with the following:

  services.nginx =
    let
      owntracks-frontend = pkgs.callPackage ./owntracks-frontend.nix { };
    in {
      enable = true;
      virtualHosts."wimpy" = {
        root = "/var/www/html";
        locations."/owntracks/" = {
          alias = "${owntracks-frontend}/";
        };
      };
    };

And, it worked!

Final Thoughts

I feel pretty happy with this accomplishment. It was really pretty simple, but I feel like I'm starting to get an inkling of how things in NixOS work.

One improvement that could be made, I think, is parameterizing the values in the config.js file. Right now the /owntracks/ base path is duplicated in the Nginx config and config.js. And surfacing the base URL for the API in the configuration.nix would also make more sense. Presumably, this could be accomplished by passing the values as parameters to the function in owntracks-frontend.nix. But I'll save that for later.