From 220d1f651c83079cf225a0308687c4496e094e7c Mon Sep 17 00:00:00 2001 From: mikl Date: Sun, 12 Jul 2026 12:23:05 +0300 Subject: [PATCH] heather: replace k3s with docker+arion+caddy+syncthing - remove k3s.nix (1.3GB overhead unjustified for single-node) - add docker.nix (docker + arion CLI, mikl in docker group) - add caddy.nix (edge proxy, TLS via ACME staging, host-based routing) - add syncthing.nix (sync daemon + strelaysrv via systemd, /srv/syncthing) - add arion-compose.nix (forgejo first, others stubbed as TODO) - add arion flake input + nixos module - default.nix: wire up arion project as systemd service (serviceName=heather) Architecture: Caddy (host) terminates TLS, routes to host services (syncthing) and docker containers (arion, 127.0.0.1:PORT). Named volumes everywhere except silverbullet (bind /srv/syncthing/herbarium). --- flake.lock | 60 +++++++++++++++ flake.nix | 8 +- hosts/heather/arion-compose.nix | 127 ++++++++++++++++++++++++++++++++ hosts/heather/caddy.nix | 85 +++++++++++++++++++++ hosts/heather/default.nix | 43 +++++++++-- hosts/heather/docker.nix | 29 ++++++++ hosts/heather/k3s.nix | 34 --------- hosts/heather/syncthing.nix | 69 +++++++++++++++++ 8 files changed, 412 insertions(+), 43 deletions(-) create mode 100644 hosts/heather/arion-compose.nix create mode 100644 hosts/heather/caddy.nix create mode 100644 hosts/heather/docker.nix delete mode 100644 hosts/heather/k3s.nix create mode 100644 hosts/heather/syncthing.nix diff --git a/flake.lock b/flake.lock index 0198836..58bc92c 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,64 @@ { "nodes": { + "arion": { + "inputs": { + "flake-parts": "flake-parts", + "haskell-flake": "haskell-flake", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1770259557, + "narHash": "sha256-EvZ09k9+mzXAngPzU2K7oLLUDlKoT1numb4bDb3Gtl4=", + "owner": "hercules-ci", + "repo": "arion", + "rev": "9b24cf65c72cb0e9616e437d55e1ac8e5c6bc715", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "arion", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": [ + "arion", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1769996383, + "narHash": "sha256-AnYjnFWgS49RlqX7LrC4uA+sCCDBj0Ry/WOJ5XWAsa0=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "57928607ea566b5db3ad13af0e57e921e6b12381", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "haskell-flake": { + "locked": { + "lastModified": 1675296942, + "narHash": "sha256-u1X1sblozi5qYEcLp1hxcyo8FfDHnRUVX3dJ/tW19jY=", + "owner": "srid", + "repo": "haskell-flake", + "rev": "c2cafce9d57bfca41794dc3b99c593155006c71e", + "type": "github" + }, + "original": { + "owner": "srid", + "ref": "0.1.0", + "repo": "haskell-flake", + "type": "github" + } + }, "home-manager": { "inputs": { "nixpkgs": [ @@ -58,6 +117,7 @@ }, "root": { "inputs": { + "arion": "arion", "home-manager": "home-manager", "nix-darwin": "nix-darwin", "nixpkgs": "nixpkgs", diff --git a/flake.nix b/flake.nix index 0810722..5be75f6 100644 --- a/flake.nix +++ b/flake.nix @@ -18,10 +18,15 @@ url = "github:Mic92/sops-nix"; inputs.nixpkgs.follows = "nixpkgs"; }; + + arion = { + url = "github:hercules-ci/arion"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; outputs = - { self, nixpkgs, nix-darwin, home-manager, sops-nix, ... }: + { self, nixpkgs, nix-darwin, home-manager, sops-nix, arion, ... }: let mkNixos = hostname: nixpkgs.lib.nixosSystem { @@ -31,6 +36,7 @@ ./hosts/${hostname} sops-nix.nixosModules.sops home-manager.nixosModules.home-manager + arion.nixosModules.arion { home-manager = { useGlobalPkgs = true; diff --git a/hosts/heather/arion-compose.nix b/hosts/heather/arion-compose.nix new file mode 100644 index 0000000..b60b4ef --- /dev/null +++ b/hosts/heather/arion-compose.nix @@ -0,0 +1,127 @@ +# heather — Arion compose (nix-native docker-compose). +# +# Все web-apps в docker-контейнерах, описаны декларативно в nix. +# Запускается как systemd-сервис через virtualisation.arion.projects.heather. +# +# Порты проброшены на localhost (127.0.0.1:PORT) — Caddy на хосте терминирует +# TLS и роутит по Host (см. caddy.nix). Контейнеры НЕ выставлены наружу +# напрямую (кроме jitsi UDP 10000/19132 — см. jitsi ниже, когда добавим). +# +# Data: named volumes везде, кроме silverbullet (bind mount на syncthing +# sync'аемую папку /srv/syncthing/herbarium — исключение, см. комментарии). +# +# Backup: offen/docker-volume-backup как sidecar, nightly tar.gz всех named +# volumes → /var/backups (добавим последним). +# +# Порядок миграции: forgejo → tuwunel+admin → cinny → jitsi → teable → +# silverbullet. Ниже forgejo как первый proof-of-concept, остальные добавим +# по мере миграции (секции заготовлены, закомментированы до теста). + +{ pkgs, ... }: + +{ + # Named volumes объявляются автоматически docker-compose при упоминании в + # services..service.volumes. Имена префиксуются проектом (heather_forgejo-data) + # — видно в `docker volume ls`. Никаких анонимных хэшей. + + services = { + # === Forgejo (git.iscg.dev) === + # Первый сервис, proof-of-concept для цепочки arion→docker→Caddy. + forgejo = { + service.image = "codeberg.org/forgejo/forgejo:9"; + service.container_name = "forgejo"; + service.restart = "unless-stopped"; + service.volumes = [ "forgejo-data:/data" ]; + service.ports = [ + "127.0.0.1:3030:3000" # web → Caddy git.iscg.dev + "2222:22" # git ssh (публично, как на jul11) + ]; + service.environment = { + # Домен для клонов: ssh://git@git.iscg.dev:2222/... + FORGEJO__server__DOMAIN = "git.iscg.dev"; + FORGEJO__server__ROOT_URL = "https://git.iscg.dev/"; + FORGEJO__server__SSH_DOMAIN = "git.iscg.dev"; + FORGEJO__server__SSH_PORT = "2222"; + }; + }; + + # === Matrix tuwunel + admin + cinny (iscg.dev, admin/chat.iscg.dev) === + # Добавить после forgejo. Crashloop на jul11 — нужно разобраться с + # конфигом/секретами до миграции. См. migration-inventory.md. + # + # tuwunel = { + # service.image = "ghcr.io/matrix-construct/tuwunel:v1.7.1"; + # service.container_name = "tuwunel"; + # service.restart = "unless-stopped"; + # service.volumes = [ + # "tuwunel-data:/data" + # "tuwunel-media-cache:/media-cache" + # ]; + # service.ports = [ "127.0.0.1:8008:8008" ]; + # # TODO: env vars, secrets (server_name, registration_shared_secret, ...) + # }; + + # cinny = { + # service.image = "ajbura/cinny:latest"; + # service.container_name = "cinny"; + # service.restart = "unless-stopped"; + # service.ports = [ "127.0.0.1:8080:80" ]; + # }; + + # tuwunel-admin был systemd на jul11 (knadh/tuwunel-admin). Если контейнер + # существует — добавить сюда, иначе оставить как NixOS-сервис или выкинуть. + + # === Jitsi Meet (jitsi.iscg.dev) === + # Стек: web, jicofo, prosody, jvb, coturn. На jul11 был конфликт :3478 + # (systemd coturn + docker stack-coturn). На heather — только docker coturn + # внутри стека, systemd coturn НЕ поднимаем. Добавить отдельнымcompose-файлом + # или секцией после forgejo. + + # === Teable (teable.iscg.dev) === + # Тяжёлый стек: app + postgres + redis. Добавить после forgejo. + # + # teable = { + # service.image = "teableio/teable:latest"; + # service.container_name = "teable"; + # service.restart = "unless-stopped"; + # service.ports = [ "127.0.0.1:2345:3000" ]; + # service.volumes = [ "teable-data:/app/data" ]; + # service.depends_on = [ "teable-db" "teable-redis" ]; + # # TODO: env (DATABASE_URL, REDIS_URL, SECRET_KEY, PUBLIC_ORIGIN, ...) + # }; + # teable-db = { + # service.image = "postgres:15-alpine"; + # service.container_name = "teable-db"; + # service.restart = "unless-stopped"; + # service.volumes = [ "teable-db:/var/lib/postgresql/data" ]; + # # TODO: POSTGRES_PASSWORD (sops) + # }; + # teable-redis = { + # service.image = "redis:alpine"; + # service.container_name = "teable-redis"; + # service.restart = "unless-stopped"; + # service.volumes = [ "teable-redis:/data" ]; + # }; + + # === Silverbullet (note.iscg.dev) — ИСКЛЮЧЕНИЕ: bind mount === + # Space = /srv/syncthing/herbarium (sync'ается syncthing на устройства). + # Единственный сервис без named volume — по твоему решению, чтобы notes + # были доступны и через web UI, и через obsidian/vscode на poppy. + # Конфликты syncthing (одновременная правка) решаются через .sync-conflict. + # + # silverbullet = { + # service.image = "zefhemel/silverbullet:latest"; + # service.container_name = "silverbullet"; + # service.restart = "unless-stopped"; + # service.ports = [ "127.0.0.1:3001:3000" ]; + # service.volumes = [ + # "/srv/syncthing/herbarium:/space" # bind mount, НЕ named volume + # ]; + # }; + + # === Backup sidecar (последним) === + # offen/docker-volume-backup: nightly tar.gz всех named volumes. + # Монтирует volumes read-only, шлёт в /var/backups (или S3). + # TODO: добавить когда основные сервисы мигрированы. + }; +} diff --git a/hosts/heather/caddy.nix b/hosts/heather/caddy.nix new file mode 100644 index 0000000..b336658 --- /dev/null +++ b/hosts/heather/caddy.nix @@ -0,0 +1,85 @@ +# heather — Caddy как единый edge (TLS termination, reverse proxy). +# +# Заменяет Traefik (k3s built-in, отключён) и старые Caddy на iscg-dev/jul11. +# Паттерн как на iscg-dev: system Caddy, конфиг декларативно в nix. +# +# Caddy терминирует TLS на :80/:443 и роутит по Host на: +# - host-сервисы (syncthing GUI на localhost:8384) — напрямую +# - docker-контейнеры (forgejo на localhost:3030 и т.д.) — проброшенные порты +# +# ACME: пока staging (чтобы не забанили Let's Encrypt пока DNS не переведён на +# heather). Когда A-записи iscg.dev → 51.250.45.111 — переключим на production +# (убрать acme_ca, Caddy использует production LE по умолчанию). + +{ config, pkgs, lib, ... }: + +let + # caddy/basic-auth под паролем. hash сгенерить: + # caddy hash-password + # Пока заглушка — syncthing GUI имеет свой пароль + Caddy basicauth сверху. + syncBasicAuth = '' + basicauth { + mikl $2a$14$bQ5MVsSFUuIJjHETVXzISOTy5SpLxRUlH6krxpzLH1.E0imi37Zb. + } + ''; + +in { + services.caddy = { + enable = true; + # Staging ACME пока DNS не переведён на heather (см. комментарий выше). + # Для production: убрать acme_ca, Caddy сам использует Let's Encrypt. + acmeCA = "https://acme-staging-v02.api.letsencrypt.org/directory"; + email = "mikl@iscg.dev"; + + virtualHosts = { + # --- Syncthing GUI (host-сервис) --- + "sync.iscg.dev".extraConfig = syncBasicAuth + '' + reverse_proxy 127.0.0.1:8384 + ''; + + # --- Контейнеры (arion, см. arion-compose.nix) --- + # Forgejo + "git.iscg.dev".extraConfig = '' + reverse_proxy 127.0.0.1:3030 + ''; + + # Matrix tuwunel (homeserver) + "iscg.dev".extraConfig = '' + reverse_proxy 127.0.0.1:8008 + encode zstd gzip + ''; + + # Matrix federation + "iscg.dev:8448".extraConfig = '' + reverse_proxy 127.0.0.1:8008 + ''; + + # tuwunel-admin + "admin.iscg.dev".extraConfig = '' + reverse_proxy 127.0.0.1:8009 + ''; + + # Cinny (matrix web client) + "chat.iscg.dev".extraConfig = '' + reverse_proxy 127.0.0.1:8080 + encode zstd gzip + ''; + + # Jitsi Meet + "jitsi.iscg.dev".extraConfig = '' + reverse_proxy 127.0.0.1:8000 + encode zstd gzip + ''; + + # Teable + "teable.iscg.dev".extraConfig = '' + reverse_proxy 127.0.0.1:2345 + ''; + + # Silverbullet + "note.iscg.dev".extraConfig = '' + reverse_proxy 127.0.0.1:3001 + ''; + }; + }; +} diff --git a/hosts/heather/default.nix b/hosts/heather/default.nix index a0d0c8b..7427da8 100644 --- a/hosts/heather/default.nix +++ b/hosts/heather/default.nix @@ -1,4 +1,4 @@ -# heather — k3s single-node cluster на YC (NixOS 25.11, собирается как qcow2-образ) +# heather — сервер на Yandex Cloud (NixOS 26.11). # https://git.iscg.dev/mikl/nix-config # # Перенесено из muscari (переименование 2026-07-11). @@ -6,21 +6,48 @@ # Образ NixOS собирается как qcow2 (см. ./image.nix, make-disk-image.nix) и # заливается как boot disk в YC. НЕ через nixos-anywhere — kexec на YC хрупкий. # См. memory/yandex-nixos-qcow2.md. +# +# Архитектура (2026-07-12, заменила k3s): +# Caddy (edge, TLS) → host-сервисы (syncthing) + docker-контейнеры (arion) +# Cilium/k3s выключены — overhead 1.3 GB был неоправдан для single-node. +# См. ./docker.nix, ./caddy.nix, ./syncthing.nix, ./arion-compose.nix. { config, pkgs, lib, ... }: { imports = [ - ./image.nix # YC guest profile + qcow2 build target (заменяет disk-config.nix) - ./k3s.nix + ./image.nix # YC guest profile + qcow2 build target + ./docker.nix # docker + arion CLI (контейнеры) + ./caddy.nix # edge proxy, TLS, роутинг по доменам + ./syncthing.nix # sync + relay ]; + # Arion-проект как systemd-сервис: arion-compose.nix → docker-compose → up. + # NB: settings — это модуль с imports, НЕ `import ./arion-compose.nix`. + virtualisation.arion.backend = "docker"; + virtualisation.arion.projects.heather = { + serviceName = "heather"; # systemd: arion-heather.service + settings = { + imports = [ ./arion-compose.nix ]; + }; + }; + networking = { hostName = "heather"; + # Firewall выключен в ./image.nix (полагаемся на YC Security Groups). + # Порты тут только для документации — реально открывает YC SG. firewall.allowedTCPPorts = [ - 22 # SSH - 80 # HTTP (Ingress через Traefik) - 443 # HTTPS (Ingress через Traefik) - # 6443 не открываем — kubectl через SSH-туннель + 22 # SSH + 80 # HTTP (Caddy → redirect to HTTPS) + 443 # HTTPS (Caddy, TLS termination) + 2222 # forgejo git ssh (публично) + 22000 # syncthing sync + 22067 # strelaysrv (relay) + 22070 # strelaysrv (status) + # jitsi (когда добавим): 3478, 5349, 10000/udp, 49152-65535/udp + ]; + firewall.allowedUDPPorts = [ + 22000 # syncthing sync (QUIC) + # jitsi: 10000 (jvb), 49152-65535 (RTP) ]; }; # useDHCP задаёт ./image.nix (mkDefault) — облачный профиль. @@ -47,4 +74,4 @@ # После первой установки 25.11 не трогаем (NixOS convention) system.stateVersion = "25.11"; -} \ No newline at end of file +} diff --git a/hosts/heather/docker.nix b/hosts/heather/docker.nix new file mode 100644 index 0000000..63cfdd8 --- /dev/null +++ b/hosts/heather/docker.nix @@ -0,0 +1,29 @@ +# heather — docker + arion (nix-native docker-compose) +# +# Заменило k3s (2026-07-12): k3s сжирал 1.3 GB (16% от 8 GB) на single-node +# без реальной выгоды. docker+arion даёт ~100 MB overhead и знакомую модель. +# Caddy на хосте терминирует TLS (см. ./caddy.nix), Traefik/ServiceLB не нужны. +# +# Arion: читает arion-compose.nix (nix-выражение), генерирует docker-compose, +# запускает как systemd-сервис через virtualisation.arion.projects. +# Контейнеры описаны в ./arion-compose.nix. + +{ pkgs, ... }: +{ + virtualisation.docker = { + enable = true; + # Хост-docker (не rootless): mikl в группе docker для `docker` CLI без sudo. + # arion-проект запускается от root через systemd (см. ниже). + }; + + # Arion: nix-native docker-compose. backend задаётся в default.nix + # (virtualisation.arion.backend = "docker"), проект — там же. + + # mikl может управлять docker без sudo (docker group). + users.users.mikl.extraGroups = [ "docker" ]; + + environment.systemPackages = with pkgs; [ + docker-compose + arion + ]; +} diff --git a/hosts/heather/k3s.nix b/hosts/heather/k3s.nix deleted file mode 100644 index 40a604c..0000000 --- a/hosts/heather/k3s.nix +++ /dev/null @@ -1,34 +0,0 @@ -# k3s daemon для heather (single-node + Cilium CNI) -# Traefik и servicelb остаются включены (встроенные в k3s) -# Cilium ставится отдельно через Helm после старта k3s - -{ config, pkgs, lib, ... }: -{ - services.k3s = { - enable = true; - extraFlags = [ - # CNI: отключаем встроенный flannel, ставим Cilium отдельно - "--flannel-backend=none" - "--disable-network-policy" # Cilium сам делает network policy - - # CIDR для подов и сервисов (default для k3s, но фиксируем явно) - "--cluster-cidr=10.42.0.0/16" - "--service-cidr=10.43.0.0/16" - - # TLS SANs для kubectl (если подключаемся не только по IP) - "--tls-san=heather.iscg.dev" - ]; - }; - - # Firewall выключен в ./image.nix (полагаемся на YC Security Groups). - # Cilium управляет pod-сетью через BPF, локальный iptables-firewall ему - # только мешает — поэтому блок trustedInterfaces/checkReversePath не нужен. - - # k3s-specific пакеты (НЕ в common — только для хостов с k3s) - environment.systemPackages = with pkgs; [ - kubectl - kubernetes-helm - kustomize - sops # для шифрования k8s secrets (если будем через SOPS) - ]; -} \ No newline at end of file diff --git a/hosts/heather/syncthing.nix b/hosts/heather/syncthing.nix new file mode 100644 index 0000000..2461d84 --- /dev/null +++ b/hosts/heather/syncthing.nix @@ -0,0 +1,69 @@ +# heather — Syncthing (sync + relay) как NixOS-сервис. +# +# Заменяет syncthing@michaotic + strelaysrv с iscg-dev. +# +# - services.syncthing: sync daemon. dataDir = /srv/syncthing (синкается на +# устройства poppy/телефон). configDir = /var/lib/syncthing (state syncthing). +# - relayOptions.enable: strelaysrv, public relay на :22067 (relay) / :22070 +# (status). Для устройств за NAT. +# +# GUI: 127.0.0.1:8384 (только localhost), Caddy проксирует на sync.iscg.dev +# с basicauth сверху (см. caddy.nix). Пароль GUI включить отдельно (syncthing +# сам требует при прослушивании не-localhost, но на localhost опционален — +# Caddy даёт внешний слой auth). +# +# /srv/syncthing/herbarium/ — silverbullet space (bind mount в контейнер, +# см. arion-compose.nix). syncthing sync'ает весь /srv/syncthing включая +# herbarium на устройства. + +{ config, pkgs, lib, ... }: +{ + services.syncthing = { + enable = true; + user = "mikl"; + group = "users"; + + # Что синкается (файлы). /srv — FHS для service data. + dataDir = "/srv/syncthing"; + + # State syncthing (config, certs, index). Не синкается. + configDir = "/var/lib/syncthing"; + + # Открывает firewall порты 22000 (sync). firewall выключен на heather + # (полагаемся на YC SG), но флаг для консистентности. + openDefaultPorts = true; + + # GUI только на localhost — Caddy проксирует снаружи. + guiAddress = "127.0.0.1:8384"; + + # НЕ override devices/folders из nix — конфигурим через GUI/API. + # overrideDevices/overrideFolders = false (default) = nix не трогает то, + # что добавлено через GUI. Иначе nix бы их стирал при rebuild. + overrideDevices = false; + overrideFolders = false; + }; + + # strelaysrv: public relay для syncthing-устройств за NAT. + # Порты 22067 (relay) и 22070 (status). + # NixOS-модуля syncthing не управляет strelaysrv — поднимаем вручную через + # systemd, пакет syncthing-relay (strelaysrv binary). + systemd.services.syncthing-relay = { + description = "Syncthing Relay Server (strelaysrv)"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + ExecStart = "${pkgs.syncthing-relay}/bin/strelaysrv"; + Restart = "on-failure"; + User = "mikl"; + Group = "users"; + # strelaysrv пишет ключи/статус в CWD — рабочая директория. + WorkingDirectory = "/var/lib/syncthing-relay"; + StateDirectory = "syncthing-relay"; + }; + }; + + # /srv/syncthing создаём, права mikl:users (syncthing работает от mikl). + systemd.tmpfiles.rules = [ + "d /srv/syncthing 0755 mikl users -" + ]; +}