diff --git a/nixos/aliases.zsh b/nixos/aliases.zsh new file mode 100644 index 0000000..c1b1470 --- /dev/null +++ b/nixos/aliases.zsh @@ -0,0 +1,24 @@ +alias gco='git checkout' +alias gb='git branch' +alias gs='git status' +alias gpcb='git push origin $(git rev-parse --abbrev-ref HEAD)' + +function git() { + if [[ $1 == clone && $# -ge 2 ]]; then + local url=$2 + # expand foo/bar -> https://github.com/foo/bar.git (no protocol, no dots in host part) + if [[ $url =~ ^[A-Za-z0-9_-]+/[A-Za-z0-9_.-]+$ ]]; then + url="https://github.com/${url%.git}.git" + echo "Cloning $url" >&2 + fi + command git clone "$url" "${@:3}" + else + command git "$@" + fi +} + +alias u='cd ..' +alias uu='cd ../..' +alias uuu='cd ../../..' +alias uuuu='cd ../../../..' +alias uuuuu='cd ../../../../..' diff --git a/nixos/arion-paperless/arion-compose.nix b/nixos/arion-paperless/arion-compose.nix new file mode 100644 index 0000000..063baec --- /dev/null +++ b/nixos/arion-paperless/arion-compose.nix @@ -0,0 +1,74 @@ +{ pkgs, ... }: +{ + project.name = "paperless"; + + networks.paperless.external = false; + + services = { + redis = { + service = { + image = "redis:7"; + container_name = "paperless-redis"; + restart = "unless-stopped"; + networks = ["paperless"]; + volumes = [ + "/var/paperless/redis:/data" + ]; + }; + }; + + db = { + service = { + image = "postgres:16"; + container_name = "paperless-db"; + restart = "unless-stopped"; + networks = ["paperless"]; + volumes = [ + "/var/paperless/postgres:/var/lib/postgresql/data" + ]; + environment = { + POSTGRES_DB = "paperless"; + POSTGRES_USER = "paperless"; + POSTGRES_PASSWORD = "paperless"; + }; + healthcheck = { + test = ["CMD-SHELL" "pg_isready -U paperless -d paperless"]; + interval = "5s"; + timeout = "5s"; + retries = 20; + }; + }; + }; + + webserver = { + service = { + image = "ghcr.io/paperless-ngx/paperless-ngx:latest"; + container_name = "paperless-ngx"; + restart = "unless-stopped"; + networks = ["paperless"]; + depends_on = ["db" "redis"]; + ports = ["127.0.0.1:8000:8000"]; + volumes = [ + "/var/paperless/data:/usr/src/paperless/data" + "/var/paperless/media:/usr/src/paperless/media" + "/var/paperless/export:/usr/src/paperless/export" + "/var/paperless/consume:/usr/src/paperless/consume" + ]; + environment = { + PAPERLESS_REDIS = "redis://redis:6379"; + PAPERLESS_DBHOST = "db"; + PAPERLESS_DBNAME = "paperless"; + PAPERLESS_DBUSER = "paperless"; + PAPERLESS_DBPASS = "paperless"; + # FIXME: replace with your actual domain + PAPERLESS_URL = "https://paperless.bobs-home.com"; + PAPERLESS_TIME_ZONE = "America/Chicago"; + PAPERLESS_OCR_LANGUAGE = "eng"; + USERMAP_UID = "1000"; + USERMAP_GID = "1000"; + }; + env_file = ["/run/agenix/paperless"]; + }; + }; + }; +} diff --git a/nixos/arion-paperless/arion-pkgs.nix b/nixos/arion-paperless/arion-pkgs.nix new file mode 100644 index 0000000..1d16305 --- /dev/null +++ b/nixos/arion-paperless/arion-pkgs.nix @@ -0,0 +1,3 @@ +import { + system = "x86_64-linux"; +} diff --git a/nixos/arion-pluto/arion-compose.nix b/nixos/arion-pluto/arion-compose.nix new file mode 100644 index 0000000..1b6a4f2 --- /dev/null +++ b/nixos/arion-pluto/arion-compose.nix @@ -0,0 +1,30 @@ +{ pkgs, ... }: +{ + project.name = "pluto"; + + networks.pluto.external = false; + + services = { + app = { + service = { + image = "julia:1.11"; + container_name = "pluto"; + restart = "unless-stopped"; + networks = ["pluto"]; + command = [ + "sh" + "-c" + "julia -e 'using Pkg; Pkg.add(\"Pluto\"); import Pluto; Pluto.run(host=\"0.0.0.0\", port=1234, launch_browser=false)'" + ]; + volumes = [ + "/var/pluto/notebooks:/notebooks" + "/var/pluto/julia-depot:/root/.julia" + ]; + environment = { + JULIA_NUM_THREADS = "auto"; + }; + ports = ["127.0.0.1:1234:1234"]; + }; + }; + }; +} diff --git a/nixos/arion-pluto/arion-pkgs.nix b/nixos/arion-pluto/arion-pkgs.nix new file mode 100644 index 0000000..1d16305 --- /dev/null +++ b/nixos/arion-pluto/arion-pkgs.nix @@ -0,0 +1,3 @@ +import { + system = "x86_64-linux"; +} diff --git a/nixos/arion-riverside/arion-compose.nix b/nixos/arion-riverside/arion-compose.nix new file mode 100644 index 0000000..6225e5f --- /dev/null +++ b/nixos/arion-riverside/arion-compose.nix @@ -0,0 +1,55 @@ +{ pkgs, ... }: +{ + # Example multi-service web app: WordPress + MySQL. + # Replace the image and env vars for your own application. + project.name = "riverside"; + + networks.riverside.external = false; + + services = { + app = { + service = { + # FIXME: replace with your actual application image + image = "wordpress:6-php8.2-apache"; + container_name = "my-webapp"; + restart = "unless-stopped"; + networks = ["riverside"]; + environment = { + WORDPRESS_DB_HOST = "db"; + WORDPRESS_DB_NAME = "wordpress"; + WORDPRESS_DB_USER = "wordpress"; + WORDPRESS_DB_PASSWORD = "wordpress"; + }; + volumes = [ + "/var/riverside/files:/var/www/html/wp-content" + ]; + ports = ["3011:80"]; + depends_on = ["db"]; + }; + }; + + db = { + service = { + image = "mysql:8"; + container_name = "my-webapp-db"; + restart = "unless-stopped"; + networks = ["riverside"]; + environment = { + MYSQL_DATABASE = "wordpress"; + MYSQL_USER = "wordpress"; + MYSQL_PASSWORD = "wordpress"; + MYSQL_RANDOM_ROOT_PASSWORD = "1"; + }; + volumes = [ + "/var/riverside/db:/var/lib/mysql" + ]; + healthcheck = { + test = ["CMD-SHELL" "mysqladmin ping -h localhost"]; + interval = "5s"; + timeout = "5s"; + retries = 20; + }; + }; + }; + }; +} diff --git a/nixos/arion-riverside/arion-pkgs.nix b/nixos/arion-riverside/arion-pkgs.nix new file mode 100644 index 0000000..1d16305 --- /dev/null +++ b/nixos/arion-riverside/arion-pkgs.nix @@ -0,0 +1,3 @@ +import { + system = "x86_64-linux"; +} diff --git a/nixos/arion/arion-compose.nix b/nixos/arion/arion-compose.nix new file mode 100644 index 0000000..29d3073 --- /dev/null +++ b/nixos/arion/arion-compose.nix @@ -0,0 +1,35 @@ +{ pkgs, ... }: +{ + project.name = "forgejo"; + networks = { + forgejo = { + external = false; + }; + }; + services = { + server = { + image = { + enableRecommendedContents = true; + }; + service = { + image = "codeberg.org/forgejo/forgejo:9"; + container_name = "forgejo"; + environment = { + USER_UID = "1000"; + USER_GID = "1000"; + }; + restart = "always"; + networks = ["forgejo"]; + volumes = [ + "/var/forge:/data" + "/etc/timezone:/etc/timezone:ro" + "/etc/localtime:/etc/localtime:ro" + ]; + ports = [ + "3000:3000" + "2200:22" + ]; + }; + }; + }; +} diff --git a/nixos/arion/arion-pkgs.nix b/nixos/arion/arion-pkgs.nix new file mode 100644 index 0000000..109fcb1 --- /dev/null +++ b/nixos/arion/arion-pkgs.nix @@ -0,0 +1,10 @@ +## This file is the default location for Arion to look for Nixpkgs when +## bootstrapping a deployment configuration. +#import ../ {} + +# Instead of pinning Nixpkgs, we can opt to use the one in NIX_PATH +import { + # We specify the architecture explicitly. Use a Linux remote builder when + # calling arion from other platforms. + system = "x86_64-linux"; +} diff --git a/nixos/cloned_repos/default.nix b/nixos/cloned_repos/default.nix index f597719..12947d2 100644 --- a/nixos/cloned_repos/default.nix +++ b/nixos/cloned_repos/default.nix @@ -16,24 +16,22 @@ interval: interval: 10m credentials: - private_key: /root/.ssh/id_rsa.pem - private_key_path: true + username: git + private_key: + file: /root/.ssh/id_rsa.pem ''; }; + # FIXME: replace these with your own repositories configFiles = [ (makeConfigFile - ["com" "philippeterson"] - "git@github.com:philip-peterson/philippeterson.com.git" - "master") + ["com" "bobs-home"] + "git@github.com:bob-smith/bobs-website.git" + "main") (makeConfigFile - ["com" "quinefoundation" "blog"] - "git@github.com:philip-peterson/blog.git" - "master") - (makeConfigFile - ["atcsim"] - "git@github.com:philip-peterson/ATC-Sim.git" - "master") + ["com" "bobs-global"] + "git@github.com:bob-smith/landing-page.git" + "main") ]; configDir = @@ -42,7 +40,6 @@ } '' mkdir -p $out - # Loop over the config files and write each one to $out ${lib.concatStringsSep "\n" (map (cf: '' echo "${cf.text}" > $out/${cf.name} chmod 0644 $out/${cf.name} @@ -59,7 +56,7 @@ in { Restart = "always"; RestartSec = "0"; User = "root"; - Group = "root"; + Group = "repo-data"; }; }; diff --git a/nixos/firewall.nix b/nixos/firewall.nix index 845d4cb..d67f4b9 100644 --- a/nixos/firewall.nix +++ b/nixos/firewall.nix @@ -1,3 +1,21 @@ {pkgs, ...}: { - networking.firewall.allowedTCPPorts = [80 22 443]; + networking.firewall.allowedTCPPorts = [ + 80 # nginx/http + 22 # ssh + 443 # ssl + + 2200 # forgejo ssh + 3000 # forgejo http + + 8082 # webdav + ]; + + # Allow DNS from all podman bridge networks (10.89.0.0/16). + # NixOS auto-adds a rule for podman0 but not for networks created by + # docker-compose/arion (podman1, podman2, …). + networking.firewall.extraCommands = '' + iptables -I nixos-fw -s 10.89.0.0/16 -p udp --dport 53 -j nixos-fw-accept + iptables -I nixos-fw -s 10.89.0.0/16 -p tcp --dport 53 -j nixos-fw-accept + iptables -I nixos-fw -s 10.89.0.0/16 -p tcp --dport 5901 -j nixos-fw-accept + ''; } diff --git a/nixos/flake.nix b/nixos/flake.nix index d5b38e4..722ccbb 100644 --- a/nixos/flake.nix +++ b/nixos/flake.nix @@ -1,8 +1,7 @@ { description = "NixOS configuration"; - # 24.05 - inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-23.11"; + inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11"; inputs.nixpkgs-unstable.url = "github:nixos/nixpkgs/nixos-unstable"; inputs.agenix.url = "github:ryantm/agenix"; @@ -13,7 +12,7 @@ inputs.nixpkgs.follows = "nixpkgs"; }; - inputs.home-manager.url = "github:nix-community/home-manager/release-23.11"; + inputs.home-manager.url = "github:nix-community/home-manager/release-24.11"; inputs.home-manager.inputs.nixpkgs.follows = "nixpkgs"; inputs.nur.url = "github:nix-community/NUR"; @@ -24,6 +23,9 @@ inputs.disko.url = "github:nix-community/disko"; inputs.disko.inputs.nixpkgs.follows = "nixpkgs"; + inputs.arion.url = "github:hercules-ci/arion"; + inputs.arion.inputs.nixpkgs.follows = "nixpkgs"; + outputs = inputs: with inputs; let system = "x86_64-linux"; @@ -37,7 +39,7 @@ }; overlays = [ - nur.overlay + nur.overlays.default (_final: prev: { # this allows us to reference pkgs.unstable unstable = import nixpkgs-unstable { @@ -89,18 +91,19 @@ [ (configurationDefaults specialArgs) home-manager.nixosModules.home-manager + arion.nixosModules.arion ] ++ modules; }; in { nixosConfigurations.nixos = mkNixosConfiguration { - hostname = "pw-mainframe"; + # FIXME: replace with your server's hostname + hostname = "bobs-server"; username = "ironmagma"; args = { nixPkgs = import nixpkgs {inherit system overlays;}; }; modules = [ - disko.nixosModules.disko agenix.nixosModules.age ./hetzner.nix ./linux.nix diff --git a/nixos/gitconfig b/nixos/gitconfig new file mode 100644 index 0000000..c433c5b --- /dev/null +++ b/nixos/gitconfig @@ -0,0 +1,2 @@ +[alias] + co = checkout diff --git a/nixos/hetzner.nix b/nixos/hetzner.nix index 82fab44..64b6c45 100644 --- a/nixos/hetzner.nix +++ b/nixos/hetzner.nix @@ -2,22 +2,46 @@ imports = [ (modulesPath + "/installer/scan/not-detected.nix") (modulesPath + "/profiles/qemu-guest.nix") - ./disk-config.nix ]; # Fixes iotop boot.kernel.sysctl = {"kernel.task_delayacct" = 1;}; boot.loader.grub = { - # no need to set devices, disko will add all devices that have a EF02 partition to the list already - # devices = [ ]; - efiSupport = true; - efiInstallAsRemovable = true; + enable = true; + device = "/dev/sda"; + efiSupport = false; + }; + + # FIXME: replace with your actual disk UUID (find with: lsblk -f) + fileSystems."/" = { + device = "/dev/disk/by-uuid/YOUR-DISK-UUID-HERE"; + fsType = "ext4"; + }; + + networking.useDHCP = false; + systemd.network = { + enable = true; + # FIXME: replace enp1s0 with your actual NIC name (check with: ip link) + networks."10-enp1s0" = { + matchConfig.Name = "enp1s0"; + networkConfig.DHCP = "ipv4"; + dhcpV4Config = { + UseGateway = false; + UseRoutes = false; + }; + routes = [ + {routeConfig = {Gateway = "172.31.1.1"; GatewayOnLink = true;};} + ]; + }; }; services.openssh = { enable = true; - settings.PasswordAuthentication = false; + # FIXME: set to false once your SSH keys are in place + settings.PasswordAuthentication = true; + settings.PermitRootLogin = "yes"; + settings.AllowUsers = ["root"]; extraConfig = '' PrintLastLog no ''; @@ -32,7 +56,7 @@ type = "ed25519"; } - # For secrets + # For secrets decryption (agenix) { path = "/root/.ssh/id_rsa_nix"; type = "ed25519"; diff --git a/nixos/linux.nix b/nixos/linux.nix index 2d8794c..898bf5b 100644 --- a/nixos/linux.nix +++ b/nixos/linux.nix @@ -11,9 +11,9 @@ }: 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}"; + startSeq = builtins.fromJSON ''""''; + endSeq = builtins.fromJSON ''""''; + motd = "${startSeq} Welcome to Bob's Server! ${endSeq}"; nixPkgs = specialArgs.nixPkgs; ourRustVersion = pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.complete); @@ -33,15 +33,41 @@ in { imports = [ (import ./cloned_repos {inherit pkgs pullomatic lib;}) - (import ./nginx.nix {inherit pkgs lib config;}) (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"; + # FIXME: set your timezone + time.timeZone = "America/Chicago"; - age.secrets.nearlyfreespeech.file = ./secrets/nearlyfreespeech.age; - age.secrets.nearlyfreespeech.owner = "root"; + age.secrets = { + nearlyfreespeech = { + file = ./secrets/nearlyfreespeech.age; + owner = "root"; + }; + + webdav = { + file = ./secrets/webdav.age; + owner = "root"; + }; + + # Optional: only needed if you use claude-code or other Anthropic tools + anthropic-api-key = { + file = ./secrets/anthropic-api-key.age; + owner = "root"; + }; + + forgejo-runner-token = { + file = ./secrets/forgejo-runner-token.age; + owner = "root"; + }; + + paperless = { + file = ./secrets/paperless.age; + owner = "root"; + }; + }; environment.systemPackages = [ ddnsPkg @@ -53,6 +79,11 @@ in { pkgs.util-linux pkgs.iotop pkgs.rust-bin.stable.latest.default + pkgs.wget + pkgs.tmux + + # Uncomment to install Claude Code on the server: + # pkgs.unstable.claude-code ]; swapDevices = [ @@ -62,15 +93,81 @@ in { } ]; + virtualisation.arion = { + backend = "podman-socket"; + projects.forgejo.settings = import ./arion/arion-compose.nix; + projects.riverside.settings = import ./arion-riverside/arion-compose.nix; + projects.pluto.settings = import ./arion-pluto/arion-compose.nix; + projects.paperless.settings = import ./arion-paperless/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"; + systemd.services.arion-pluto.environment.DOCKER_HOST = "unix:///run/podman/podman.sock"; + systemd.services.arion-paperless.environment.DOCKER_HOST = "unix:///run/podman/podman.sock"; + + services.gitea-actions-runner.instances."ubuntu" = { + enable = true; + name = "ubuntu"; + # FIXME: point at your Forgejo instance + 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/pluto/notebooks 0755 root root" + "d /var/pluto/julia-depot 0755 root root" + "d /var/paperless/data 0755 root root" + "d /var/paperless/media 0755 root root" + "d /var/paperless/export 0755 root root" + "d /var/paperless/consume 0755 root root" + "d /var/paperless/postgres 0755 root root" + "d /var/paperless/redis 0755 root root" + "d /var/riverside/files 0755 root root" + "d /var/riverside/db 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 = '' + # Uncomment if you have an anthropic-api-key secret configured: + # export ANTHROPIC_API_KEY=$(cat ${config.age.secrets.anthropic-api-key.path}) + ''; environment.pathsToLink = ["/share/zsh"]; environment.shells = [pkgs.zsh]; @@ -82,32 +179,26 @@ in { system.stateVersion = "22.05"; - virtualisation.docker = { + virtualisation.podman = { enable = true; - enableOnBoot = true; - autoPrune.enable = true; + defaultNetwork.settings.dns_enabled = true; }; + # KDE Plasma spawns many threads. Raise machine.slice TasksMax so podman + # scopes don't get capped at the systemd default (~9286). + systemd.slices.machine.sliceConfig.TasksMax = "infinity"; + virtualisation.containers.containersConf.settings.containers.pids_limit = 0; + virtualisation.oci-containers = { - backend = "docker"; + backend = "podman"; containers = { - "hello" = { - autoStart = true; - image = "nginxdemos/hello"; - #user = "root:jellyfin"; - volumes = [ - ]; - ports = ["8081:80"]; - }; - "navidrome" = { autoStart = true; environment = { - "TZ" = "America/Anchorage"; + "TZ" = "America/Chicago"; "PUID" = "1000"; "PGID" = "100"; - "ND_SCANSCHEDULE" = "1h"; "ND_LOGLEVEL" = "info"; "ND_SESSIONTIMEOUT" = "24h"; @@ -124,29 +215,41 @@ in { "webdav" = { autoStart = true; image = "dgraziotin/nginx-webdav-nononsense"; - #user = "root:jellyfin"; volumes = [ "/mnt/webdav/data:/data" "/mnt/webdav/config:/config" ]; environment = { - "WEBDAV_USERNAME" = "foo"; - # TODO - "WEBDAV_PASSWORD" = "bar"; - "TZ" = "America/Anchorage"; - + "WEBDAV_USERNAME" = "bob"; + "TZ" = "America/Chicago"; "PUID" = "60"; # nginx user "PGID" = "60"; # nginx group }; + environmentFiles = [config.age.secrets.webdav.path]; ports = ["8082:80"]; }; + + "blog-quine" = { + autoStart = true; + image = "quineglobal/blog-quine:latest"; + volumes = []; + environment = {}; + ports = ["3010:8080"]; + }; + + "hyper-quineglobal-com" = { + autoStart = true; + image = "forge.quinefoundation.com/ironmagma/hyper-quineglobal-com:latest"; + volumes = []; + environment = {}; + ports = ["3013:8081"]; + }; }; }; nix = { settings = { trusted-users = [username]; - accept-flake-config = true; auto-optimise-store = true; }; @@ -163,7 +266,7 @@ in { "/nix/var/nix/profiles/per-user/root/channels" ]; - package = pkgs.nixFlakes; + package = pkgs.nixVersions.stable; extraOptions = ''experimental-features = nix-command flakes''; gc = { @@ -172,15 +275,55 @@ in { }; }; - # HTTPS - + # HTTPS — DNS challenge via NearlyFreeSpeech for domains that need certs + # before nginx is running (avoids circular systemd dependency). + # FIXME: replace all domain names with your own. security.acme = { acceptTerms = true; - defaults.email = "peterson@sent.com"; - certs."philippeterson.com" = { + defaults.email = "bob@bobs-home.com"; + + certs."bobs-home.com" = { dnsProvider = "nearlyfreespeech"; environmentFile = config.age.secrets."nearlyfreespeech".path; webroot = null; }; + + certs."webdav.bobs-home.com" = { + dnsProvider = "nearlyfreespeech"; + environmentFile = config.age.secrets."nearlyfreespeech".path; + webroot = null; + }; + + certs."pluto.bobs-home.com" = { + dnsProvider = "nearlyfreespeech"; + environmentFile = config.age.secrets."nearlyfreespeech".path; + webroot = null; + group = config.services.nginx.group; + }; + + certs."paperless.bobs-home.com" = { + dnsProvider = "nearlyfreespeech"; + environmentFile = config.age.secrets."nearlyfreespeech".path; + webroot = null; + group = config.services.nginx.group; + }; }; + + # Break the systemd ordering cycle that deadlocks nixos-rebuild switch. + # DNS-challenge certs don't need nginx running; swap the real acme services + # out of nginx's After list for the selfsigned fallback services instead. + # Every domain with enableACME = true in nginx.nix must appear here. + # FIXME: update this list whenever you add or remove ACME-enabled vhosts. + systemd.services.nginx.after = lib.mkForce [ + "network.target" + "acme-selfsigned-bobs-home.com.service" + "acme-selfsigned-www.bobs-home.com.service" + "acme-selfsigned-bobs-hobby.com.service" + "acme-selfsigned-www.bobs-hobby.com.service" + "acme-selfsigned-hyper.quineglobal.com.service" + "acme-selfsigned-forge.quinefoundation.com.service" +"acme-selfsigned-webdav.bobs-home.com.service" + "acme-selfsigned-pluto.bobs-home.com.service" + "acme-selfsigned-paperless.bobs-home.com.service" + ]; } diff --git a/nixos/nginx.nix b/nixos/nginx.nix index e5ec4a0..6dbe94b 100644 --- a/nixos/nginx.nix +++ b/nixos/nginx.nix @@ -7,49 +7,192 @@ services.nginx = { enable = true; - virtualHosts = { - "_default" = { - listen = [ - { addr = "0.0.0.0"; port = 80; } - { addr = "[::]"; port = 80; } - ]; - serverName = "_"; - extraConfig = '' - deny all; - return 444; - ''; + virtualHosts = let + # Static site embedded in the Nix store at build time. + # Replace static-site/ with your own files. + hobbyRoot = pkgs.runCommand "bobs-hobby-web" {} '' + mkdir -p $out + cp ${./static-site/index.html} $out/index.html + ''; + + # Wraps a vhost config to also create a www. redirect. + withWww = domain: cfg: { + "${domain}" = cfg; + "www.${domain}" = { + enableACME = true; + addSSL = cfg.addSSL or true; + forceSSL = cfg.forceSSL or false; + locations."/" = { + extraConfig = '' + return 301 https://${domain}$request_uri; + ''; + }; + }; }; + in + { + # Deny anything that doesn't match a named vhost. + "_default" = { + listen = [ + { + addr = "0.0.0.0"; + port = 80; + } + { + addr = "[::]"; + port = 80; + } + ]; + serverName = "_"; + extraConfig = '' + deny all; + return 444; + ''; + }; - "philippeterson.com" = { - enableACME = true; # Enable Let's Encrypt certificate for HTTPS - forceSSL = false; # Redirect HTTP to HTTPS? + # Quine Global landing page — served from a git repo pulled by pullomatic. + "quineglobal.com" = { + enableACME = false; + forceSSL = false; + addSSL = false; + locations."/" = { + extraConfig = '' + index index.html index.htm; + root /etc/pullomatic/com_quineglobal; + ''; + }; + }; + + # Quine Global blog — proxied to a container on port 3010. + "blog.quineglobal.com" = { + enableACME = false; + forceSSL = false; + addSSL = false; + locations."/" = { + proxyPass = "http://127.0.0.1:3010/"; + }; + }; + + # Hyper — proxied to a container on port 3013. + "hyper.quineglobal.com" = { + enableACME = true; + forceSSL = false; + addSSL = true; + locations."/" = { + proxyPass = "http://127.0.0.1:3013/"; + }; + }; + + # WebDAV — proxy to the webdav container. + # FIXME: replace domain + "webdav.bobs-home.com" = { + serverName = "webdav.bobs-home.com"; + useACMEHost = "webdav.bobs-home.com"; + onlySSL = true; + locations."/" = { + proxyPass = "http://127.0.0.1:8082/"; + extraConfig = '' + proxy_set_header Host $host; + 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 Authorization $http_authorization; + proxy_read_timeout 300; + proxy_connect_timeout 300; + proxy_send_timeout 300; + client_max_body_size 100M; + proxy_buffering off; + proxy_cache off; + ''; + }; + }; + + # Paperless-ngx document management. + # FIXME: replace domain + "paperless.bobs-home.com" = { + useACMEHost = "paperless.bobs-home.com"; + onlySSL = true; + locations."/" = { + proxyPass = "http://127.0.0.1:8000/"; + extraConfig = '' + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_read_timeout 86400; + client_max_body_size 100M; + ''; + }; + }; + + # Pluto — Julia notebook server. + # FIXME: replace domain + "pluto.bobs-home.com" = { + useACMEHost = "pluto.bobs-home.com"; + onlySSL = true; + locations."/" = { + proxyPass = "http://127.0.0.1:1234/"; + extraConfig = '' + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_read_timeout 86400; + ''; + }; + }; + + # Forgejo git forge — HTTPS vhost. + "forge.quinefoundation.com-https" = { + serverName = "forge.quinefoundation.com"; + enableACME = true; + onlySSL = true; + extraConfig = '' + client_max_body_size 0; + ''; + locations."/" = { + proxyPass = "http://127.0.0.1:3000/"; + }; + }; + + # Forgejo — HTTP vhost that redirects to HTTPS. + "forge.quinefoundation.com-http" = { + serverName = "forge.quinefoundation.com"; + listen = [ + { + addr = "0.0.0.0"; + port = 80; + } + { + addr = "[::]"; + port = 80; + } + ]; + locations."/" = { + extraConfig = '' + return 302 https://$host$request_uri; + ''; + }; + }; + } + # Personal site with PHP — served from a git repo pulled by pullomatic. + # FIXME: replace domain and adjust URL rewrite rules for your site. + // (withWww "bobs-home.com" { + enableACME = true; + forceSSL = false; addSSL = true; - - root = "/etc/pullomatic/com_philippeterson"; - + root = "/etc/pullomatic/com_bobs-home"; locations."~ /.git(/.*)$ " = { extraConfig = '' deny all; return 404; ''; }; - - locations."~ ^/games/atcsim(/[^/\\s]*)*$" = { - extraConfig = '' - index index.html index.htm; - rewrite ^/games/atcsim/?$ "/index.html" break; - rewrite ^/games/atcsim(?(/[^/\\s]*)*)$ "$query" break; - root /etc/pullomatic/atcsim; - ''; - }; - locations."~ ^/echo(?((/[^/\\s]*)*))$" = { extraConfig = '' add_header Content-Type text/plain; return 200 "$query"; ''; }; - locations."/" = { extraConfig = '' try_files $uri $uri.php $uri/ =404; @@ -58,7 +201,6 @@ rewrite ^/resume$ /resume.php last; ''; }; - locations."~ \.php$" = { extraConfig = '' include ${pkgs.nginx}/conf/fastcgi.conf; @@ -66,53 +208,16 @@ fastcgi_pass unix:${config.services.phpfpm.pools.main.socket}; ''; }; - }; - "blog.quinefoundation.com" = { + }) + # Static hobby site served directly from the Nix store. + # FIXME: replace domain and put your files in static-site/ + // (withWww "bobs-hobby.com" { enableACME = true; forceSSL = false; addSSL = true; + root = hobbyRoot; + }); - root = "/etc/pullomatic/com_quinefoundation_blog/markdown-blog"; - - locations."~ /.git(/.*)$ " = { - extraConfig = '' - deny all; - return 404; - ''; - }; - - locations."~ ^/static(/.*)?$" = { - extraConfig = '' - autoindex on; - root /etc/pullomatic/com_quinefoundation_blog/static; - rewrite ^/static(?(/[^/\\s]*)*)$ "$query" break; - ''; - }; - - locations."/" = { - extraConfig = '' - rewrite ^/?$ /blog-posts-list.php last; - rewrite ^/post/?$ /blog-posts-list.php last; - rewrite ^/about/?$ /about.php last; - rewrite ^/credits/?$ /credits.php last; - rewrite ^/post/([-a-zA-Z0-9]*)$ /blog-page.php?page=$1.md last; - rewrite ^/rss.xml$ /rss.php last; - try_files $uri $uri/ =404; - index index.php index.html index.htm; - ''; - }; - - locations."~ \.php$" = { - extraConfig = '' - include ${pkgs.nginx}/conf/fastcgi.conf; - fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_pass unix:${config.services.phpfpm.pools.main.socket}; - ''; - }; - }; - }; - - # Optionally configure additional options recommendedGzipSettings = true; recommendedProxySettings = true; recommendedTlsSettings = true; @@ -124,8 +229,6 @@ user = "nginx"; group = "nginx"; settings = { - # listen = /run/phpfpm.sock - # "listen.mode = 0660 "listen.owner" = "nginx"; "listen.group" = "nginx"; "pm" = "dynamic"; @@ -134,7 +237,6 @@ "pm.min_spare_servers" = 5; "pm.max_spare_servers" = 20; "pm.max_requests" = 500; - "php_admin_value[error_log]" = "stderr"; "php_admin_flag[log_errors]" = true; "catch_workers_output" = true; diff --git a/nixos/secrets/default.nix b/nixos/secrets/default.nix index 819eae3..a7cbb4f 100644 --- a/nixos/secrets/default.nix +++ b/nixos/secrets/default.nix @@ -1,8 +1,27 @@ -{}: let - mainframePublicKey = builtins.toString "../keys/mainframe.pub"; +let + mainframePublicKey = builtins.readFile ../keys/mainframe.pub; in { - # This .age file should contain the following environment variables: - # NEARLYFREESPEECH_API_KEY - # NEARLYFREESPEECH_LOGIN + # NEARLYFREESPEECH_API_KEY, NEARLYFREESPEECH_LOGIN + # Used for DNS-challenge ACME cert provisioning via the NearlyFreeSpeech DNS provider. "./nearlyfreespeech.age".publicKeys = [mainframePublicKey]; + + # WEBDAV_PASSWORD + "./webdav.age".publicKeys = [mainframePublicKey]; + + # ANTHROPIC_API_KEY (optional — only needed if you use claude-code or other Anthropic tools) + "./anthropic-api-key.age".publicKeys = [mainframePublicKey]; + + # TOKEN= + "./forgejo-runner-token.age".publicKeys = [mainframePublicKey]; + + # PAPERLESS_SECRET_KEY= + # PAPERLESS_ADMIN_USER=admin + # PAPERLESS_ADMIN_PASSWORD= + # PAPERLESS_ADMIN_EMAIL= + "./paperless.age".publicKeys = [mainframePublicKey]; + + # Add any app-specific secrets here, e.g.: + # DATABASE_URL= + # APP_SECRET= + # "./my-webapp.age".publicKeys = [mainframePublicKey]; } diff --git a/nixos/static-site/index.html b/nixos/static-site/index.html new file mode 100644 index 0000000..6408acd --- /dev/null +++ b/nixos/static-site/index.html @@ -0,0 +1,20 @@ + + + + + + Bob's Hobby Site + + + +

Bob's Hobby Site

+

+ This page is served directly from the Nix store — the HTML is embedded in the + system closure at build time, so there is no external file to manage. +

+

Replace this file with your own static content.

+ + diff --git a/nixos/system/home/shared.nix b/nixos/system/home/shared.nix index 1ea7e34..e9eca2f 100644 --- a/nixos/system/home/shared.nix +++ b/nixos/system/home/shared.nix @@ -16,6 +16,7 @@ jq killall tmux + tree unzip vim wget @@ -27,6 +28,7 @@ go nodejs python3 + clojure alejandra # nix formatter ]; @@ -36,7 +38,7 @@ joinedString = lib.concatStringsSep " " pubKeys; in { imports = [ - nix-index-database.hmModules.nix-index + nix-index-database.homeModules.nix-index ]; home = { @@ -47,12 +49,12 @@ in { sessionVariables.EDITOR = "vim"; sessionVariables.SHELL = "/etc/profiles/per-user/${username}/bin/zsh"; - packages = lib.mkDefault ( - stable-packages - ++ unstable-packages - ); + packages = stable-packages ++ unstable-packages; - file.".ssh/known_hosts".text = joinedString; + file.".ssh/known_hosts" = { + text = joinedString; + force = true; + }; }; programs = { @@ -100,8 +102,9 @@ in { side-by-side = true; navigate = true; }; - userEmail = "1326208+philip-peterson@users.noreply.github.com"; - userName = "philip-peterson"; + # FIXME: replace with your own name and email + userEmail = "bob@bobs-home.com"; + userName = "bob-smith"; extraConfig = { push = { default = "current"; @@ -114,15 +117,16 @@ in { colorMoved = "default"; }; safe = { - directory = "/var/petersweb-infra"; + directory = "/var/bobs-infra"; }; }; + includes = [{path = toString ../../gitconfig;}]; }; zsh = { enable = true; autocd = true; - enableAutosuggestions = true; + autosuggestion.enable = true; enableCompletion = true; defaultKeymap = "emacs"; history.size = 10000; @@ -150,17 +154,9 @@ in { ]; shellAliases = { - "u" = "cd .."; - "uu" = "cd ../.."; - "uuu" = "cd ../../.."; - "uuuu" = "cd ../../../.."; - gs = "git status"; gc = "nix-collect-garbage --delete-old"; refresh = "source ~/.zshrc"; show_path = "echo $PATH | tr ':' '\n'"; - - gst = "git status"; - gco = "git checkout"; }; envExtra = '' @@ -168,6 +164,7 @@ in { ''; initExtra = '' + ${builtins.readFile ../../aliases.zsh} bindkey '^p' history-search-backward bindkey '^n' history-search-forward bindkey '^e' end-of-line @@ -177,21 +174,12 @@ in { bindkey ";5D" backward-word zstyle ':completion:*:*:*:*:*' menu select - - # Complete . and .. special directories zstyle ':completion:*' special-dirs true - zstyle ':completion:*' list-colors "" zstyle ':completion:*:*:kill:*:processes' list-colors '=(#b) #([0-9]#) ([0-9a-z-]#)*=01;34=0=01' - - # disable named-directories autocompletion zstyle ':completion:*:cd:*' tag-order local-directories directory-stack path-directories - - # Use caching so that commands like apt and dpkg complete are useable zstyle ':completion:*' use-cache on zstyle ':completion:*' cache-path "$XDG_CACHE_HOME/zsh/.zcompcache" - - # Don't complete uninteresting users zstyle ':completion:*:*:*:users' ignored-patterns \ adm amanda apache at avahi avahi-autoipd beaglidx bin cacti canna \ clamav daemon dbus distcache dnsmasq dovecot fax ftp games gdm \ @@ -201,10 +189,7 @@ in { operator pcap polkitd postfix postgres privoxy pulse pvm quagga radvd \ rpc rpcuser rpm rtkit scard shutdown squid sshd statd svn sync tftp \ usbmux uucp vcsa wwwrun xfs '_*' - # ... unless we really want to. zstyle '*' single-ignored complete - - # https://thevaluable.dev/zsh-completion-guide-examples/ zstyle ':completion:*' completer _extensions _complete _approximate zstyle ':completion:*:descriptions' format '%F{green}-- %d --%f' zstyle ':completion:*' group-name "" @@ -212,7 +197,6 @@ in { zstyle ':completion:*' squeeze-slashes true zstyle ':completion:*' matcher-list "" 'm:{a-zA-Z}={A-Za-z}' 'r:|[._-]=* r:|=*' 'l:|=* r:|=*' - # mkcd is equivalent to takedir function mkcd takedir() { mkdir -p $@ && cd ''${@:$#} } @@ -243,8 +227,6 @@ in { } WORDCHARS='*?[]~=&;!#$%^(){}<>' - - # fixes duplication of commands when using tab-completion export LANG=C.UTF-8 ''; }; diff --git a/nixos/system/users.nix b/nixos/system/users.nix index 10f47d1..e96fcd8 100644 --- a/nixos/system/users.nix +++ b/nixos/system/users.nix @@ -12,10 +12,11 @@ authorizedKeys, homeConfig ? null, isNormalUser ? true, + enableHomeManager ? false, }: { extraGroups = extraGroups ++ [username]; - home-manager.users.${username} = homeConfig; + home-manager.users.${username} = if enableHomeManager then homeConfig else {}; users.users.${username} = { isNormalUser = isNormalUser; @@ -38,8 +39,9 @@ isNormalUser = false; username = "root"; home = "/root"; - extraGroups = []; + extraGroups = ["podman"]; authorizedKeys = authorizedKeys; + enableHomeManager = true; homeConfig = import ./home/root.nix { username = "root"; inherit config pkgs nix-index-database lib; @@ -52,8 +54,10 @@ extraGroups = [ "wheel" "docker" + "podman" ]; authorizedKeys = authorizedKeys; + enableHomeManager = false; homeConfig = import ./home/ironmagma.nix { username = "ironmagma"; inherit config pkgs nix-index-database lib; @@ -66,5 +70,6 @@ in { }; users.users = rootUser.users.users // ironmagmaUser.users.users // {}; - home-manager.users = rootUser.home-manager.users // ironmagmaUser.home-manager.users; + home-manager.users = rootUser.home-manager.users; + home-manager.backupFileExtension = "hm-backup"; }