Files
mayatnikovandClaude Opus 5 f9ac83bef8 Развести номера ADR: 064/065/066 этой ветки → 073/074/075
Ветка отведена от 522ba89, где последним был ADR-063, и заняла
064–072. Тем временем в main через #3 и #4 пришли свои 064
(строка ввода — div вместо form), 065 (стороны сообщений)
и 066 (серверного перца нет).

Переезжают три ADR этой ветки, 067–072 остаются на месте:
их номера свободны, а внутренние ссылки менять незачем.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QR8uS3zGkybzWRW4GEY1oz
2026-08-24 11:23:12 +03:00

153 lines
9.8 KiB
Markdown
Raw Permalink 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.
# Деплой
Цель — `ssh xmatic` (Ubuntu 22.04, x86_64), домен `bare.xmatic.team`, A-запись на IP сервера. Решения — ADR-022.
## Сборка
```sh
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o bare ./cmd/bare
```
Версия бинаря — `vcs.revision` и `vcs.time` из `debug.ReadBuildInfo()`. `bare version` печатает семь символов ревизии и время коммита в UTC: `cfd0ec0 2026-08-23T04:54:16Z`. У сборки из изменённого рабочего дерева (`vcs.modified`) к ревизии дописывается `+dirty` — сверка со сборкой из тега не должна проходить молча (ADR-057); у сборки не из git печатается `unknown`. Времени компиляции в бинаре нет: оно делало бы каждую пересборку одного коммита новым файлом (ADR-074). Те же значения отдаёт `GET /api/config` полями `version` и `commitAt`. `/healthz` отвечает только `ok`.
## Первичная настройка сервера (один раз)
```sh
sudo useradd --system --home /var/lib/bare --shell /usr/sbin/nologin bare
sudo mkdir -p /opt/bare /var/lib/bare /etc/bare
sudo chown bare:bare /var/lib/bare
sudo chmod 0700 /var/lib/bare /etc/bare
```
Права закрыты намеренно (ADR-032): в базе лежат `argon2id(authKey)` и ключевые блобы, машина общая.
`/etc/bare/env` (владелец root, режим 0600):
```
BARE_ADDR=127.0.0.1:8411
BARE_DB=/var/lib/bare/bare.db
BARE_ORIGIN=https://bare.xmatic.team
BARE_VAPID_PUBLIC=<из bare vapid>
BARE_VAPID_PRIVATE=<из bare vapid>
BARE_VAPID_SUBJECT=mailto:admin@xmatic.team
BARE_INVITE_CODE=<пусто или код>
```
`bare vapid` печатает пару ключей; выполняется локально один раз, результат вписывается в файл.
`BARE_VAPID_SUBJECT` — URI по RFC 8292: `mailto:<адрес>` или `https://<хост>`. Форма проверяется при старте: с пустым при заданных ключах или с голым адресом без схемы сервер не поднимается. Отдавать субъект библиотеке приходится без схемы `mailto:` — она приписывает её сама, и готовый URI превратился бы в `mailto:mailto:…`, который push-сервис отвергает; нормализация живёт в `internal/push`, запись в этом файле верна и не меняется.
`/etc/systemd/system/bare.service`:
```ini
[Unit]
Description=Bare chat
After=network-online.target
Wants=network-online.target
[Service]
User=bare
Group=bare
EnvironmentFile=/etc/bare/env
ExecStart=/opt/bare/bare serve
Restart=on-failure
RestartSec=2
StateDirectory=bare
StateDirectoryMode=0700
UMask=0077
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ReadWritePaths=/var/lib/bare
[Install]
WantedBy=multi-user.target
```
`/etc/nginx/sites-available/bare.xmatic.team` (затем симлинк в `sites-enabled`):
```nginx
server {
listen 80;
listen [::]:80;
server_name bare.xmatic.team;
access_log off;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name bare.xmatic.team;
ssl_certificate /etc/letsencrypt/live/bare.xmatic.team/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/bare.xmatic.team/privkey.pem;
add_header Strict-Transport-Security "max-age=31536000" always;
# журнал запросов ведёт только bare, и ведёт без ника, IP и query (ADR-056)
access_log off;
client_max_body_size 64k;
location /api/events {
proxy_pass http://127.0.0.1:8411;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Connection "";
proxy_buffering off;
proxy_cache off;
gzip off;
proxy_read_timeout 1h;
}
location / {
proxy_pass http://127.0.0.1:8411;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Connection "";
}
}
```
Сертификат: сначала временный конфиг только с блоком `:80` (без `return`, с `root` для ACME) или `certbot --nginx -d bare.xmatic.team` — на машине certbot уже обслуживает соседние сайты, использовать тот же способ, что у них (`ls /etc/letsencrypt/renewal/` показывает, какой плагин).
```sh
sudo nginx -t && sudo systemctl reload nginx
sudo systemctl daemon-reload && sudo systemctl enable --now bare
```
## Обновление — `scripts/deploy.sh`
Перед сборкой: если менялись `index.html`, `app.css`, `js/*`, `manifest.json` или иконки — сменить `VERSION` в `web/sw.js` (ADR-023). Смена версии делает файл воркера другим, и установленные приложения обновляются сами: клиент замечает новую оболочку при запуске или при возвращении в приложение, включает её и перезагружает страницу (ADR-068). Без смены версии новая статика доедет только по ETag, вторым открытием, — и во вкладке, и в установленном приложении одинаково: обработчик `fetch` у них один. Но воркер и предзагруженный список оболочки останутся прежними, а самообновление не запустится вовсе.
```sh
#!/bin/sh
set -eu
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /tmp/bare ./cmd/bare
scp /tmp/bare xmatic:/tmp/bare
ssh xmatic 'sudo install -m 0755 -o root -g root /tmp/bare /opt/bare/bare && sudo systemctl restart bare && sleep 1 && curl -fsS http://127.0.0.1:8411/healthz'
```
Сверка подлинности: `sha256sum /opt/bare/bare` на сервере равен хешу сборки из тега на той же версии Go с теми же флагами.
## Проверка после деплоя
- `curl -I https://bare.xmatic.team/` — 200, заголовки CSP и nosniff.
- `curl -N https://bare.xmatic.team/api/events` — 401 (без cookie), без буферизации.
- `curl -s https://bare.xmatic.team/sw.js | grep VERSION` — версия та, что в репозитории.
- `curl -s https://bare.xmatic.team/api/config``version` равен `git rev-parse --short=7 HEAD` задеплоенного коммита и без `+dirty`. Длина задана явно: сервер режет ревизию ровно до семи символов, а `--short` без числа берёт её из `core.abbrev` и растит по мере роста репозитория.
- `journalctl -u bare -f` — старт, применённые миграции, нет ошибок.
## Бэкап
`sqlite3 /var/lib/bare/bare.db "VACUUM INTO '/var/lib/bare/backup.db'"` или копия файла при остановленном сервисе. В базе только шифротексты и метаданные — бэкап не содержит переписки. Копия наследует режим 0600 (ADR-032); при восстановлении в другое место права надо выставить руками.
## Логи
Сервер пишет в stdout: время, метод, путь, статус, длительность; для маршрутов `/api/` вместо пути пишется шаблон (`/api/users/{nick}`), чтобы ник не попадал в журнал, а если отказ случился до маршрутизации (`Origin`, предел тела) и шаблона ещё нет — просто `/api/`; ника в журнале нет вовсе, включая отказы по лимитам (ADR-055); IP не пишется. Причины ответов `500 internal` (ADR-027) пишутся отдельной строкой, без данных запроса. Отправитель пушей пишет класс отказа — «таймаут», «имя не разрешилось», «отправка не удалась» — без адреса подписки и идентификатора устройства (ADR-047). Ответ push-сервиса пишется статусом и коротким кодом причины из тела: «пуш: push-сервис ответил 403 (BadJwtToken)». Код — диагностика вендора; всё, что на короткий код не похоже, отбрасывается целиком, и остаётся один статус (ADR-073). journald хранит по своим правилам.
nginx журнал запросов не ведёт: `access_log off` в обоих server-блоках (ADR-056). Без этой строки он унаследовал бы `access.log` формата `combined` из `/etc/nginx/nginx.conf` — с адресом клиента и полным URI, то есть с ником и социальным графом. `error_log` остаётся: это журнал сбоев, а не запросов, и при отказе он записывает адрес клиента.