The Wobserver is currently in my living room running on my Homelab Build, so how do you view these pages? Surely my home IP address is not blasted on to the web via DynDNS, right? Right.
The Edge Server is a small VM co-located with AS64241 in beautiful Seattle, Washington. Yours could be on AWS, or DigitalOcean, or Hetzner, or whatever, man; it could also be your home IP address blasted on to the web via DynDNS, but it doesn't need to be. The Edge Server can establish an encrypted connection to the Wobserver using Tailscale. SSL can be terminated by an NGINX and proxied to the Nginx on the far side, or transparently proxied for termination within the Wobserver.
Base NixOS Server Manifest
This thing is "just" an Arroyo System, it pulls its configuration from my Knowledge Base.
{ config, pkgs, lib, ... }:
{
services.nginx.on-retreat.enable = false;
services.nginx.on-retreat.returnDate = "2 November";
services.tailscale.useRoutingFeatures = "server";
networking.firewall.logRefusedConnections = false;
imports = [
# <<arroyo_nixos_imports()>>
../nixos/akkoma-media-proxy.nix
];
home-manager.users.rrix.imports = [
# <<arroyo_home-manager_imports()>>
];
system.stateVersion = lib.mkDefault "24.11";
home-manager.users.rrix = {
home.stateVersion = config.system.stateVersion;
# don't ship pinentry-qt
services.gpg-agent.pinentryPackage = lib.mkForce pkgs.pinentry-curses;
};
services.prometheus.exporters.node = {
enable = true;
enabledCollectors = [ "systemd" ];
};
services.openssh.enable = true;
environment.systemPackages = with pkgs; [
pkgs.htop
];
}Minimizing the Edge Server's NixOS Closure Size
{ lib, pkgs, ... }:
{The edge server is a 10gb-disk VM that only ever receives pushed deployments; it does not build things and nothing runs nixpkgs#... there.
The flake-registry pin copies the full nixpkgs source into the closure (measured ~200MB / 90k+ inodes on this input).
nixpkgs.flake.setFlakeRegistry = false;
nixpkgs.flake.setNixPath = false;Cut everything that ships docs, man pages, or pulls the nixpkgs source into the closure.
documentation.enable = false;
documentation.nixos.enable = false;
documentation.man.enable = false;
documentation.doc.enable = false;defaultPackages pull grub/micro-code/etc.
environment.defaultPackages = [];growpart needs cloud-utils + python3 in the closure; autoResize already grows the filesystem post-build, and the partition table is baked into the qcow2 at the size it will ever be. mkForce because hosts/king-mountain sets it true for the generic image.
boot.growPartition = lib.mkForce false;nixos-rebuild + installer tooling pulls nixos-rebuild-ng (python3 runtime, ~140MB) into the closure; deployments are pushed from the build host, this VM never rebuilds itself.
system.disableInstallerTools = lib.mkForce true;keep the store from holding old generations on a small disk; nixpkgs.nix sets 30d for big hosts, edge wants aggressive gc.
nix.gc.options = lib.mkForce "--delete-older-than 7d"; i18n.supportedLocales = [ "en_US.UTF-8/UTF-8" "en_GB.UTF-8/UTF-8" "C.UTF-8/UTF-8" ];home-manager ships a manual + reference manpages + mime-info in its user env by default; none of that is useful on a headless VM. and don't ship pinentry-qt
home-manager.users.rrix = {
manual.manpages.enable = false;
manual.html.enable = false;
manual.json.enable = false;
xdg.mime.enable = false;
xdg.mimeApps.enable = false;
programs.man.enable = false;
services.gpg-agent.pinentryPackage = lib.mkForce pkgs.pinentry-curses;
};
}Helpers
local modules = arroyo.nixos_modules("edge")
local lines = {}
for i, m in ipairs(modules) do
table.insert(lines, "../../" .. m)
end
return table.concat(lines, "\n")local modules = arroyo.home_modules("edge")
local lines = {}
for i, m in ipairs(modules) do
table.insert(lines, "../../" .. m)
end
return table.concat(lines, "\n")The Edge Server's Base Nginx Forwarding configuration
The Edge Server mostly exists to serve My HTTP services and My Websites from The Wobserver to the web from a stable public IP address.
It's set up as a VM King Mountain right now but can quickly come up anywhere you can get a NixOS. Build a global content distribution network on the back of Tailscale's peering agreements, today!
{ config, pkgs, lib, ... }:
let
enabled = config.services.nginx.on-retreat.enable;
returnDate = config.services.nginx.on-retreat.returnDate;
retreatPage = pkgs.replaceVars ../files/on-retreat.html {
inherit returnDate;
};
in
{
options.services.nginx.on-retreat = with lib; {
enable = mkEnableOption { name = "turn off sites while offline"; };
returnDate = mkOption { type = types.str; default = ""; };
};
config = {
networking.firewall.allowedTCPPorts = [ 80 22 443 ];
services.logrotate = {
enable = true;
settings.nginx = {
frequency = "daily";
rotate = 365;
compress = true;
delaycompress = true;
};
};
services.nginx.virtualHosts."fontkeming.fail" = {
serverAliases = [
<<mkVirtualHosts()>>
];
forceSSL = true;
locations = lib.mkMerge [
# Retreat mode: serve the 503 page
(lib.mkIf enabled {
"/" = {
return = "503";
extraConfig = ''
error_page 503 /503.html;
'';
};
})
# Normal mode: proxy everything
(lib.mkIf (!enabled) {
"/" = {
proxyPass = "http://last-bank";
extraConfig = ''
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header Host $host;
proxy_read_timeout 600s;
proxy_connect_timeout 600s;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
'';
};
})
{
"=/503.html" = {
alias = retreatPage;
extraConfig = ''
internal;
add_header Content-Type text/html;
'';
};
}
];
};
};
}On Retreat page
<!DOCTYPE html>
<html>
<head>
<title>The Arcology Site Network is temporarily offline</title>
</head>
<body>
<h1>Ryan Rix is offline and AFK right now</h1>
<p>They will be returning from a meditation retreat on <b>@returnDate@</b> and his sites will be offline until then. Thank you for understanding.</p>
<p>Please contact <a href="https://bluecliffzen.org">Blue Cliff Zen Center</a> if you need to get ahold of them despite this. Be good and be good at it.</p>
</body>
</html>Helpers for Hosts
local lines = {}
for i, row in ipairs(tbl) do
table.insert(lines, '"' .. row[1] .. '"')
end
return table.concat(lines, "\n")"fontkeming.fail"
"dns.fontkeming.fail"
"docker.fontkeming.fail"
"scanner.fontkeming.fail"
"vault.whatthefuck.computer"
"kickass.systems"
"thelionsrear.com"
"arcology.garden"
"engine.arcology.garden"
"thechanceencounter.com"
"cce.whatthefuck.computer"
"whatthefuck.computer"
"files.fontkeming.fail"
"notes.whatthefuck.computer"
"i.notes.whatthefuck.computer"
"bag.fontkeming.fail"
"ring.whatthefuck.computer"
"media.whatthefuck.computer"
"feeds.whatthefuck.computer"
"dongiverse.com"
"links.fontkeming.fail"