One of my favourite aspects of self-hosting with NixOS is how simple it is to deploy a multitude of services using the built-in modules, and as a result, I deploy many services.
Unfortunately, it can become quite a headache trying to keep track of so many different services all potentially running on different hosts, with different URLs and ports.
To help with this, I have tended to configure my Firefox bookmarks using the home-manager module, but it is still a somewhat tedious process to manually configure each one.
Fortunately, using the power of flakes and home-manager along with a little bit of Nix magic, it is possible to automatically generate a bookmarks folder for Firefox containing all my Nix based services, without having to manually configure anything!
Configuring an example service#
To demonstrate how this works, here is a typical example of a web service configuration (Forgejo in this case) including the nginx reverse proxy configuration:
{config, ...}: {
services.forgejo = {
enable = true;
settings = {
server = {
DOMAIN = "git.williamvandervalk.com";
ROOT_URL = "https://git.williamvandervalk.com";
HTTP_PORT = 3000;
};
};
};
services.nginx.virtualHosts."git.williamvandervalk.com" = {
locations."/".proxyPass = "http://localhost:3000";
};
}
Home-manager Firefox configuration#
The goal is to create a bookmark in the home-manager Firefox configuration, that links to our service.
Normally, bookmarks are configured manually in home-manager configuration as follows:
{
programs.firefox = {
enable = true;
profiles.default = {
bookmarks = {
force = true;
settings = {
toolbar = true;
bookmarks = [
{
name = "Services";
bookmarks =
[
{
name = "ArgoCD";
url = "https://argocd.williamvandervalk.com";
}
{
name = "Ceph";
url = "https://ceph.williamvandervalk.com";
}
{
name = "Git";
url = "https://git.williamvandervalk.com";
}
]
}
];
};
};
};
};
}
This will create a bookmarks folder in the bookmarks toolbar named “Services”, containing the “ArgoCD”, “Ceph” and “Git” bookmarks.
Passing the flake as argument to home-manager#
To automate this process, it is necessary to somehow collect all instances of
config.services.nginx.virtualHosts."service.williamvandervalk.com"
configured
in the flake.
To do this, simply pass the flake itself (i.e. self
) from flake.nix
as an
extraSpecialArg
to home-manager.
This will make it possible to use
self.outputs.nixosConfigurations.<host>.config.services.nginx.virtualHosts
to
parse the virtualHosts configured for a specific host in the same flake.
flake.nix#
{
description = "NixOS configuration";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05";
home-manager = {
url = "github:nix-community/home-manager/release-25.05";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
inputs@{
nixpkgs,
home-manager,
self,
...
}:
{
nixosConfigurations = {
hostname = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
./configuration.nix
home-manager.nixosModules.home-manager
{
home-manager = {
useGlobalPkgs = true;
useUserPackages = true;
users.jdoe = ./home.nix;
# Pass flake outputs to home-manager
extraSpecialArgs = {
inherit inputs self;
};
};
}
];
};
};
};
}
Automatically creating bookmarks#
Now that the flake itself has been passed as an argument to home-manager, it is possible to generate the list of bookmarks for all services that have a corresponding nginx virtualHost automatically using the following Nix expression:
{
lib,
self,
...
}:
{
programs.firefox = {
enable = true;
profiles.default = {
bookmarks = {
force = true;
settings = {
toolbar = true;
bookmarks = [
{
name = "Services";
bookmarks =
[
{
name = "ArgoCD";
url = "https://argocd.williamvandervalk.com";
}
{
name = "Ceph";
url = "https://ceph.williamvandervalk.com";
}
]
++
# Create bookmarks for all nginx virtualHosts in this flake
lib.mapAttrsToList
(virtualHost: _virtualHostConfig: {
name = virtualHost;
url = "https://${virtualHost}";
})
(
lib.concatMapAttrs (
_host: osConfig: osConfig.config.services.nginx.virtualHosts
) self.outputs.nixosConfigurations
);
}
];
};
};
};
};
}
Here is the result of the initial expression:
As you can see, it is functional, but not exactly elegant. Notice that it successfully picked up the configured Forgejo instance, as well as several other virtualHosts, including a Minio instance that I have configured.
Explanation#
Breaking down what is going on in the previous Nix expression, there is an initial list of manually defined bookmarks:
...
[
{
name = "ArgoCD";
url = "https://argocd.williamvandervalk.com";
}
{
name = "Ceph";
url = "https://ceph.williamvandervalk.com";
}
]
...
The attribute set of all virtualHosts configured for all hosts in the flake is created using:
...
(
lib.concatMapAttrs (
_host: osConfig: osConfig.config.services.nginx.virtualHosts
) self.outputs.nixosConfigurations
);
...
This is used as the input for lib.mapAttrsToList
which constructs the list
items corresponding to each virtualHost. The resulting list is then concatenated
to the list of manually defined bookmarks:
...
[
{
name = "ArgoCD";
url = "https://argocd.williamvandervalk.com";
}
{
name = "Ceph";
url = "https://ceph.williamvandervalk.com";
}
]
++
lib.mapAttrsToList
(virtualHost: _virtualHostConfig: {
name = virtualHost;
url = "https://${virtualHost}";
})
(
lib.concatMapAttrs (
_host: osConfig: osConfig.config.services.nginx.virtualHosts
) self.outputs.nixosConfigurations
);
...
Cleaning up result#
Trimming domain from names#
The previous result is functional, but a little bit ugly. To improve it,
lib.strings.removeSuffix
can be used to trim the domains from the bookmark
names:
{
lib,
self,
...
}:
{
programs.firefox = {
enable = true;
profiles.default = {
bookmarks = {
force = true;
settings = {
toolbar = true;
bookmarks = [
{
name = "Services";
bookmarks =
[
{
name = "ArgoCD";
url = "https://argocd.williamvandervalk.com";
}
{
name = "Ceph";
url = "https://ceph.williamvandervalk.com";
}
]
++
# Create bookmarks for all nginx virtualHosts in this flake
lib.mapAttrsToList
(virtualHost: _virtualHostConfig: {
name = lib.strings.removeSuffix ".williamvandervalk.com" virtualHost;
url = "https://${virtualHost}";
})
(
lib.concatMapAttrs (
_host: osConfig: osConfig.config.services.nginx.virtualHosts
) self.outputs.nixosConfigurations
);
}
];
};
};
};
};
}
Here is the result of the improved expression, after trimming domains from the bookmark names:
Filtering out unwanted virtualHosts#
This is an improved result, but there are still some extra bookmarks (e.g. the
root domain, “www”) that might be nice to remove, which is achieved using
builtins.removeAttrs
:
{
lib,
self,
...
}:
{
programs.firefox = {
enable = true;
profiles.default = {
bookmarks = {
force = true;
settings = {
toolbar = true;
bookmarks = [
{
name = "Services";
bookmarks =
[
{
name = "ArgoCD";
url = "https://argocd.williamvandervalk.com";
}
{
name = "Ceph";
url = "https://ceph.williamvandervalk.com";
}
]
++
# Create bookmarks for all nginx virtualHosts in this flake
lib.mapAttrsToList
(virtualHost: _virtualHostConfig: {
name = lib.strings.removeSuffix ".williamvandervalk.com" virtualHost;
url = "https://${virtualHost}";
})
(
builtins.removeAttrs
(lib.concatMapAttrs (
_host: osConfig: osConfig.config.services.nginx.virtualHosts
) self.outputs.nixosConfigurations)
[
"default"
"localhost"
"s3.williamvandervalk.com"
"williamvandervalk.com"
"www.williamvandervalk.com"
]
);
}
];
};
};
};
};
}
Here is the result of the improved expression, after filtering unwanted virtualHosts:
Capitalizing bookmark names#
Almost perfect! To polish it a little more, define a custom function that will capitalize the first character of each name:
{
lib,
self,
...
}:
let
# Helper function to capitalize the first character of a string
capitalize =
str: lib.toUpper (builtins.substring 0 1 str) + (lib.removePrefix (builtins.substring 0 1 str) str);
in
{
programs.firefox = {
enable = true;
profiles.default = {
bookmarks = {
force = true;
settings = {
toolbar = true;
bookmarks = [
{
name = "Services";
bookmarks =
[
{
name = "ArgoCD";
url = "https://argocd.williamvandervalk.com";
}
{
name = "Ceph";
url = "https://ceph.williamvandervalk.com";
}
]
++
# Create bookmarks for all nginx virtualHosts in this flake
lib.mapAttrsToList
(virtualHost: _virtualHostConfig: {
name = capitalize (lib.strings.removeSuffix ".williamvandervalk.com" virtualHost);
url = "https://${virtualHost}";
})
(
builtins.removeAttrs
(lib.concatMapAttrs (
_host: osConfig: osConfig.config.services.nginx.virtualHosts
) self.outputs.nixosConfigurations)
[
"williamvandervalk.com"
"www.williamvandervalk.com"
]
);
}
];
};
};
};
};
}
Here is the final result: