Розгортання на живому сервері: три виправлення, знайдені першим запуском
Образи зібрались із першого разу, але далі знайшлось три речі. memlock без обмеження для Dragonfly дозволений не всюди: у контейнерній віртуалізації ядро відмовляє, і контейнер не стартує взагалі. Швидкість того не варта — обмеження знято. Caddy нескінченно просив Let's Encrypt видати сертифікат на IP, чого той не робить, і сайт лишався без TLS. Тепер порожня ACME_EMAIL означає самопідписаний сертифікат. Плюс default_sni: SNI не містить IP-адрес, тож без нього рукостискання обривалось «internal error». У README команда заведення власника не працювала: у образу ENTRYPOINT, і без --entrypoint ім'я команди йшло аргументом до netpulse-api. Заразом netpulse-user навчився створювати перший кабінет — свіжа база не має жодного, а користувач без кабінету нікуди не належить. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
03ea70e5de
commit
6843bd3280
4 changed files with 76 additions and 13 deletions
|
|
@ -4,12 +4,36 @@
|
|||
# знімає з розгортання найчастішу причину нічного дзвінка — прострочений
|
||||
# сертифікат на системі, яка сама має повідомляти про проблеми.
|
||||
|
||||
|
||||
{
|
||||
email {$ACME_EMAIL}
|
||||
# Яким сертифікатом відповідати, коли клієнт не назвав імені.
|
||||
#
|
||||
# SNI не містить IP-адрес: стандарт дозволяє там лише доменні імена,
|
||||
# тож браузер, який іде на https://93.171.243.247, не каже нічого — і
|
||||
# Caddy не має за чим обрати сертифікат. Рукостискання обривається
|
||||
# «internal error», сайт виглядає мертвим.
|
||||
#
|
||||
# З доменом ця настройка нічого не міняє: SNI приходить, і збіг
|
||||
# знаходиться звичайним шляхом.
|
||||
default_sni {$NETPULSE_DOMAIN}
|
||||
}
|
||||
|
||||
# Одна директива на два випадки.
|
||||
#
|
||||
# tls <пошта> — сертифікат від Let's Encrypt на цю адресу;
|
||||
# tls internal — власний самопідписаний.
|
||||
#
|
||||
# Значення приходить із NETPULSE_TLS, яке compose виводить із ACME_EMAIL:
|
||||
# пошта задана — беремо справжній сертифікат, порожньо — самопідписаний.
|
||||
#
|
||||
# Порожнє значення тут не працює: Let's Encrypt не видає сертифікатів на
|
||||
# публічні IP-адреси, і Caddy вічно повторював би спробу, а сайт лежав
|
||||
# би без TLS. Перевірено живим розгортанням на голому IP.
|
||||
|
||||
# --- інтерфейс і API -------------------------------------------------
|
||||
{$NETPULSE_DOMAIN} {
|
||||
tls {$NETPULSE_TLS}
|
||||
|
||||
encode zstd gzip
|
||||
|
||||
# WebSocket живих оновлень. Окремим блоком, бо йому не можна
|
||||
|
|
@ -43,6 +67,8 @@
|
|||
# Зонди говорять gRPC поверх HTTP/2. Усередині мережі — h2c без TLS:
|
||||
# сертифікат живе тут, а не в кожному примірнику колектора.
|
||||
{$NETPULSE_DOMAIN}:9443 {
|
||||
tls {$NETPULSE_TLS}
|
||||
|
||||
reverse_proxy h2c://collector:9443 {
|
||||
# Потік Control двонаправлений і довгий: агент тримає його
|
||||
# відкритим, доки живий. Буферизація тут ламає саме те, заради
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ docker compose exec -T db \
|
|||
зашифрованими в базі, і репозиторій повністю відтворюється з неї —
|
||||
|
||||
```sh
|
||||
docker compose run --rm api netpulse-gitsync
|
||||
docker compose run --rm --entrypoint netpulse-gitsync api
|
||||
```
|
||||
|
||||
Зворотне невірно: з репозиторію базу не відновити. Тому джерелом істини
|
||||
|
|
@ -197,6 +197,17 @@ NETPULSE_DEK=np2=<новий hex>,np1=<старий hex>
|
|||
яка сама має повідомляти про проблеми, — найгірший спосіб дізнатись про
|
||||
проблему. Caddy оновлює його сам.
|
||||
|
||||
## Без домену
|
||||
|
||||
Домен потрібен для справжнього сертифіката: Let's Encrypt не видає їх на
|
||||
IP-адреси. Поки домену немає, лишіть `ACME_EMAIL` порожнім — Caddy
|
||||
випише самопідписаний, і система працюватиме одразу, з попередженням у
|
||||
браузері.
|
||||
|
||||
Коли домен зʼявиться: замінити `NETPULSE_DOMAIN`, вписати `ACME_EMAIL` і
|
||||
`docker compose restart proxy`. Caddyfile змонтований, тож перезбирати
|
||||
образи не треба — але й `up -d` без `restart` його не перечитає.
|
||||
|
||||
## Дашборд на телевізор
|
||||
|
||||
В інтерфейсі: **Дашборд → На телевізор → Видати посилання**. Отриману
|
||||
|
|
|
|||
|
|
@ -52,8 +52,10 @@ services:
|
|||
|
||||
cache:
|
||||
image: docker.dragonflydb.io/dragonflydb/dragonfly:v1.25.5
|
||||
ulimits:
|
||||
memlock: -1
|
||||
# memlock без обмеження прискорює Dragonfly, але дозволений не всюди:
|
||||
# у контейнерній віртуалізації (LXC, частина VPS) ядро відмовляє, і
|
||||
# контейнер не стартує взагалі з «error setting rlimit type 8».
|
||||
# Швидкість того не варта — обмеження знято.
|
||||
volumes:
|
||||
- cache-data:/data
|
||||
restart: unless-stopped
|
||||
|
|
@ -133,10 +135,11 @@ services:
|
|||
image: caddy:2.8-alpine
|
||||
environment:
|
||||
NETPULSE_DOMAIN: ${NETPULSE_DOMAIN:?потрібен NETPULSE_DOMAIN}
|
||||
# Порожній рядок Caddy не приймає: директива email без значення —
|
||||
# помилка розбору, і проксі не підніметься взагалі. Тому підстановка
|
||||
# спрацьовує і на незадану змінну, і на задану порожньою.
|
||||
ACME_EMAIL: ${ACME_EMAIL:-admin@${NETPULSE_DOMAIN}}
|
||||
# Порожня пошта означає «домену ще немає» — тоді самопідписаний
|
||||
# сертифікат. Інакше Caddy нескінченно проситиме Let's Encrypt
|
||||
# видати сертифікат на IP, чого той не робить, і сайт лишиться
|
||||
# без TLS зовсім.
|
||||
NETPULSE_TLS: ${ACME_EMAIL:-internal}
|
||||
volumes:
|
||||
- ./deploy/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- caddy-data:/data
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ func run() error {
|
|||
var (
|
||||
dsn = flag.String("dsn", os.Getenv("NETPULSE_DSN"), "DSN PostgreSQL")
|
||||
tenant = flag.String("tenant", "", "slug або uuid тенанта")
|
||||
newTenant = flag.String("create-tenant", "",
|
||||
"створити тенанта з таким іменем, якщо його ще немає")
|
||||
login = flag.String("login", "", "логін користувача")
|
||||
email = flag.String("email", "", "email (необовʼязково — для сповіщень)")
|
||||
fullName = flag.String("name", "", "повне ім'я")
|
||||
|
|
@ -57,7 +59,7 @@ func run() error {
|
|||
}
|
||||
defer conn.Close(ctx)
|
||||
|
||||
tenantID, tenantName, err := resolveTenant(ctx, conn, *tenant)
|
||||
tenantID, tenantName, err := resolveTenant(ctx, conn, *tenant, *newTenant)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -156,17 +158,38 @@ func run() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func resolveTenant(ctx context.Context, conn *pgx.Conn, ref string) (id, name string, err error) {
|
||||
// resolveTenant знаходить кабінет або створює перший.
|
||||
//
|
||||
// Свіжа інсталяція не має жодного тенанта, а користувач без кабінету
|
||||
// нікуди не належить. Виходить те саме коло, заради розриву якого існує
|
||||
// ця команда, лише рівнем вище: щоб завести власника, потрібен кабінет,
|
||||
// а щоб завести кабінет — панель, у яку ще нікому зайти.
|
||||
//
|
||||
// Створення за окремим прапорцем, а не мовчки: друкарська помилка в
|
||||
// -tenant інакше плодила б порожні кабінети замість зрозумілої відмови.
|
||||
func resolveTenant(ctx context.Context, conn *pgx.Conn, ref, createName string) (id, name string, err error) {
|
||||
err = conn.QueryRow(ctx, `
|
||||
SELECT id::text, name FROM core.tenants
|
||||
WHERE (slug::text = $1 OR id::text = $1) AND deleted_at IS NULL
|
||||
`, ref).Scan(&id, &name)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return "", "", fmt.Errorf("тенанта %q не знайдено", ref)
|
||||
}
|
||||
if !errors.Is(err, pgx.ErrNoRows) {
|
||||
return id, name, err
|
||||
}
|
||||
|
||||
if createName == "" {
|
||||
return "", "", fmt.Errorf(
|
||||
"тенанта %q не знайдено; для першого запуску додайте -create-tenant \"Назва\"", ref)
|
||||
}
|
||||
if err := conn.QueryRow(ctx, `
|
||||
INSERT INTO core.tenants (slug, name) VALUES ($1, $2)
|
||||
RETURNING id::text, name
|
||||
`, ref, createName).Scan(&id, &name); err != nil {
|
||||
return "", "", fmt.Errorf("створення тенанта: %w", err)
|
||||
}
|
||||
fmt.Printf("створено кабінет %q (%s)\n", name, ref)
|
||||
return id, name, nil
|
||||
}
|
||||
|
||||
func listUsers(ctx context.Context, conn *pgx.Conn, tenantID, tenantName string) error {
|
||||
rows, err := conn.Query(ctx, `
|
||||
SELECT u.username::text, COALESCE(u.full_name,''), r.key::text,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue