Самостоятельно размещаемая медиагалерея на Nuxt 4, Vue 3, Bun, Tailwind CSS 4 и SQLite. Изображения и короткие видео хранятся локально, администратор управляет материалами, авторами, источниками, тегами и предложениями посетителей через отдельную админку.
Нужны Bun 1.3+, Node.js 22.5+ и Git. Node.js используется Nuxt dev-сервером на Windows; production-сервер работает под Bun. FFmpeg и FFprobe устанавливаются вместе с зависимостями.
Скопируйте или клонируйте проект, перейдите в его каталог и выполните:
bun install
bun run setupОткройте созданный site.config.ts, задайте локальный site.origin, логин и длинный уникальный пароль, затем выполните:
bun run doctor
bun run devУ каждого ассета есть приватный UUID assets.id, который используется и внутренними связями, и каталогом файла; отдельного physical_id нет. Наружу отдаётся только неизменяемый base62 public_id. Публичные адреса ассетов, источников, тегов и авторов получают безопасный читаемый префикс (slug-publicId), который не хранится в БД и не вводится вручную. После переименования старый slug перенаправляется на новый, а если из названия нельзя получить читаемый ASCII-slug, URL состоит только из public ID. Прежний порядок publicId-slug не поддерживается.
Источники — основной цветной справочник с описанием, внешней ссылкой и необязательной иконкой. Теги — отдельные второстепенные серые отметки, у которых хранится только public ID и название; даты создания, цвета, иконки, описания и ссылки для них отсутствуют. Галерея фильтрует материалы по обеим группам независимо.
Сайт откроется на http://127.0.0.1:3000, админка — на /admin. Nuxt работает с HMR: пересобирать проект после каждого изменения не нужно.
Все параметры находятся в серверном site.config.ts. Файл исключён из Git и Docker-образа; образ получает его только через read-only mount. Полный безопасный шаблон — site.config.example.ts.
| Раздел | Назначение |
|---|---|
site |
Название, описание, canonical origin, необязательная HTTPS-ссылка repositoryUrl и объект публичных ссылок links |
admin |
Логин и пароль администратора |
storage |
Переносимый каталог данных; обычно ./data |
media |
Размеры загрузок, качество превью и параметры перекодирования |
suggestions |
Включение предложки, лимит файла, частота и резерв диска |
antibot |
Провайдер Turnstile либо disabled |
analytics |
Необязательный Google tag ID |
email |
SMTP, отправитель, reply-to и адрес администратора |
limits |
Периоды дедупликации, ограничение входа и число одновременно обрабатываемых медиа (maxConcurrentMediaJobs, обычно 2) |
Production не запускается с HTTP-origin, примерным доменом, путём внутри origin или слабым паролем. После редактирования конфигурации полезно запускать bun run doctor.
Укажите Measurement ID или Google tag ID, например:
analytics: {
googleTagId: 'G-XXXXXXXXXX',
},Пустая строка полностью отключает интеграцию. Скрипт загружается только после подтверждения возраста и согласия в первом диалоге; административные страницы не отслеживаются. Идентификатор публичен по своей природе, секреты в этот раздел добавлять нельзя.
Включите email.enabled и заполните host, port, secure, при необходимости username/password, а также from, replyTo и adminTo. Для порта 465 обычно используется secure: true, для 587 — false с STARTTLS.
bun run email:checkКоманда проверяет соединение и авторизацию SMTP, но не отправляет письмо. При отключённой почте публичная форма не собирает email.
Создайте виджет для текущего hostname, задайте antibot.provider: 'turnstile' и заполните site key и secret key. Secret остаётся только в site.config.ts. Для локальной разработки можно использовать disabled; production rate limits при этом продолжают действовать. При смене домена добавьте новый hostname в панели Cloudflare.
| Команда | Что делает |
|---|---|
bun run dev |
Nuxt dev-сервер с HMR |
bun run build |
Production-сборка Nitro |
bun run start |
Запуск готовой .output под Bun |
bun run setup |
Создание каталогов и site.config.ts без перезаписи |
bun run doctor |
Проверка конфига, SQLite, хранилища и FFmpeg |
bun run email:check |
Проверка SMTP без отправки письма |
bun run backup:client -- … |
Исходная версия внешнего backup-клиента |
bun run backup:build |
Сборка standalone backup-клиентов Windows/Linux |
bun run backup:import -- <папка> |
Импорт проверенной копии в чистую установку той же версии |
bun run db:generate |
Создать новую SQL-миграцию из server/db/schema.ts |
bun run db:check |
Проверить согласованность Drizzle schema и migrations |
bun run db:migrate |
Транзакционно применить ещё не выполненные миграции |
bun run check:drizzle |
Запретить возврат неявного терминатора Drizzle .execute() |
bun run lint |
ESLint |
bun run typecheck |
Проверка TypeScript/Vue |
bun run test |
Unit-тесты |
bun run test:vue |
Nuxt/Vue component-тесты |
bun run test:integration |
Интеграционные тесты Playwright |
bun run test:e2e |
Браузерные сценарии desktop/mobile |
bun run check |
Быстрый финальный набор без E2E |
bun run check:all |
Все проверки и production-сборка |
Нужны Linux, Docker Engine, Compose plugin, открытые TCP 80/443 и UDP 443, а также A/AAAA-запись домена на VPS.
- Скопируйте проект на сервер и выполните
bun run setupлибо создайтеsite.config.tsиз шаблона вручную. - Укажите реальный HTTPS
site.origin, сильный пароль и production-ключи. - Запустите:
docker compose up -d --build
docker compose ps
docker compose logs -f app caddyInit-контейнер валидирует конфигурацию и создаёт data/runtime/Caddyfile. Одноразовый сервис migrate применяет все migrations до запуска app; повторный запуск безопасен. Caddy автоматически получает HTTPS-сертификат. Наружу опубликованы только 80/443; порт приложения остаётся внутри Docker-сетей.
Обновление:
./scripts/update.shПеред первым запуском выдайте файлу право на выполнение: chmod +x scripts/update.sh. Скрипт проверяет Git/upstream и tracked-изменения, получает только fast-forward, собирает новые образы при работающем старом сайте, затем останавливает только app. После WAL checkpoint он сохраняет SQLite и JSON с version/commit в data/backups/db/, применяет migrations и запускает Compose с --wait. Caddy во время обновления не останавливается. В конце версия из /api/health сверяется с package.json.
Если migration завершается ошибкой, её транзакция откатывается и снова запускается прежний контейнер приложения. После успешной migration автоматический downgrade не выполняется: скрипт выводит путь к backup и старый commit для ручного восстановления.
Единственный номер версии сайта — поле version в package.json (SemVer). Перед каждым релизом его повышают вручную. Эта версия попадает в Nuxt appConfig, видна рядом с логотипом, а на мобильном экране располагается под публичными иконками-ссылками, и возвращается полем version из /api/health. Если в shared/release-notes.ts есть запись с тем же номером, рядом появляется пояснение с двумя или тремя короткими пунктами; без записи попап не показывается. Номер ведёт в site.repositoryUrl в новой вкладке, а при пустом значении остаётся обычным текстом. Последняя загруженная версия хранится в cult-petra:last-loaded-version:v1: вернувшемуся после обновления посетителю документированная новая версия на один визит подсвечивается малиновым огоньком, а первый визит и релизы без заметок не выделяются.
site.links — объект ссылок под названием сайта в публичной шапке. Ключ становится доступной подписью и подсказкой, а значение задаётся короткой строкой либо объектом с SVG-иконкой:
links: {
Почта: 'mailto:hello@example.com',
Телеграм: {
link: 'https://t.me/example',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="white"><path d="…"/></svg>',
},
}Разрешены только https: и mailto:. Без icon показывается стандартная иконка цепочки; пользовательский SVG проходит серверную проверку и отображается как изолированное изображение. Все ссылки открываются в новой вкладке.
Каноническая SQLite-схема находится в server/db/schema.ts, связи RQB v2 — в server/db/relations.ts, а неизменяемая история — в drizzle/. Новое изменение БД выполняется так:
bun run db:generate
# проверить новый drizzle/<timestamp>_*/migration.sql и snapshot.json
bun run db:check
bun run db:migrateSQL/снимки уже выпущенной migration не редактируют. FTS5, PRAGMA и другие SQLite-специфичные операции оформляются параметризованным sql или custom migration. drizzle-kit push в проекте запрещён: production изменяется только коммитнутыми migrations. Для первого локального перехода со старой, немигрированной БД удалите или перенесите data/db/cult.sqlite, -wal и -shm; реальных данных до этого перехода проект не хранил. Updater сам распознаёт этот единственный pre-Drizzle случай, но удаляет старую БД только после создания backup.
Один Caddy может обслуживать несколько поддоменов. Этот Compose создаёт сеть cult-petra-web. Подключите к ней контейнер второго приложения как к external network и добавьте data/runtime/sites/resume.caddy:
resume.example.com {
reverse_proxy resume-app:3000
}После этого перезапустите Caddy. Только один reverse proxy на VPS должен публиковать 80/443; контейнеры приложений наружу не открываются.
Основной внешний backup не копирует SQLite и не зависит от внутренних UUID или раскладки data/media. Отдельная Windows/Linux-машина входит через обычную admin session, получает /api/admin/backups/manifest, потоково скачивает канонические оригиналы, аватары и иконки, а затем проверяет размер и SHA-256 каждого файла. Незавершённая .partial-папка никогда не считается резервной копией.
Backup manifest хранит богатые источники в sources и assets[].sourceIds, а простые теги — в tags и assets[].tagIds. Формат 0.2.0 не содержит fallback-полей прежней модели.
Готовые cult-backup binaries публикуются в GitHub Releases для Windows и Linux x64/ARM64. Для запуска из исходников используйте bun run backup:client -- перед аргументами клиента.
Каждый релиз сайта обязан иметь Git-тег v<version>, точно соответствующий версии в package.json: без него backup этой версии нельзя гарантированно воспроизвести. site.config.ts, admin/SMTP-секреты и сам Git-репозиторий в контентную копию не входят; актуальный конфиг храните отдельно от VPS в зашифрованном хранилище.
Пример backup.config.json:
{
"origin": "https://cult.gwynerva.net",
"destination": "./copies",
"datasetId": "cult-gwynerva"
}Плановый запуск получает пароль из отдельного credentials.json:
{
"username": "admin",
"password": "ADMIN-PASSWORD"
}На Linux выдайте файлу права chmod 600 credentials.json. На Windows ограничьте ACL текущим пользователем. Не передавайте пароль аргументом команды, не помещайте credentials рядом с публичными файлами и не синхронизируйте его вместе с backup-папками.
Планировщик должен ежедневно выполнять:
cult-backup backup --if-due --config /path/backup.config.json --credentials /path/credentials.jsonКлиент самостоятельно пропустит запуск, пока от последней успешной копии не прошло 168 часов. Внеплановый запуск выполняется вручную и скрыто спрашивает логин и пароль:
cult-backup backup --now --config /path/backup.config.jsonУспешный ручной запуск становится новой точкой недельного отсчёта; ошибка срок не сдвигает. Проверить существующую папку можно без доступа к сайту:
cult-backup verify /path/cult-gwynerva-backup_site-0.1.0_2026-07-18T12-00-00ZОбзор /admin показывает последний подтверждённый backup, текущую стадию и heartbeat. Через восемь суток без успеха виджет становится аварийным, а при включённом SMTP раз в сутки ставится письмо администратору через общую очередь.
Для восстановления найдите Git-тег версии из manifest.json, разверните эту версию с пустой БД и пустыми data/media, data/authors, data/tags, остановите app, примените migrations и запустите importer:
docker compose stop app
docker compose run --rm migrate
docker compose run --rm --no-deps -v /absolute/backup:/backup:ro migrate \
bun run backup:import -- /backup/cult-gwynerva-backup_site-0.1.0_2026-07-18T12-00-00Z
docker compose up -d --buildImporter сначала повторно проверяет все контрольные суммы, требует точного совпадения версии и отказывается работать с непустой коллекцией. Он сохраняет публичные ID, тексты, даты и связи, создаёт новые внутренние UUID и заново строит превью, размеры, цвет карточки, FTS и link previews. После проверки старой версии сайт обновляется обычным scripts/update.sh вместе со всеми migrations.
Всё переносимое состояние находится в data/; отдельно нужен только site.config.ts. Для согласованной копии SQLite остановите сервисы:
docker compose downСкопируйте проект, site.config.ts и весь data/. На новом VPS измените site.origin, обновите DNS и hostname Turnstile, затем выполните docker compose up -d --build. Публичные ID и относительные ссылки сохранятся. Тома сертификатов Caddy переносить необязательно.
Никогда не публикуйте data/ и site.config.ts, не добавляйте их в Git и не помещайте внутрь public/. Перед обновлением или переносом всегда делайте резервную копию.
Для ручного восстановления после уже применённой migration остановите app, сохраните текущую БД отдельно, верните указанный updater-файл на место data/db/cult.sqlite, удалите относящиеся к заменённой БД cult.sqlite-wal/cult.sqlite-shm, переключите Git на выведенный старый commit и пересоберите сервисы. Такое восстановление намеренно не автоматизировано: код, SQL-схема и backup должны возвращаться согласованно.
bun run doctor
docker compose config
docker compose ps
docker compose logs --tail=200 app caddy config-init migrateПроверьте DNS и доступность 80/443, если Caddy не получает сертификат. Ошибки обработки медиа обычно указывают на неподдерживаемый файл, превышение лимита или проблему FFmpeg; статус и текст ошибки видны в админке.
Архитектурная памятка для AI-агентов находится в AGENTS.md. При изменении API, хранения, конфигурации или команд её следует обновлять вместе с README.