Background photo (mounted ro from /var/lib/homepage-images into /app/public/images). cardBlur: md for matte semi-transparent service cards. opacity 70 to soften the photo.
419 lines
19 KiB
Nix
419 lines
19 KiB
Nix
# 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). Контейнеры НЕ выставлены наружу
|
||
# напрямую (кроме forgejo git ssh на :2222).
|
||
#
|
||
# 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 → teable →
|
||
# silverbullet. Ниже forgejo как первый proof-of-concept, остальные добавим
|
||
# по мере миграции (секции заготовлены, закомментированы до теста).
|
||
|
||
{ pkgs, ... }:
|
||
|
||
let
|
||
# Homepage (gethomepage) dashboard configs — baked via pkgs.writeText,
|
||
# bind-mounted ro в контейнер. Полностью декларативно, версионировано в git.
|
||
# Документация: https://gethomepage.dev/configs/
|
||
homepageServicesYaml = pkgs.writeText "homepage-services.yaml" ''
|
||
- Infrastructure:
|
||
- Forgejo:
|
||
icon: forgejo.png
|
||
href: https://git.iscg.dev
|
||
description: Git forge (ssh :2222)
|
||
- Syncthing:
|
||
icon: syncthing.png
|
||
href: https://sync.iscg.dev
|
||
description: File sync GUI
|
||
- tuwunel-admin:
|
||
icon: matrix.png
|
||
href: https://admin.iscg.dev
|
||
description: Matrix admin room UI
|
||
- Communication:
|
||
- Tuwunel:
|
||
icon: matrix.png
|
||
href: https://iscg.dev
|
||
description: Matrix homeserver (iscg.dev)
|
||
- Cinny:
|
||
icon: cinny.png
|
||
href: https://chat.iscg.dev
|
||
description: Matrix web client
|
||
- Jitsi Meet:
|
||
icon: jitsi.png
|
||
href: https://jitsi.iscg.dev
|
||
description: Video conferencing
|
||
- Productivity:
|
||
- Silverbullet:
|
||
icon: silverbullet.png
|
||
href: https://note.iscg.dev
|
||
description: Notes wiki (herbarium)
|
||
- Teable:
|
||
icon: teable.png
|
||
href: https://teable.iscg.dev
|
||
description: No-code database
|
||
'';
|
||
|
||
homepageSettingsYaml = pkgs.writeText "homepage-settings.yaml" ''
|
||
title: iscg.dev
|
||
theme: dark
|
||
color: slate
|
||
headerStyle: boxed
|
||
layout:
|
||
Infrastructure:
|
||
Communication:
|
||
Productivity:
|
||
quicklaunch:
|
||
searchDescriptions: true
|
||
hideInternetSearch: true
|
||
hideVersion: true
|
||
# Background photo (mounted ro at /app/public/images/ from /var/lib/homepage-images).
|
||
# cardBlur = frosted-glass / matte semi-transparent service & bookmark cards.
|
||
# NB: cardBlur incompatible with background blur/saturate/brightness filters,
|
||
# so only `opacity` is used here to soften the photo.
|
||
background:
|
||
image: /images/background.jpg
|
||
opacity: 70
|
||
cardBlur: md
|
||
'';
|
||
|
||
homepageBookmarksYaml = pkgs.writeText "homepage-bookmarks.yaml" ''
|
||
- Forge:
|
||
- nix-config:
|
||
- https://git.iscg.dev/mikl/nix-config
|
||
'';
|
||
|
||
in
|
||
{
|
||
# Обязательное имя проекта (COMPOSE_PROJECT_NAME). Объекты префиксуются им:
|
||
# heather_forgejo-data и т.д.
|
||
project.name = "heather";
|
||
|
||
# --- Named volumes (декларируем явно, никаких анонимных хэшей) ---
|
||
# docker volume create <name> при первом up. Видно в `docker volume ls`.
|
||
docker-compose.volumes = {
|
||
forgejo-data = {};
|
||
tuwunel-data = {};
|
||
tuwunel-media-cache = {};
|
||
teable-data = {};
|
||
teable-db = {};
|
||
teable-redis = {};
|
||
teable-assets = {};
|
||
# jitsi-стек (minimal, per official docker-compose.yml)
|
||
jitsi-web-config = {};
|
||
jitsi-prosody-config = {};
|
||
jitsi-prosody-data = {};
|
||
jitsi-jicofo-config = {};
|
||
jitsi-jvb-config = {};
|
||
jitsi-transcripts = {};
|
||
};
|
||
|
||
# Custom network for jitsi (FQDN-based comms, prosody alias xmpp.meet.jitsi)
|
||
networks.meet-jitsi.driver = "bridge";
|
||
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 (homeserver) + cinny (web client) ===
|
||
# iscg.dev → tuwunel:8008 (matrix client API + federation + well-known).
|
||
# chat.iscg.dev → cinny:80. admin.iscg.dev → tuwunel-admin (systemd, не контейнер,
|
||
# см. tuwunel-admin.nix — бинарник статический, публичного docker-образа нет).
|
||
# Поднято с нуля 2026-07-13: пользователей/медиа со старого jul11 НЕ переносим.
|
||
# Медиа — на Yandex Object Storage (S3-совместимый), bucket iscg-tuwunel-media.
|
||
# Конфиг tuwunel.toml (с s3 secret + registration token) вне git:
|
||
# /var/lib/tuwunel-secrets/tuwunel.toml (root:docker 0640, bind-mount ro)
|
||
# Cinny config: /var/lib/tuwunel-secrets/cinny-config.json (bind-mount ro).
|
||
tuwunel = {
|
||
service.image = "ghcr.io/matrix-construct/tuwunel:v1.7.1";
|
||
service.container_name = "tuwunel";
|
||
service.restart = "unless-stopped";
|
||
service.volumes = [
|
||
"tuwunel-data:/var/lib/tuwunel"
|
||
"tuwunel-media-cache:/var/cache/tuwunel"
|
||
"/var/lib/tuwunel-secrets/tuwunel.toml:/etc/tuwunel/tuwunel.toml:ro"
|
||
];
|
||
service.ports = [ "127.0.0.1:8008:8008" ];
|
||
service.environment = {
|
||
TUWUNEL_CONFIG = "/etc/tuwunel/tuwunel.toml";
|
||
};
|
||
};
|
||
cinny = {
|
||
service.image = "ajbura/cinny:latest";
|
||
service.container_name = "cinny";
|
||
service.restart = "unless-stopped";
|
||
service.volumes = [
|
||
"/var/lib/tuwunel-secrets/cinny-config.json:/app/config.json:ro"
|
||
];
|
||
service.ports = [ "127.0.0.1:8080:80" ];
|
||
};
|
||
|
||
# === Xray (VLESS + Reality, :8443) ===
|
||
# Прокси-сервер для обхода блокировок. Не относится к iscg.dev-сервисам:
|
||
# НЕ за Caddy, публичный порт 8443 напрямую. Reality маскируется под
|
||
# www.microsoft.com (чужой TLS, своего домена/cert не нужно).
|
||
# Конфиг с UUID + Reality privateKey в /var/lib/xray-secrets/config.json
|
||
# (вне git, root:docker 0640). Клиент: v2RayTun / v2rayN / etc.
|
||
# Секреты в pass iscg.dev/xray/{uuid,reality-private-key,reality-public-key}.
|
||
xray = {
|
||
service.image = "ghcr.io/xtls/xray-core:latest";
|
||
service.container_name = "xray";
|
||
service.restart = "unless-stopped";
|
||
# Образ по умолчанию читает -confdir /usr/local/etc/xray/ (split-конфиги).
|
||
# Переопределяем на единый config.json с VLESS+Reality.
|
||
service.command = [ "-config" "/etc/xray/config.json" ];
|
||
service.volumes = [
|
||
"/var/lib/xray-secrets/config.json:/etc/xray/config.json:ro"
|
||
];
|
||
service.ports = [ "8443:8443" ];
|
||
};
|
||
|
||
# === Teable (teable.iscg.dev) ===
|
||
# Стек: app + postgres + redis. Данные перенесены с iscg-dev (2026-07-12):
|
||
# - postgres восстановлен из pg_dump (18M, consistent, без остановки источника)
|
||
# - .assets (3.2G, 4392 файлов) в named volume heather_teable-assets
|
||
# - teable-data (local-storage) пустой — наполняется при работе
|
||
# - redis пустой — кэш, перестраивается
|
||
# Секреты (POSTGRES_PASSWORD, SECRET_KEY) новые, в pass iscg.dev/teable/.
|
||
# НЕ пушить env-файл в forgejo — secrets.
|
||
teable = {
|
||
# Пин по digest: :latest (release 2198, 2026-07-11) сломал роутинг —
|
||
# Next.js отдаёт 404 на / и /health, assets под /plugin/. Образ на iscg-dev
|
||
# (март 2026, digest baddd95f) работает: / → 307 → /space. Пиним его.
|
||
# TODO: разобраться с роутингом новой версии (возможно нужны NEXT_PUBLIC_* env).
|
||
service.image = "teableio/teable@sha256:baddd95f6c944334a30d17bddb209c4af04b52e619811a4c3997ae3adb8ad42e";
|
||
service.container_name = "teable";
|
||
service.restart = "unless-stopped";
|
||
service.ports = [ "127.0.0.1:2345:3000" ];
|
||
service.volumes = [
|
||
"teable-data:/app/.local-storage"
|
||
"teable-assets:/app/.assets"
|
||
];
|
||
service.depends_on = [ "teable-db" "teable-redis" ];
|
||
service.env_file = [ "/var/lib/teable-secrets/teable.env" ];
|
||
service.environment = {
|
||
POSTGRES_HOST = "teable-db";
|
||
POSTGRES_PORT = "5432";
|
||
POSTGRES_DB = "teable";
|
||
POSTGRES_USER = "teable";
|
||
REDIS_URL = "redis://teable-redis:6379";
|
||
PUBLIC_DOMAIN = "teable.iscg.dev";
|
||
PUBLIC_ORIGIN = "https://teable.iscg.dev";
|
||
};
|
||
};
|
||
teable-db = {
|
||
service.image = "postgres:15-alpine";
|
||
service.container_name = "teable-db";
|
||
service.restart = "unless-stopped";
|
||
service.volumes = [ "teable-db:/var/lib/postgresql/data" ];
|
||
service.env_file = [ "/var/lib/teable-secrets/teable.env" ];
|
||
};
|
||
teable-redis = {
|
||
service.image = "redis:alpine";
|
||
service.container_name = "teable-redis";
|
||
service.restart = "unless-stopped";
|
||
service.command = "redis-server --appendonly yes";
|
||
service.volumes = [ "teable-redis:/data" ];
|
||
};
|
||
|
||
# === Jitsi Meet (jitsi.iscg.dev) ===
|
||
# Installed from scratch 2026-07-13 per official docs:
|
||
# https://jitsi.github.io/handbook/docs/devops-guide/devops-guide-docker
|
||
# Repo: https://github.com/jitsi/docker-jitsi-meet (release stable-11031)
|
||
# Minimal stack: web/prosody/jicofo/jvb. No coturn/jigasi/jibri.
|
||
# - Images: jitsi/* (docker hub; GHCR ghcr.io/jitsi/* not accessible)
|
||
# - read_only + tmpfs from official compose NOT supported by arion schema (skipped)
|
||
# - Named volumes (config + prosody data separate)
|
||
# - Reverse proxy: DISABLE_HTTPS=1, Caddy terminates TLS
|
||
# - JVB_ADVERTISE_IPS=51.250.45.111, :10000/udp public
|
||
# - Auth: internal (ENABLE_AUTH=1, AUTH_TYPE=internal). No GUESTS
|
||
# (template bug: guest domain lacks websocket module → disconnect).
|
||
# - Secrets in /var/lib/jitsi-secrets/jitsi.env (root:docker 0640)
|
||
jitsi-web = {
|
||
service.image = "jitsi/web:stable-11031";
|
||
service.container_name = "jitsi-web";
|
||
service.restart = "unless-stopped";
|
||
service.ports = [ "127.0.0.1:8000:80" ];
|
||
service.volumes = [
|
||
"jitsi-web-config:/config"
|
||
"jitsi-transcripts:/usr/share/jitsi-meet/transcripts"
|
||
];
|
||
service.depends_on = [ "jitsi-jvb" ];
|
||
service.networks = [ "meet-jitsi" ];
|
||
service.env_file = [ "/var/lib/jitsi-secrets/jitsi.env" ];
|
||
service.environment = {
|
||
PUBLIC_URL = "https://jitsi.iscg.dev";
|
||
TZ = "Europe/Moscow";
|
||
HTTP_PORT = "8000";
|
||
DISABLE_HTTPS = "1";
|
||
ENABLE_HTTP_REDIRECT = "0";
|
||
ENABLE_HSTS = "0";
|
||
ENABLE_LETSENCRYPT = "0";
|
||
JVB_ADVERTISE_IPS = "51.250.45.111";
|
||
DOCKER_HOST_ADDRESS = "51.250.45.111";
|
||
ENABLE_AUTH = "1";
|
||
AUTH_TYPE = "internal";
|
||
ENABLE_XMPP_WEBSOCKET = "1";
|
||
ENABLE_COLIBRI_WEBSOCKET = "1";
|
||
# TURN (coturn) — relay for clients behind symmetric NAT
|
||
TURN_HOST = "51.250.45.111";
|
||
TURN_PORT = "3478";
|
||
TURN_TRANSPORT = "udp";
|
||
# Guest access: unauthenticated users join via guest domain, wait in
|
||
# lobby until a moderator (mikl) authenticates and admits them.
|
||
ENABLE_GUESTS = "1";
|
||
};
|
||
};
|
||
|
||
jitsi-prosody = {
|
||
service.image = "jitsi/prosody:stable-11031";
|
||
service.container_name = "jitsi-prosody";
|
||
service.restart = "unless-stopped";
|
||
service.expose = [ "5222" "5269" "5347" "5280" ];
|
||
service.volumes = [
|
||
"jitsi-prosody-config:/config"
|
||
"jitsi-prosody-data:/var/lib/prosody"
|
||
# Patched prosody template: guest VirtualHost gets "websocket" module
|
||
# (upstream bug — guest domain lacks it → wss disconnect loop).
|
||
# Patched file at /var/lib/jitsi-secrets/jitsi-meet.cfg.lua.template
|
||
"/var/lib/jitsi-secrets/jitsi-meet.cfg.lua.template:/defaults/conf.d/jitsi-meet.cfg.lua:ro"
|
||
];
|
||
service.networks.meet-jitsi.aliases = [ "xmpp.meet.jitsi" ];
|
||
service.env_file = [ "/var/lib/jitsi-secrets/jitsi.env" ];
|
||
service.environment = {
|
||
TZ = "Europe/Moscow";
|
||
PUBLIC_URL = "https://jitsi.iscg.dev";
|
||
ENABLE_AUTH = "1";
|
||
AUTH_TYPE = "internal";
|
||
ENABLE_XMPP_WEBSOCKET = "1";
|
||
ENABLE_IPV6 = "0";
|
||
ENABLE_GUESTS = "1";
|
||
};
|
||
};
|
||
|
||
jitsi-jicofo = {
|
||
service.image = "jitsi/jicofo:stable-11031";
|
||
service.container_name = "jitsi-jicofo";
|
||
service.restart = "unless-stopped";
|
||
service.ports = [ "127.0.0.1:8888:8888" ];
|
||
service.volumes = [ "jitsi-jicofo-config:/config" ];
|
||
service.depends_on = [ "jitsi-prosody" ];
|
||
service.networks = [ "meet-jitsi" ];
|
||
service.env_file = [ "/var/lib/jitsi-secrets/jitsi.env" ];
|
||
service.environment = {
|
||
TZ = "Europe/Moscow";
|
||
ENABLE_AUTH = "1";
|
||
AUTH_TYPE = "internal";
|
||
ENABLE_GUESTS = "1";
|
||
};
|
||
};
|
||
|
||
jitsi-jvb = {
|
||
service.image = "jitsi/jvb:stable-11031";
|
||
service.container_name = "jitsi-jvb";
|
||
service.restart = "unless-stopped";
|
||
service.ports = [
|
||
"10000:10000/udp"
|
||
"127.0.0.1:8080:8080"
|
||
];
|
||
service.volumes = [ "jitsi-jvb-config:/config" ];
|
||
service.depends_on = [ "jitsi-prosody" ];
|
||
service.networks.meet-jitsi.aliases = [ "jvb" ];
|
||
service.env_file = [ "/var/lib/jitsi-secrets/jitsi.env" ];
|
||
service.environment = {
|
||
TZ = "Europe/Moscow";
|
||
PUBLIC_URL = "https://jitsi.iscg.dev";
|
||
JVB_ADVERTISE_IPS = "51.250.45.111";
|
||
JVB_PORT = "10000";
|
||
DOCKER_HOST_ADDRESS = "51.250.45.111";
|
||
};
|
||
};
|
||
|
||
# === Coturn (TURN relay for jitsi) ===
|
||
# Host network: real client IPs + correct NAT traversal. No port conflicts
|
||
# on heather (no system coturn). Relays RTP on 49152-65535/udp.
|
||
# YC SG already has 3478/5349/49152-65535 open. Secret TURN_STATIC_AUTH_SECRET
|
||
# in jitsi.env (shared with jitsi-web via TURN_CREDENTIALS).
|
||
# Docs: https://jitsi.github.io/handbook/docs/devops-guide/devops-guide-docker#turn-server-configuration
|
||
jitsi-coturn = {
|
||
service.image = "coturn/coturn:4.7";
|
||
service.container_name = "jitsi-coturn";
|
||
service.restart = "unless-stopped";
|
||
service.network_mode = "host";
|
||
service.env_file = [ "/var/lib/jitsi-secrets/jitsi.env" ];
|
||
service.command = "-v --log-file=stdout --realm=turn.iscg.dev --external-ip=51.250.45.111 --use-auth-secret --static-auth-secret=$${TURN_STATIC_AUTH_SECRET} --min-port=49152 --max-port=65535 --listening-port=3478 --no-tls --no-dtls --fingerprint --no-cli --simple-log";
|
||
};
|
||
|
||
# === Silverbullet (note.iscg.dev) — ИСКЛЮЧЕНИЕ: bind mount ===
|
||
# Space = /srv/syncthing/herbarium (sync'ается syncthing на устройства).
|
||
# Единственный сервис без named volume — по твоему решению, чтобы notes
|
||
# были доступны и через web UI, и через obsidian/vscode на poppy.
|
||
# Конфликты syncthing (одновременная правка) решаются через .sync-conflict.
|
||
# Данные перенесены с iscg-dev (2026-07-12), auth-файл .silverbullet.auth.json
|
||
# в составе данных — silverbullet подхватит.
|
||
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
|
||
];
|
||
service.environment = {
|
||
SB_USER = "mikl:milkingfemboys";
|
||
SB_INDEX_PAGE = "flowers";
|
||
};
|
||
};
|
||
|
||
# === Homepage (home.iscg.dev) — dashboard со всеми сервисами ===
|
||
# gethomepage/homepage: статичный список ссылок на все сервисы heather.
|
||
# Конфиги (services/settings/bookmarks.yaml) генерятся через pkgs.writeText
|
||
# в nix store и bind-mount ro. Без docker socket — без живых статусов,
|
||
# просто навигация. Образ :latest (GHCR для gethomepage доступен, в отличие
|
||
# от jitsi). BASE_URL = публичный домен.
|
||
homepage = {
|
||
service.image = "ghcr.io/gethomepage/homepage:latest";
|
||
service.container_name = "homepage";
|
||
service.restart = "unless-stopped";
|
||
service.ports = [ "127.0.0.1:3002:3000" ];
|
||
service.volumes = [
|
||
"${homepageServicesYaml}:/app/config/services.yaml:ro"
|
||
"${homepageSettingsYaml}:/app/config/settings.yaml:ro"
|
||
"${homepageBookmarksYaml}:/app/config/bookmarks.yaml:ro"
|
||
# Background image (photo), outside git (binary). See settings.yaml `background`.
|
||
"/var/lib/homepage-images:/app/public/images:ro"
|
||
];
|
||
service.environment = {
|
||
HOMEPAGE_ALLOWED_HOSTS = "home.iscg.dev";
|
||
};
|
||
};
|
||
|
||
# === Backup sidecar (последним) ===
|
||
# offen/docker-volume-backup: nightly tar.gz всех named volumes.
|
||
# Монтирует volumes read-only, шлёт в /var/backups (или S3).
|
||
# TODO: добавить когда основные сервисы мигрированы.
|
||
};
|
||
}
|