nix-config/hosts/heather/arion-compose.nix

468 lines
20 KiB
Nix
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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)
server: heather
container: forgejo
- Syncthing:
icon: syncthing.png
href: https://sync.iscg.dev
description: File sync GUI (systemd, not container)
- tuwunel-admin:
icon: matrix.png
href: https://admin.iscg.dev
description: Matrix admin room UI (systemd, not container)
- Pi Web:
icon: /images/pi.png
href: https://pi.iscg.dev
description: pi coding agent web UI (systemd, not container)
- Communication:
- Tuwunel:
icon: matrix.png
href: https://iscg.dev
description: Matrix homeserver (iscg.dev)
server: heather
container: tuwunel
- Cinny:
icon: cinny.png
href: https://chat.iscg.dev
description: Matrix web client
server: heather
container: cinny
- Jitsi Meet:
icon: jitsi-meet.png
href: https://jitsi.iscg.dev
description: Video conferencing
server: heather
container: jitsi-web
- Productivity:
- Silverbullet:
icon: silverbullet.png
href: https://note.iscg.dev
description: Notes wiki (herbarium)
server: heather
container: silverbullet
- Teable:
icon: /images/teable.png
href: https://teable.iscg.dev
description: No-code database
server: heather
container: teable
'';
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
# Live container status: green dot for healthy containers (docker-socket-proxy).
statusStyle: dot
# 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:
- abbr: Nx
href: https://git.iscg.dev/mikl/nix-config
'';
# Docker integration: live container status (green dot / UP) per service.
# docker-socket-proxy exposes read-only /containers/json. homepage talks to it
# over the compose network (service name `dockerproxy`), no root on docker.sock.
homepageDockerYaml = pkgs.writeText "homepage-docker.yaml" ''
heather:
host: dockerproxy
port: 2375
'';
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";
};
};
# === Docker socket proxy (read-only, for homepage live container status) ===
# tecnativa/docker-socket-proxy: whitelisted read-only subset of Docker API.
# CONTAINERS=1 → only /containers/json + /containers/{id}/json. POST=0 →
# no mutations. homepage connects over compose network (host: dockerproxy).
# Avoids mounting docker.sock directly into homepage (which would need root).
dockerproxy = {
service.image = "ghcr.io/tecnativa/docker-socket-proxy:latest";
service.container_name = "dockerproxy";
service.restart = "unless-stopped";
service.volumes = [
"/var/run/docker.sock:/var/run/docker.sock:ro"
];
service.environment = {
CONTAINERS = "1";
POST = "0";
SERVICES = "1";
TASKS = "1";
};
};
# === 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"
"${homepageDockerYaml}:/app/config/docker.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: добавить когда основные сервисы мигрированы.
};
}