diet: заведён плейбук, периметр и бэкапы
- playbook-diet.yml и files/diet: образ через app_image, uid=gid=1111, каталоги config/data/cache, backup-targets со строкой data/ - блок сайта в Caddy разделён: /api/* идёт мимо Authelia под собственную аутентификацию, клиентские Remote-* срезаются на уровне сайта - правило Authelia one_factor без subject: пользователи шире группы admins
This commit is contained in:
@@ -77,6 +77,7 @@ uv run ansible-galaxy install --role-file requirements.yml
|
|||||||
- `playbook-transcriber.yml` — транскрибация (образ собирается локально ролью `app_image`).
|
- `playbook-transcriber.yml` — транскрибация (образ собирается локально ролью `app_image`).
|
||||||
- `playbook-wanderer.yml` — пешие маршруты.
|
- `playbook-wanderer.yml` — пешие маршруты.
|
||||||
- `playbook-remembos.yml` — интервальное повторение.
|
- `playbook-remembos.yml` — интервальное повторение.
|
||||||
|
- `playbook-diet.yml` — дневник питания (Go + SQLite, образ собирается локально ролью `app_image`; сайт разделён — `/api/*` мимо Authelia под собственную аутентификацию, разбор еды моделями через Bifrost).
|
||||||
- `playbook-tuwunel.yml` — Matrix-сервер (Tuwunel) с federation-делегацией на apex-домен.
|
- `playbook-tuwunel.yml` — Matrix-сервер (Tuwunel) с federation-делегацией на apex-домен.
|
||||||
- `playbook-tududi.yml` — планировщик задач (SQLite, OIDC через Authelia, AI-фичи через Bifrost).
|
- `playbook-tududi.yml` — планировщик задач (SQLite, OIDC через Authelia, AI-фичи через Bifrost).
|
||||||
- `playbook-bifrost.yml` — LLM-шлюз (OpenAI-совместимый) для AI-фич других приложений, провайдер DeepSeek.
|
- `playbook-bifrost.yml` — LLM-шлюз (OpenAI-совместимый) для AI-фич других приложений, провайдер DeepSeek.
|
||||||
|
|||||||
@@ -747,6 +747,14 @@ access_control:
|
|||||||
subject: "group:admins"
|
subject: "group:admins"
|
||||||
policy: "two_factor"
|
policy: "two_factor"
|
||||||
|
|
||||||
|
# diet — дневник питания. Без subject и одним фактором намеренно: круг
|
||||||
|
# пользователей шире админов (семья), а второй фактор кого-то отпугнёт.
|
||||||
|
# Кого периметр пропустил, тому приложение заводит запись при первом входе:
|
||||||
|
# своего списка разрешённых у него нет. Правило касается только клиента и
|
||||||
|
# POST /auth/session — /api/* до Authelia не доходит вовсе.
|
||||||
|
- domain: "diet.vakhrushev.me"
|
||||||
|
policy: "one_factor"
|
||||||
|
|
||||||
- domain: "rssbridge.vakhrushev.me"
|
- domain: "rssbridge.vakhrushev.me"
|
||||||
subject: "group:admins"
|
subject: "group:admins"
|
||||||
policy: "one_factor"
|
policy: "one_factor"
|
||||||
|
|||||||
@@ -225,6 +225,38 @@ remembos.vakhrushev.me {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# diet — дневник питания. Сайт разделён, в отличие от соседей: у ассистента и
|
# diet — дневник питания. Сайт разделён, в отличие от соседей: у ассистента и
|
||||||
|
# скриптов своя аутентификация (личный токен), через Authelia они пройти не могут.
|
||||||
|
diet.vakhrushev.me {
|
||||||
|
tls anwinged@ya.ru
|
||||||
|
import common
|
||||||
|
|
||||||
|
# Заголовки личности ставит только Authelia. Клиентские срезаются на всех
|
||||||
|
# маршрутах разом, до всякой маршрутизации: приложение источник запроса не
|
||||||
|
# проверяет, поэтому подделанный заголовок — это подделанная личность.
|
||||||
|
# copy_headers ниже живёт внутри forward_auth и /api/* не покрывает.
|
||||||
|
request_header -Remote-User
|
||||||
|
request_header -Remote-Groups
|
||||||
|
request_header -Remote-Email
|
||||||
|
request_header -Remote-Name
|
||||||
|
|
||||||
|
# API: своя аутентификация (сессионная кука или личный токен), периметр
|
||||||
|
# в неё не вмешивается.
|
||||||
|
handle /api/* {
|
||||||
|
reverse_proxy diet_app:8080
|
||||||
|
}
|
||||||
|
|
||||||
|
# Всё остальное — клиент и вход — за Authelia.
|
||||||
|
handle {
|
||||||
|
forward_auth authelia_app:9091 {
|
||||||
|
uri /api/authz/forward-auth
|
||||||
|
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
|
||||||
|
}
|
||||||
|
|
||||||
|
reverse_proxy diet_app:8080
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
calibre.vakhrushev.me {
|
||||||
tls anwinged@ya.ru
|
tls anwinged@ya.ru
|
||||||
import common
|
import common
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,11 @@
|
|||||||
description: Интервальное повторение
|
description: Интервальное повторение
|
||||||
icon: mdi-brain
|
icon: mdi-brain
|
||||||
siteMonitor: http://remembos_app:8080
|
siteMonitor: http://remembos_app:8080
|
||||||
|
- Diet:
|
||||||
|
href: https://diet.vakhrushev.me
|
||||||
|
description: Дневник питания
|
||||||
|
icon: mdi-food-apple
|
||||||
|
siteMonitor: http://diet_app:8080
|
||||||
- Matrix:
|
- Matrix:
|
||||||
href: https://matrix.vakhrushev.me
|
href: https://matrix.vakhrushev.me
|
||||||
description: Мессенджер (Tuwunel)
|
description: Мессенджер (Tuwunel)
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# Diet — конфигурация. Рендерится плейбуком playbook-diet.yml.
|
||||||
|
#
|
||||||
|
# Здесь только обязательные поля и те умолчания, что меняются осознанно:
|
||||||
|
# полный справочник полей с их умолчаниями — config.example.toml в репозитории
|
||||||
|
# приложения. Разбор строгий: неизвестный ключ в этом файле роняет старт.
|
||||||
|
|
||||||
|
[general]
|
||||||
|
# Часовой пояс новых пользователей: по нему режутся сутки в итогах дня.
|
||||||
|
# Хранение времени и журнал всегда в UTC, его это не трогает.
|
||||||
|
timezone = "Europe/Moscow"
|
||||||
|
|
||||||
|
[http]
|
||||||
|
# Порт наружу не публикуется: в сеть смотрит только Caddy, а внутрь
|
||||||
|
# web_proxy_network приложение видно по имени контейнера diet_app.
|
||||||
|
listen = "0.0.0.0:8080"
|
||||||
|
|
||||||
|
[storage]
|
||||||
|
# data/ приложения смонтирован на /var/lib/diet — единственное, что уезжает
|
||||||
|
# в бэкап (backup-targets).
|
||||||
|
db_path = "/var/lib/diet/diet.db"
|
||||||
|
|
||||||
|
[storage.files]
|
||||||
|
# Снимки тарелок и этикеток: каталог на диске этой же машины.
|
||||||
|
type = "local"
|
||||||
|
dir = "/var/lib/diet/files"
|
||||||
|
|
||||||
|
[cache.images]
|
||||||
|
# cache/ смонтирован на /var/cache/diet: содержимое восстановимо целиком,
|
||||||
|
# в бэкап не идёт и сносится в любой момент.
|
||||||
|
type = "local"
|
||||||
|
dir = "/var/cache/diet/images"
|
||||||
|
|
||||||
|
[llm]
|
||||||
|
# Шлюз в общей сети; версию API ("/v1/chat/completions") приложение
|
||||||
|
# дописывает само, поэтому адрес указывается корнем.
|
||||||
|
base_url = "http://bifrost_app:8080/openai"
|
||||||
|
|
||||||
|
# Bifrost в закрытой сети ключа не спрашивает: governance у него не включён
|
||||||
|
# и виртуальных ключей нет. Пустая строка — приложение ходит без авторизации.
|
||||||
|
# Появится ключ — значение приедет из vault, а файл станет секретным:
|
||||||
|
# тогда задаче рендера нужны no_log и права 0600.
|
||||||
|
api_key = ""
|
||||||
|
|
||||||
|
[llm.recognition]
|
||||||
|
# Снимки читает vision-модель через bothub — кандидат по умолчанию из замера
|
||||||
|
# на живых фотографиях (docs/research/2026-08-31-vision-recognition.md
|
||||||
|
# в репозитории приложения): точность не хуже моделей в девяносто раз дороже.
|
||||||
|
type = "openai-compat"
|
||||||
|
model = "bothub/gemini-3.1-flash-lite"
|
||||||
|
|
||||||
|
[llm.composition]
|
||||||
|
# Сведение позиций. Модель медленнее соседки по шлюзу, зато честно говорит
|
||||||
|
# «не знаю» о незнакомом продукте вместо выдуманного КБЖУ. Разбор фоновый,
|
||||||
|
# человек ответа у экрана не ждёт, поэтому ожидание здесь дешевле выдумки.
|
||||||
|
type = "openai-compat"
|
||||||
|
model = "bothub/deepseek-v4-flash"
|
||||||
|
|
||||||
|
# Замер давал этой модели 19–70 секунд на ответ, то есть умолчание в минуту
|
||||||
|
# обрывало бы её самые долгие ответы, засчитывая их временной неудачей.
|
||||||
|
timeout = "2m"
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Образ собирается локально и доставляется на сервер ролью app_image
|
||||||
|
# (см. playbook-diet.yml): тег — случайный BUILD_ID каждого прогона,
|
||||||
|
# в реестре образа нет, поэтому pull_policy: never.
|
||||||
|
#
|
||||||
|
# В образе нет оболочки (distroless): зайти внутрь работающего контейнера
|
||||||
|
# нечем, разбор идёт по журналу и GET /healthz.
|
||||||
|
|
||||||
|
services:
|
||||||
|
|
||||||
|
diet_app:
|
||||||
|
image: "{{ app_name }}:{{ app_image_tag }}"
|
||||||
|
container_name: diet_app
|
||||||
|
restart: unless-stopped
|
||||||
|
pull_policy: never
|
||||||
|
user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}"
|
||||||
|
networks:
|
||||||
|
- "web_proxy_network"
|
||||||
|
volumes:
|
||||||
|
- "{{ config_dir }}:/config:ro"
|
||||||
|
- "{{ data_dir }}:/var/lib/diet"
|
||||||
|
- "{{ cache_dir }}:/var/cache/diet"
|
||||||
|
command:
|
||||||
|
- "--config"
|
||||||
|
- "/config/config.toml"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
web_proxy_network:
|
||||||
|
external: true
|
||||||
@@ -37,6 +37,9 @@
|
|||||||
- name: "Configure remembos"
|
- name: "Configure remembos"
|
||||||
ansible.builtin.import_playbook: playbook-remembos.yml
|
ansible.builtin.import_playbook: playbook-remembos.yml
|
||||||
|
|
||||||
|
- name: "Configure diet"
|
||||||
|
ansible.builtin.import_playbook: playbook-diet.yml
|
||||||
|
|
||||||
- name: "Configure apprise"
|
- name: "Configure apprise"
|
||||||
ansible.builtin.import_playbook: playbook-apprise.yml
|
ansible.builtin.import_playbook: playbook-apprise.yml
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
---
|
||||||
|
- name: "Configure diet application"
|
||||||
|
hosts: all
|
||||||
|
|
||||||
|
vars:
|
||||||
|
app_name: "diet"
|
||||||
|
app_user: "{{ app_name }}"
|
||||||
|
app_owner_uid: 1111
|
||||||
|
app_owner_gid: 1111
|
||||||
|
base_dir: "{{ (application_dir, app_name) | path_join }}"
|
||||||
|
config_dir: "{{ (base_dir, 'config') | path_join }}"
|
||||||
|
data_dir: "{{ (base_dir, 'data') | path_join }}"
|
||||||
|
cache_dir: "{{ (base_dir, 'cache') | path_join }}"
|
||||||
|
config_file: "{{ (config_dir, 'config.toml') | path_join }}"
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: "Create user and environment"
|
||||||
|
ansible.builtin.import_role:
|
||||||
|
name: owner
|
||||||
|
vars:
|
||||||
|
owner_name: "{{ app_user }}"
|
||||||
|
owner_uid: "{{ app_owner_uid }}"
|
||||||
|
owner_gid: "{{ app_owner_gid }}"
|
||||||
|
|
||||||
|
# Подкаталоги files и images заводятся здесь, а не приложением: оно
|
||||||
|
# проверяет их на запись при старте и без них не поднимается вовсе.
|
||||||
|
# Всё, что появится внутри них, приложение создаёт само.
|
||||||
|
- name: "Create application internal directories"
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ item }}"
|
||||||
|
state: "directory"
|
||||||
|
owner: "{{ app_user }}"
|
||||||
|
group: "{{ app_user }}"
|
||||||
|
mode: "0750"
|
||||||
|
loop:
|
||||||
|
- "{{ base_dir }}"
|
||||||
|
- "{{ config_dir }}"
|
||||||
|
- "{{ data_dir }}"
|
||||||
|
- "{{ (data_dir, 'files') | path_join }}"
|
||||||
|
- "{{ cache_dir }}"
|
||||||
|
- "{{ (cache_dir, 'images') | path_join }}"
|
||||||
|
|
||||||
|
# Секретов в файле пока нет: ключ к шлюзу пустой, а больше приложению
|
||||||
|
# ничего секретного не нужно. Поэтому без no_log — правку конфига видно
|
||||||
|
# в --diff. Появится ключ из vault — вернуть no_log и права 0600.
|
||||||
|
- name: "Copy config"
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: "./files/{{ app_name }}/config.template.toml"
|
||||||
|
dest: "{{ config_file }}"
|
||||||
|
owner: "{{ app_user }}"
|
||||||
|
group: "{{ app_user }}"
|
||||||
|
mode: "0640"
|
||||||
|
notify: "Restart application"
|
||||||
|
|
||||||
|
# Данные — только data/: config/ восстанавливает этот плейбук, cache/
|
||||||
|
# приложение пересчитывает само.
|
||||||
|
- name: "Create backup targets file"
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: "{{ base_dir }}/backup-targets"
|
||||||
|
content: |
|
||||||
|
{{ data_dir }}
|
||||||
|
owner: "{{ app_user }}"
|
||||||
|
group: "{{ app_user }}"
|
||||||
|
mode: "0640"
|
||||||
|
|
||||||
|
# Образ собирается на control-хосте самим репозиторием diet (контракт —
|
||||||
|
# `task image`) и едет на сервер через docker save/load, без реестра.
|
||||||
|
# Роль выставляет факт app_image_tag — его подхватывает docker-compose.
|
||||||
|
- name: "Build and deliver application image"
|
||||||
|
ansible.builtin.import_role:
|
||||||
|
name: "app_image"
|
||||||
|
vars:
|
||||||
|
app_image_name: "{{ app_name }}"
|
||||||
|
app_image_src_dir: '{{ (playbook_dir, "..", "diet") | path_join }}'
|
||||||
|
|
||||||
|
- name: "Copy docker compose file"
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: "./files/{{ app_name }}/docker-compose.template.yml"
|
||||||
|
dest: "{{ base_dir }}/docker-compose.yml"
|
||||||
|
owner: "{{ app_user }}"
|
||||||
|
group: "{{ app_user }}"
|
||||||
|
mode: "0640"
|
||||||
|
|
||||||
|
- name: "Run application with docker compose"
|
||||||
|
community.docker.docker_compose_v2:
|
||||||
|
project_src: "{{ base_dir }}"
|
||||||
|
state: "present"
|
||||||
|
remove_orphans: true
|
||||||
|
tags:
|
||||||
|
- run-app
|
||||||
|
|
||||||
|
handlers:
|
||||||
|
# Приложение читает конфиг только при старте, а `state: present` не
|
||||||
|
# пересоздаёт контейнер, если docker-compose.yml не изменился, — поэтому
|
||||||
|
# правку конфига применяет только явный рестарт.
|
||||||
|
- name: "Restart application"
|
||||||
|
community.docker.docker_compose_v2:
|
||||||
|
project_src: "{{ base_dir }}"
|
||||||
|
state: "restarted"
|
||||||
|
tags:
|
||||||
|
- run-app
|
||||||
Reference in New Issue
Block a user