diff --git a/README.md b/README.md index b0a2295..d556891 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ The bucket is the source of truth. The service polls it and mirrors it to `./vau ## Quick start -1. Make a folder with the compose file and the env file: +1. Download the compose file and the env file: ```sh mkdir hyperhack && cd hyperhack @@ -47,13 +47,9 @@ The bucket is the source of truth. The service polls it and mirrors it to `./vau chmod 600 .env ``` -2. Edit `.env` (see [Configuration](#configuration)). Generate each secret with: + `docker-compose.yml` only pulls the released image from GHCR and pins the latest release, for example `ghcr.io/mimic890/hyperhack:0.1.0`. Image tags are the release version without the `v`, and they are never overwritten. There is no `latest` tag. - ```sh - openssl rand -base64 48 - ``` - -3. Create the data folders. The container runs as user `10001`: +2. Create the data folders. The container runs as user `10001`: ```sh mkdir data vault @@ -62,9 +58,13 @@ The bucket is the source of truth. The service polls it and mirrors it to `./vau Caddy must be able to read `vault/assets/`. The service writes folders as `0755` and files as `0644`. The SQLite file is `0600`. -4. In `docker-compose.yml`, set `image:` to a released version and remove the `build: .` line. Tags are semver and are never overwritten. There is no `latest` tag. +3. Fill `.env` (see [Configuration](#configuration)). Generate each secret with: -5. Verify the image and start the service: + ```sh + openssl rand -base64 48 + ``` + +4. Verify the image and start the service. Use the tag from `image:` in `docker-compose.yml`: ```sh gh attestation verify oci://ghcr.io/mimic890/hyperhack: -R Mimic890/hyperhack @@ -75,7 +75,7 @@ The bucket is the source of truth. The service polls it and mirrors it to `./vau On the first start the service needs the bucket. If S3 is unreachable and there is no mirror yet, it exits with code 1. Later starts build the index from the mirror and do not wait for S3. -6. Set up Caddy (see [Caddy](#caddy)) and set `TRUSTED_PROXIES`. +5. Set up Caddy (see [Caddy](#caddy)) and set `TRUSTED_PROXIES`. The service listens on port `8080` inside the container. Compose publishes it on `127.0.0.1:8080` only. Do not expose it to the internet: Caddy is the only public entry. @@ -384,11 +384,20 @@ Vote cookies (`vid`) are random and do not depend on any secret. ## Upgrading -Set the new version in `docker-compose.yml`, verify it with `gh attestation verify`, then: +Versions are listed under [tags](https://github.com/Mimic890/hyperhack/tags). Release `v0.2.0` is image `ghcr.io/mimic890/hyperhack:0.2.0`. -```sh -docker compose pull && docker compose up -d -``` +1. Back up `data/` (see [Backups](#backups)). +2. In `docker-compose.yml`, set the new tag in `image:`. If you have not changed the file, you can download it again from `main` instead: it pins the latest release. +3. Verify the new image, pull it and restart: + + ```sh + gh attestation verify oci://ghcr.io/mimic890/hyperhack: -R Mimic890/hyperhack + docker compose pull + docker compose up -d + docker compose ps # wait for "healthy" + ``` + +To roll back, set the old tag and run `docker compose up -d`. If the old version does not start, restore `data/` from the backup. ## Development @@ -397,9 +406,11 @@ Go 1.27 (`toolchain go1.27.3`). ```sh set -a; . ./.env; set +a DATA_DIR=./data VAULT_DIR=./vault LOG_FORMAT=text go run . -# or: docker compose up --build +# or, in Docker: docker compose -f compose.dev.yml up --build ``` +`compose.dev.yml` builds the image from the checkout as `hyperhack:dev` and otherwise uses the settings of `docker-compose.yml`. Compose never loads it on its own, so production stays on the released image. + Checks, run before every commit and in CI: ```sh diff --git a/README.ru.md b/README.ru.md index 5d5eb90..417d24c 100644 --- a/README.ru.md +++ b/README.ru.md @@ -38,7 +38,7 @@ VictoriaMetrics ──scrape /metrics──▶ hyperhack ; Grafana ──▶ Vi ## Быстрый старт -1. Создайте папку с compose-файлом и файлом окружения: +1. Скачайте compose-файл и файл окружения: ```sh mkdir hyperhack && cd hyperhack @@ -47,13 +47,9 @@ VictoriaMetrics ──scrape /metrics──▶ hyperhack ; Grafana ──▶ Vi chmod 600 .env ``` -2. Заполните `.env` (см. [Настройки](#настройки)). Каждый секрет генерируйте так: + `docker-compose.yml` только скачивает выпущенный образ из GHCR и закрепляет последний релиз, например `ghcr.io/mimic890/hyperhack:0.1.0`. Тег образа — версия релиза без `v`, теги никогда не перезаписываются. Тега `latest` нет. - ```sh - openssl rand -base64 48 - ``` - -3. Создайте папки для данных. Контейнер работает от пользователя `10001`: +2. Создайте папки для данных. Контейнер работает от пользователя `10001`: ```sh mkdir data vault @@ -62,9 +58,13 @@ VictoriaMetrics ──scrape /metrics──▶ hyperhack ; Grafana ──▶ Vi Caddy должен иметь право читать `vault/assets/`. Сервис создаёт папки с правами `0755`, файлы — `0644`. Файл SQLite — `0600`. -4. В `docker-compose.yml` укажите в `image:` выпущенную версию и удалите строку `build: .`. Теги — semver, они никогда не перезаписываются. Тега `latest` нет. +3. Заполните `.env` (см. [Настройки](#настройки)). Каждый секрет генерируйте так: -5. Проверьте образ и запустите сервис: + ```sh + openssl rand -base64 48 + ``` + +4. Проверьте образ и запустите сервис. Тег возьмите из `image:` в `docker-compose.yml`: ```sh gh attestation verify oci://ghcr.io/mimic890/hyperhack: -R Mimic890/hyperhack @@ -75,7 +75,7 @@ VictoriaMetrics ──scrape /metrics──▶ hyperhack ; Grafana ──▶ Vi При первом запуске сервису нужен бакет. Если S3 недоступен, а зеркала ещё нет, сервис завершается с кодом 1. При следующих запусках индекс строится из зеркала, и сервис не ждёт S3. -6. Настройте Caddy (см. [Caddy](#caddy)) и задайте `TRUSTED_PROXIES`. +5. Настройте Caddy (см. [Caddy](#caddy)) и задайте `TRUSTED_PROXIES`. Внутри контейнера сервис слушает порт `8080`. Compose публикует его только на `127.0.0.1:8080`. Не открывайте его в интернет: единственный публичный вход — Caddy. @@ -384,11 +384,20 @@ Cookie голосов (`vid`) случайные и не зависят ни о ## Обновление -Укажите новую версию в `docker-compose.yml`, проверьте её через `gh attestation verify`, затем: +Версии перечислены в [тегах](https://github.com/Mimic890/hyperhack/tags). Релизу `v0.2.0` соответствует образ `ghcr.io/mimic890/hyperhack:0.2.0`. -```sh -docker compose pull && docker compose up -d -``` +1. Сделайте резервную копию `data/` (см. [Резервные копии](#резервные-копии)). +2. В `docker-compose.yml` укажите новый тег в `image:`. Если вы не меняли файл, можно вместо этого скачать его заново из `main`: там закреплён последний релиз. +3. Проверьте новый образ, скачайте его и перезапустите сервис: + + ```sh + gh attestation verify oci://ghcr.io/mimic890/hyperhack: -R Mimic890/hyperhack + docker compose pull + docker compose up -d + docker compose ps # дождитесь "healthy" + ``` + +Чтобы откатиться, верните старый тег и выполните `docker compose up -d`. Если старая версия не запускается, восстановите `data/` из резервной копии. ## Разработка @@ -397,9 +406,11 @@ Go 1.27 (`toolchain go1.27.3`). ```sh set -a; . ./.env; set +a DATA_DIR=./data VAULT_DIR=./vault LOG_FORMAT=text go run . -# или: docker compose up --build +# или в Docker: docker compose -f compose.dev.yml up --build ``` +`compose.dev.yml` собирает образ из рабочей копии как `hyperhack:dev`, остальные настройки берёт из `docker-compose.yml`. Compose сам его не подключает, поэтому в продакшене остаётся выпущенный образ. + Проверки, перед каждым коммитом и в CI: ```sh diff --git a/SPEC.md b/SPEC.md index 394d8d8..eb4e356 100644 --- a/SPEC.md +++ b/SPEC.md @@ -109,7 +109,8 @@ Proposed variables (final names set during implementation): ## Deployment - The service runs in Docker and starts with Docker Compose. -- GitHub Actions builds the image. Proposed: push to GHCR (`ghcr.io`). +- GitHub Actions builds the image and pushes it to GHCR (`ghcr.io`). +- `docker-compose.yml` is for production: it only pulls the released image from GHCR, with no `build:`, and pins the latest release. Local builds use `compose.dev.yml` (extends the production service, adds `build: .`, image `hyperhack:dev`), run only with `docker compose -f compose.dev.yml`. No `docker-compose.override.yml`: Compose loads it automatically, so it would reach production. - Compose uses bind mounts to subfolders (`./data`, …). No named volumes. - The service port is not published to the internet: localhost, Docker network or mesh IP only. The reverse proxy (Caddy) handles TLS. - The agent writes `.gitignore` (at least: `.env`, `data/`, build output). @@ -556,7 +557,7 @@ Other methods → `405`. Unknown paths → `404`. - Dependabot: `gomod`, `github-actions`, `docker`. - CI checks: `go vet`, `staticcheck`, `govulncheck`, tests with `-race`, CodeQL (Go). - Release: build image, push to GHCR, attest with `actions/attest` (`push-to-registry: true`). Verify on the host: `gh attestation verify oci://ghcr.io//hyperhack: -R /hyperhack`. -- Image tags: semver, never overwritten. Compose pins a version, not `latest`. +- Image tags: semver without the `v` (git tag `v0.1.0` → image `0.1.0`), never overwritten. Compose pins a version, not `latest`. After a release, a PR moves the tag in `docker-compose.yml` to the new version. - Repo settings: branch protection on `main` (PR and checks required, no force push), secret scanning + push protection, private vulnerability reporting. `SECURITY.md` explains how to report. ### Security tests @@ -568,8 +569,6 @@ Other methods → `405`. Unknown paths → `404`. ## Proposed (not confirmed) -- Image registry: GHCR. - ## Open questions @@ -617,3 +616,4 @@ Other methods → `405`. Unknown paths → `404`. - 2026-11-04 — Search, tags, RSS, SEO: Bleve `v2.6.1`, in-memory index, one document mapping per language (`en`, `ru`). Fuzziness by word length: ≤ 3 letters 0, ≤ 6 letters 1, longer 2; prefix on the last word. Snippets keep only ``. Search works without JS; `search.js` swaps the results while typing (debounced). Results page `noindex`. Tags lower case, Obsidian characters, `/tags/` paginated like the main page. 24 tag hue buckets (FNV-1a hash), contrast checked in tests. RSS: `dc:creator` from `AUTHOR_NAME`, `atom:updated` from `updated`. Image `width`/`height` read from the mirror (`golang.org/x/image` for WebP); an asset change re-renders all posts. IndexNow: one ping per sync with new, changed and removed post URLs, not on the first index load. New `AUTHOR_NAME`; `DEFAULT_OG_IMAGE` is a path inside `assets/`. - 2026-11-04 — Metrics, votes, limits: SQLite `DATA_DIR/hyperhack.db` (`modernc.org/sqlite` `v1.61.0`, `0600`, WAL). All-time counters per post and label (Grafana gets daily trends with `increase()`), daily views only for "Popular" (pruned after `POPULAR_DAYS`). Search queries: live requests from `search.js` (`X-Search: live`) are not stored, the settled query is; TTL 90 days, at most 10 000 rows, top 20 in `/metrics`, refreshed every 5 min. ALTCHA: `altcha-lib-go/v2` `v2.4.1`, v2 challenge with SHA-256, cost 1, `uint32` counter up to `ALTCHA_MAX_NUMBER`, key signature; own small solver `static/altcha.js` instead of the widget; signing keys derived from `ALTCHA_HMAC_KEY`; replay map capped at 100 000. Beacon: a delta on every hide and `pagehide`, sent with `fetch(…, {keepalive: true})` (JSON body), each with a random per-page-view id; the server sums time per view and clamps the sum to 4 h; time on page = average over views with reading time. Views and read depth: once per visitor per post per day; daily hashes in memory only (cap 200 000), `ip_prefix` = IPv4 address or IPv6 `/64`. Source `internal` for referrers from the site itself. Outbound hosts limited to links in the post. `/api/altcha` and `/api/vote` share `RATE_LIMIT_VOTE`; limiter tracks at most 10 000 clients. Vote cookie stored as SHA-256. "Popular" after "Newest" on page 1. `/metrics` in Prometheus text format written with the standard library. - 2026-11-04 — Grafana, Caddy, README: dashboard `grafana/hyperhack.json` for a Prometheus-type data source on VictoriaMetrics (uses the VictoriaMetrics function `sort_by_label_numeric`); daily bars cover the day that starts at the bar, so today is shown. `Caddyfile.example` for Caddy `v2.11.7` with `caddy-ratelimit` `v0.1.0` (1200 requests per minute per client IP; `/static/*` and `/assets/*` are not counted) and `caddy-crowdsec-bouncer` `v0.14.1`; `/metrics` and `/reindex` (and their subpaths) answer `404` on the public domain; compression `zstd`/`gzip`. README: behind Caddy on the host, `TRUSTED_PROXIES` is the gateway of the Compose network. Full README in English and Russian. +- 2026-11-04 — Production Compose: image registry GHCR (confirmed). `docker-compose.yml` only pulls the released image, no `build:`, pinned to the latest release (`0.1.0`; image tags are the git tag without `v`). Local builds in `compose.dev.yml`, used only with `-f`; no `docker-compose.override.yml`. README Quick start uses the downloaded `docker-compose.yml` as is, and README documents upgrades and rollback. diff --git a/SPEC.ru.md b/SPEC.ru.md index 6123f93..2026f52 100644 --- a/SPEC.ru.md +++ b/SPEC.ru.md @@ -109,7 +109,8 @@ ## Развёртывание - Сервис работает в Docker, запускается через Docker Compose. -- Образ собирает GitHub Actions. Предложено: публикация в GHCR (`ghcr.io`). +- Образ собирает GitHub Actions и публикует в GHCR (`ghcr.io`). +- `docker-compose.yml` — для продакшена: он только скачивает выпущенный образ из GHCR, без `build:`, и закрепляет последний релиз. Локальная сборка — в `compose.dev.yml` (расширяет сервис из продакшен-файла, добавляет `build: .`, образ `hyperhack:dev`), запускается только через `docker compose -f compose.dev.yml`. Без `docker-compose.override.yml`: Compose подключает его сам, и он попал бы в продакшен. - Compose: bind mount в подпапки (`./data`, …). Без именованных томов. - Порт сервиса не публикуется наружу: только localhost, Docker-сеть или mesh-IP. TLS делает reverse proxy (Caddy). - `.gitignore` пишет агент (минимум: `.env`, `data/`, результаты сборки). @@ -556,7 +557,7 @@ Garage не шлёт уведомления об изменениях в бак - Dependabot: `gomod`, `github-actions`, `docker`. - Проверки в CI: `go vet`, `staticcheck`, `govulncheck`, тесты с `-race`, CodeQL (Go). - Релиз: сборка образа, push в GHCR, attestation через `actions/attest` (`push-to-registry: true`). Проверка на хосте: `gh attestation verify oci://ghcr.io//hyperhack: -R /hyperhack`. -- Теги образа: semver, никогда не перезаписываются. В Compose закреплена версия, не `latest`. +- Теги образа: semver без `v` (git-тег `v0.1.0` → образ `0.1.0`), никогда не перезаписываются. В Compose закреплена версия, не `latest`. После релиза отдельный PR переводит тег в `docker-compose.yml` на новую версию. - Настройки репозитория: защита ветки `main` (обязательны PR и проверки, без force push), secret scanning + push protection, private vulnerability reporting. `SECURITY.md` объясняет, как сообщить об уязвимости. ### Тесты безопасности @@ -568,8 +569,6 @@ Garage не шлёт уведомления об изменениях в бак ## Предложено (не подтверждено) -- Реестр образов: GHCR. - ## Открытые вопросы @@ -617,3 +616,4 @@ Garage не шлёт уведомления об изменениях в бак - 2026-11-04 — Поиск, теги, RSS, SEO: Bleve `v2.6.1`, индекс в памяти, своя схема документа на каждый язык (`en`, `ru`). Опечатки по длине слова: ≤ 3 букв 0, ≤ 6 букв 1, длиннее 2; префикс по последнему слову. В сниппетах остаётся только ``. Поиск работает без JS; `search.js` подменяет результаты при наборе (с задержкой). У страницы результатов `noindex`. Теги в нижнем регистре, символы как в Obsidian, `/tags/` с пагинацией как на главной. 24 оттенка для тегов (хеш FNV-1a), контраст проверяется в тестах. RSS: `dc:creator` из `AUTHOR_NAME`, `atom:updated` из `updated`. `width`/`height` картинок читаются из зеркала (`golang.org/x/image` для WebP); изменение ассета перерисовывает все посты. IndexNow: один пинг за синхронизацию с URL новых, изменённых и удалённых постов, не при первой загрузке индекса. Новая `AUTHOR_NAME`; `DEFAULT_OG_IMAGE` — путь внутри `assets/`. - 2026-11-04 — Метрики, голоса, лимиты: SQLite `DATA_DIR/hyperhack.db` (`modernc.org/sqlite` `v1.61.0`, `0600`, WAL). Счётчики за всё время по посту и метке (дневные тренды Grafana считает через `increase()`), дневные просмотры — только для «Популярных» (удаляются старше `POPULAR_DAYS`). Поисковые запросы: живые запросы `search.js` (`X-Search: live`) не сохраняются, сохраняется устоявшийся запрос; TTL 90 дней, не больше 10 000 строк, топ-20 в `/metrics`, обновляется раз в 5 минут. ALTCHA: `altcha-lib-go/v2` `v2.4.1`, задача v2 с SHA-256, cost 1, счётчик `uint32` до `ALTCHA_MAX_NUMBER`, подпись ключа; свой маленький решатель `static/altcha.js` вместо виджета; ключи подписи выводятся из `ALTCHA_HMAC_KEY`; карта повторов ограничена 100 000. Beacon: дельта при каждом скрытии и `pagehide`, отправка через `fetch(…, {keepalive: true})` (тело JSON), в каждой — случайный id просмотра страницы; сервер суммирует время по просмотру и ограничивает сумму 4 ч; время на странице — среднее по просмотрам со временем чтения. Просмотры и глубина чтения: один раз на посетителя, пост и день; суточные хеши только в памяти (не больше 200 000), `ip_prefix` — адрес IPv4 или `/64` IPv6. Источник `internal` для переходов с самого сайта. Внешние домены — только из ссылок поста. `/api/altcha` и `/api/vote` делят `RATE_LIMIT_VOTE`; лимитер помнит не больше 10 000 клиентов. Cookie голоса хранится как SHA-256. «Популярные» после «Новых» на первой странице. `/metrics` в текстовом формате Prometheus, написан на стандартной библиотеке. - 2026-11-04 — Grafana, Caddy, README: дашборд `grafana/hyperhack.json` для источника данных типа Prometheus на VictoriaMetrics (использует функцию VictoriaMetrics `sort_by_label_numeric`); дневной столбец покрывает день, который начинается в его точке, поэтому сегодняшний день виден. `Caddyfile.example` для Caddy `v2.11.7` с `caddy-ratelimit` `v0.1.0` (1200 запросов в минуту на IP клиента; `/static/*` и `/assets/*` не учитываются) и `caddy-crowdsec-bouncer` `v0.14.1`; `/metrics` и `/reindex` (и их подпути) на публичном домене отвечают `404`; сжатие `zstd`/`gzip`. README: если Caddy на хосте, `TRUSTED_PROXIES` — шлюз сети Compose. Полный README на английском и русском. +- 2026-11-04 — Продакшен Compose: реестр образов GHCR (подтверждено). `docker-compose.yml` только скачивает выпущенный образ, без `build:`, закреплён последний релиз (`0.1.0`; тег образа — git-тег без `v`). Локальная сборка в `compose.dev.yml`, только через `-f`; без `docker-compose.override.yml`. Быстрый старт в README использует скачанный `docker-compose.yml` без правок, README описывает обновление и откат. diff --git a/compose.dev.yml b/compose.dev.yml new file mode 100644 index 0000000..866c5b4 --- /dev/null +++ b/compose.dev.yml @@ -0,0 +1,8 @@ +# Local build from this checkout. Use only with: docker compose -f compose.dev.yml up --build +services: + hyperhack: + extends: + file: docker-compose.yml + service: hyperhack + image: hyperhack:dev + build: . diff --git a/docker-compose.yml b/docker-compose.yml index 0f348e3..539339e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ +# Production: pulls the released image from GHCR. Local builds: compose.dev.yml. services: hyperhack: image: ghcr.io/mimic890/hyperhack:0.1.0 - build: . user: "10001:10001" read_only: true cap_drop: [ALL]