{ config, username, hostname, pkgs, lib, nix-index-database, inputs, specialArgs, ... }: let ddnsPkg = import ./invoke-ddns {inherit pkgs;}; startSeq = builtins.fromJSON ''"\u001b[7m"''; # Start inverted color endSeq = builtins.fromJSON ''"\u001b[27m"''; # End inverted color motd = "${startSeq} Welcome to the Peterson Mainframe! Look, touch, but DO NOT LICK. ${endSeq}"; nixPkgs = specialArgs.nixPkgs; ourRustVersion = pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.complete); vncContext = builtins.path { path = ./vnc-desktop; name = "vnc-desktop-context"; }; ourRustPlatform = nixPkgs.makeRustPlatform { rustc = ourRustVersion; cargo = ourRustVersion; }; pullomaticPkg = import ./pullomatic { inherit lib pkgs; rustPlatform = ourRustPlatform; specialArgs = {}; }; pullomatic = "${pullomaticPkg}/bin/pullomatic"; in { imports = [ (import ./cloned_repos {inherit pkgs pullomatic lib;}) (import ./firewall.nix {inherit pkgs;}) (import ./nginx.nix {inherit pkgs lib config;}) (import ./system/users.nix {inherit pkgs config lib nix-index-database;}) ]; time.timeZone = "America/Anchorage"; age.secrets = { nearlyfreespeech = { file = ./secrets/nearlyfreespeech.age; owner = "root"; }; webdav = { file = ./secrets/webdav.age; owner = "root"; }; anthropic-api-key = { file = ./secrets/anthropic-api-key.age; owner = "root"; }; postmark = { file = ./secrets/postmark.age; owner = "root"; }; forgejo-runner-token = { file = ./secrets/forgejo-runner-token.age; owner = "root"; }; vnc-password = { file = ./secrets/vnc-password.age; owner = "root"; }; vnc-htpasswd = { file = ./secrets/vnc-htpasswd.age; owner = "nginx"; }; }; environment.systemPackages = [ ddnsPkg pullomaticPkg pkgs.vim pkgs.php pkgs.rustc pkgs.cargo pkgs.util-linux pkgs.iotop pkgs.rust-bin.stable.latest.default pkgs.wget pkgs.tmux pkgs.unstable.claude-code ]; swapDevices = [ { device = "/swapfile"; size = 1 * 1024; # 1GB } ]; virtualisation.arion = { backend = "podman-socket"; projects.forgejo.settings = import ./arion/arion-compose.nix; projects.riverside.settings = import ./arion-riverside/arion-compose.nix; }; # The arion NixOS module sets backend = "podman-socket" but doesn't inject # DOCKER_HOST into the service units; docker CLI falls back to /var/run/docker.sock # (no daemon). Point it at the podman-compatible socket instead. systemd.services.arion-forgejo.environment.DOCKER_HOST = "unix:///run/podman/podman.sock"; systemd.services.arion-riverside.environment.DOCKER_HOST = "unix:///run/podman/podman.sock"; # Build the VNC desktop image locally from the Dockerfile — no registry push/pull needed. # vncContext is a Nix store path that changes whenever any file under vnc-desktop/ changes, # which causes build-vnc-image to re-run and podman-vnc-desktop to restart on nixos-rebuild. systemd.services.build-vnc-image = { description = "Build VNC desktop container image from Dockerfile"; wantedBy = [ "podman-vnc-desktop.service" ]; before = [ "podman-vnc-desktop.service" ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; ExecStart = pkgs.writeShellScript "build-vnc-image" '' STAMP=/var/lib/build-vnc-image/context-hash EXPECTED="${vncContext}" if [ -f "$STAMP" ] && [ "$(cat "$STAMP")" = "$EXPECTED" ]; then echo "VNC image is up to date, skipping build" exit 0 fi echo "Building VNC desktop image..." ${pkgs.podman}/bin/podman build \ -t forge.quinefoundation.com/ironmagma/vnc-desktop:latest \ ${vncContext} mkdir -p "$(dirname "$STAMP")" echo "$EXPECTED" > "$STAMP" ''; }; }; systemd.services.podman-vnc-desktop.restartTriggers = [ "${vncContext}" ]; services.gitea-actions-runner.instances."ubuntu" = { enable = true; name = "ubuntu"; url = "http://localhost:3000"; tokenFile = config.age.secrets.forgejo-runner-token.path; labels = [ "ubuntu-latest:host" "ubuntu-22.04:host" "ubuntu-20.04:host" ]; }; users.users.gitea-runner = { isSystemUser = true; group = "gitea-runner"; extraGroups = [ "podman" ]; home = "/var/lib/gitea-runner"; createHome = true; }; users.groups.gitea-runner = {}; systemd.services.gitea-runner-ubuntu = { after = [ "arion-forgejo.service" ]; wants = [ "arion-forgejo.service" ]; environment.PATH = lib.mkForce ( "${pkgs.podman}/bin:${pkgs.git}/bin:${pkgs.nodejs}/bin:/run/current-system/sw/bin:/run/wrappers/bin" ); serviceConfig = { DynamicUser = lib.mkForce false; User = lib.mkForce "gitea-runner"; Group = lib.mkForce "gitea-runner"; }; }; systemd.tmpfiles.rules = [ "d /home/ironmagma/.config 0755 ${username} users" "d /root/.config 0755 ${username} users" "d /var/riverside/files 0755 root root" "d /var/riverside/postgres 0755 root root" "d /var/lib/gitea-runner/ubuntu 0755 gitea-runner gitea-runner" ]; networking.hostName = "${hostname}"; # FIXME: change your shell here if you don't want zsh programs.zsh.enable = true; programs.zsh.shellInit = '' export ANTHROPIC_API_KEY=$(cat ${config.age.secrets.anthropic-api-key.path}) ''; environment.pathsToLink = ["/share/zsh"]; environment.shells = [pkgs.zsh]; environment.enableAllTerminfo = true; security.sudo.wheelNeedsPassword = false; users.motd = motd; system.stateVersion = "22.05"; virtualisation.podman = { enable = true; defaultNetwork.settings.dns_enabled = true; }; # KDE Plasma spawns many threads (and obexd loops without Bluetooth hardware). # The libpod scope for each container inherits systemd's DefaultTasksMax (~9286); # raise machine.slice to infinity so podman scopes aren't capped. systemd.slices.machine.sliceConfig.TasksMax = "infinity"; virtualisation.containers.containersConf.settings.containers.pids_limit = 0; virtualisation.oci-containers = { backend = "podman"; containers = { # Example: # "hello" = { # autoStart = true; # image = "nginxdemos/hello"; # #user = "root:jellyfin"; # volumes = [ # ]; # ports = ["8081:80"]; # }; "vnc-desktop" = { autoStart = true; image = "forge.quinefoundation.com/ironmagma/vnc-desktop:latest"; environmentFiles = [ config.age.secrets.vnc-password.path ]; volumes = [ "/root/.ssh:/root/host-ssh:ro" ]; ports = [ "127.0.0.1:6080:6080" ]; extraOptions = [ "--add-host=hetzner-host:host-gateway" "--pids-limit=-1" ]; }; "navidrome" = { autoStart = true; environment = { "TZ" = "America/Anchorage"; "PUID" = "1000"; "PGID" = "100"; "ND_SCANSCHEDULE" = "1h"; "ND_LOGLEVEL" = "info"; "ND_SESSIONTIMEOUT" = "24h"; "ND_BASEURL" = ""; }; ports = ["4533:4533"]; volumes = [ "/var/navidrome/data:/data" "/var/navidrome/music:/music:ro" ]; image = "deluan/navidrome"; }; "nextcloud" = { autoStart = true; image = "quineglobal/ubuntu-with-ssh@sha256:64210887d48fae65bc4552503bf2d21a750ba0417ada530fd31254a8cc916746"; # image = "nextcloud/28-apache@sha256:ed95d344718ec86df96886b4b3465a9ce553c08b44b47306d399f0f201b04cb3"; volumes = [ ]; environment = { }; ports = ["8087:80"]; }; # "ubuntu" = { # autoStart = true; # image = "quineglobal/ubuntu-with-ssh@sha256:64210887d48fae65bc4552503bf2d21a750ba0417ada530fd31254a8cc916746"; # volumes = [ ]; # environment = {}; # ports = ["222:22"]; # }; "sync.io" = { autoStart = true; image = "quineglobal/sync.io@sha256:cbb180301fde42d8d22c26c952a4d4a487469d6491465302d8d79ebf194813b3"; volumes = [ "/var/syncio-cache:/sync.io-cache" ]; environment = {}; ports = ["9090:8080"]; user = "0"; # run as root }; "blog-quine" = { autoStart = true; image = "quineglobal/blog-quine@sha256:88097e4867a99a375db490bf7a989c122653cdb48bfdf6d9ad5e2f6a0bfb2d38"; volumes = []; environment = {}; ports = ["3010:8080"]; }; "hyper-quineglobal-com" = { autoStart = true; image = "forge.quinefoundation.com/ironmagma/hyper-quineglobal-com@sha256:d90132b3b60eb2ae73aa463b1b09677c1bc9b33b62c3e18430d47eb192f105eb"; volumes = []; environment = {}; ports = ["3013:8081"]; }; "coldairnetworks" = { autoStart = true; image = "quineglobal/coldairnetworks-com:latest"; volumes = []; environment = { POSTMARK_SERVER_TOKEN = "e718a146-c590-4550-a750-a3b925056e29"; }; environmentFiles = [ config.age.secrets.postmark.path ]; ports = ["3012:8081"]; }; }; }; nix = { settings = { trusted-users = [username]; accept-flake-config = true; auto-optimise-store = true; }; registry = { nixpkgs = { flake = inputs.nixpkgs; }; }; nixPath = [ "nixpkgs=${inputs.nixpkgs.outPath}" "nixos-config=/etc/nixos/configuration.nix" "/nix/var/nix/profiles/per-user/root/channels" ]; package = pkgs.nixVersions.stable; extraOptions = ''experimental-features = nix-command flakes''; gc = { automatic = true; options = "--delete-older-than 7d"; }; }; # HTTPS security.acme = { acceptTerms = true; defaults.email = "peterson@sent.com"; certs."philippeterson.com" = { dnsProvider = "nearlyfreespeech"; environmentFile = config.age.secrets."nearlyfreespeech".path; webroot = null; }; certs."webdav.philippeterson.com" = { dnsProvider = "nearlyfreespeech"; environmentFile = config.age.secrets."nearlyfreespeech".path; webroot = null; }; }; # Break the systemd ordering cycle that deadlocks nixos-rebuild switch. # The cycle: nginx → After → acme-{philippeterson,webdav}.com (DNS challenge) # → Wants → nginx-config-reload → After → acme-coldairnetworks (HTTP webroot) # → After → nginx # DNS-challenge certs don't need nginx running to provision; nginx only needs the # selfsigned fallback cert before real certs arrive. Remove the real ACME services # from nginx's After so the HTTP-webroot chain doesn't complete the loop. systemd.services.nginx.after = lib.mkForce [ "network.target" "acme-selfsigned-coldairnetworks.com.service" "acme-selfsigned-www.coldairnetworks.com.service" "acme-selfsigned-forge.quinefoundation.com.service" "acme-selfsigned-hyper.quineglobal.com.service" "acme-selfsigned-pdxdestiny.com.service" "acme-selfsigned-www.pdxdestiny.com.service" "acme-selfsigned-philippeterson.com.service" "acme-selfsigned-www.philippeterson.com.service" "acme-selfsigned-riverside.coldairnetworks.com.service" "acme-selfsigned-vnc.quinefoundation.com.service" "acme-selfsigned-webdav.philippeterson.com.service" ]; }