Compare commits

..

2 commits

Author SHA1 Message Date
220d1f651c 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).
2026-07-12 12:23:05 +03:00
c8b87ffcf1 git: use canonical git-lfs filter strings to fix 3.7 warning 2026-07-12 01:28:20 +03:00
9 changed files with 419 additions and 43 deletions

View file

@ -1,5 +1,64 @@
{ {
"nodes": { "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": { "home-manager": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
@ -58,6 +117,7 @@
}, },
"root": { "root": {
"inputs": { "inputs": {
"arion": "arion",
"home-manager": "home-manager", "home-manager": "home-manager",
"nix-darwin": "nix-darwin", "nix-darwin": "nix-darwin",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs",

View file

@ -18,10 +18,15 @@
url = "github:Mic92/sops-nix"; url = "github:Mic92/sops-nix";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
arion = {
url = "github:hercules-ci/arion";
inputs.nixpkgs.follows = "nixpkgs";
};
}; };
outputs = outputs =
{ self, nixpkgs, nix-darwin, home-manager, sops-nix, ... }: { self, nixpkgs, nix-darwin, home-manager, sops-nix, arion, ... }:
let let
mkNixos = hostname: mkNixos = hostname:
nixpkgs.lib.nixosSystem { nixpkgs.lib.nixosSystem {
@ -31,6 +36,7 @@
./hosts/${hostname} ./hosts/${hostname}
sops-nix.nixosModules.sops sops-nix.nixosModules.sops
home-manager.nixosModules.home-manager home-manager.nixosModules.home-manager
arion.nixosModules.arion
{ {
home-manager = { home-manager = {
useGlobalPkgs = true; useGlobalPkgs = true;

View file

@ -1,10 +1,17 @@
{ pkgs, ... }: { pkgs, ... }:
{ {
# git-lfs на PATH (home-manager пакеты лежат в /etc/profiles/per-user/mikl/bin,
# он уже в home.sessionPath). lfs.package = null заставляет модуль генерить
# каноничную строку "git-lfs clean -- %f" вместо абсолютного nix-store пути —
# иначе git-lfs 3.7+ сыплет warning'ом при каждом вызове.
home.packages = [ pkgs.git-lfs ];
programs.git = { programs.git = {
enable = true; enable = true;
# Git LFS support # Git LFS support
lfs.enable = true; lfs.enable = true;
lfs.package = null;
settings = { settings = {
user = { user = {

View file

@ -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.<name>.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: добавить когда основные сервисы мигрированы.
};
}

85
hosts/heather/caddy.nix Normal file
View file

@ -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
'';
};
};
}

View file

@ -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 # https://git.iscg.dev/mikl/nix-config
# #
# Перенесено из muscari (переименование 2026-07-11). # Перенесено из muscari (переименование 2026-07-11).
@ -6,21 +6,48 @@
# Образ NixOS собирается как qcow2 (см. ./image.nix, make-disk-image.nix) и # Образ NixOS собирается как qcow2 (см. ./image.nix, make-disk-image.nix) и
# заливается как boot disk в YC. НЕ через nixos-anywhere — kexec на YC хрупкий. # заливается как boot disk в YC. НЕ через nixos-anywhere — kexec на YC хрупкий.
# См. memory/yandex-nixos-qcow2.md. # См. 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, ... }: { config, pkgs, lib, ... }:
{ {
imports = [ imports = [
./image.nix # YC guest profile + qcow2 build target (заменяет disk-config.nix) ./image.nix # YC guest profile + qcow2 build target
./k3s.nix ./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 = { networking = {
hostName = "heather"; hostName = "heather";
# Firewall выключен в ./image.nix (полагаемся на YC Security Groups).
# Порты тут только для документации — реально открывает YC SG.
firewall.allowedTCPPorts = [ firewall.allowedTCPPorts = [
22 # SSH 22 # SSH
80 # HTTP (Ingress через Traefik) 80 # HTTP (Caddy → redirect to HTTPS)
443 # HTTPS (Ingress через Traefik) 443 # HTTPS (Caddy, TLS termination)
# 6443 не открываем — kubectl через SSH-туннель 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) — облачный профиль. # useDHCP задаёт ./image.nix (mkDefault) — облачный профиль.
@ -47,4 +74,4 @@
# После первой установки 25.11 не трогаем (NixOS convention) # После первой установки 25.11 не трогаем (NixOS convention)
system.stateVersion = "25.11"; system.stateVersion = "25.11";
} }

29
hosts/heather/docker.nix Normal file
View file

@ -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
];
}

View file

@ -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)
];
}

View file

@ -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 -"
];
}