Розгортання на живому сервері: три виправлення, знайдені першим запуском
Образи зібрались із першого разу, але далі знайшлось три речі. 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 -------------------------------------------------
|
# --- інтерфейс і API -------------------------------------------------
|
||||||
{$NETPULSE_DOMAIN} {
|
{$NETPULSE_DOMAIN} {
|
||||||
|
tls {$NETPULSE_TLS}
|
||||||
|
|
||||||
encode zstd gzip
|
encode zstd gzip
|
||||||
|
|
||||||
# WebSocket живих оновлень. Окремим блоком, бо йому не можна
|
# WebSocket живих оновлень. Окремим блоком, бо йому не можна
|
||||||
|
|
@ -43,6 +67,8 @@
|
||||||
# Зонди говорять gRPC поверх HTTP/2. Усередині мережі — h2c без TLS:
|
# Зонди говорять gRPC поверх HTTP/2. Усередині мережі — h2c без TLS:
|
||||||
# сертифікат живе тут, а не в кожному примірнику колектора.
|
# сертифікат живе тут, а не в кожному примірнику колектора.
|
||||||
{$NETPULSE_DOMAIN}:9443 {
|
{$NETPULSE_DOMAIN}:9443 {
|
||||||
|
tls {$NETPULSE_TLS}
|
||||||
|
|
||||||
reverse_proxy h2c://collector:9443 {
|
reverse_proxy h2c://collector:9443 {
|
||||||
# Потік Control двонаправлений і довгий: агент тримає його
|
# Потік Control двонаправлений і довгий: агент тримає його
|
||||||
# відкритим, доки живий. Буферизація тут ламає саме те, заради
|
# відкритим, доки живий. Буферизація тут ламає саме те, заради
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ docker compose exec -T db \
|
||||||
зашифрованими в базі, і репозиторій повністю відтворюється з неї —
|
зашифрованими в базі, і репозиторій повністю відтворюється з неї —
|
||||||
|
|
||||||
```sh
|
```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 оновлює його сам.
|
проблему. 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:
|
cache:
|
||||||
image: docker.dragonflydb.io/dragonflydb/dragonfly:v1.25.5
|
image: docker.dragonflydb.io/dragonflydb/dragonfly:v1.25.5
|
||||||
ulimits:
|
# memlock без обмеження прискорює Dragonfly, але дозволений не всюди:
|
||||||
memlock: -1
|
# у контейнерній віртуалізації (LXC, частина VPS) ядро відмовляє, і
|
||||||
|
# контейнер не стартує взагалі з «error setting rlimit type 8».
|
||||||
|
# Швидкість того не варта — обмеження знято.
|
||||||
volumes:
|
volumes:
|
||||||
- cache-data:/data
|
- cache-data:/data
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
@ -133,10 +135,11 @@ services:
|
||||||
image: caddy:2.8-alpine
|
image: caddy:2.8-alpine
|
||||||
environment:
|
environment:
|
||||||
NETPULSE_DOMAIN: ${NETPULSE_DOMAIN:?потрібен NETPULSE_DOMAIN}
|
NETPULSE_DOMAIN: ${NETPULSE_DOMAIN:?потрібен NETPULSE_DOMAIN}
|
||||||
# Порожній рядок Caddy не приймає: директива email без значення —
|
# Порожня пошта означає «домену ще немає» — тоді самопідписаний
|
||||||
# помилка розбору, і проксі не підніметься взагалі. Тому підстановка
|
# сертифікат. Інакше Caddy нескінченно проситиме Let's Encrypt
|
||||||
# спрацьовує і на незадану змінну, і на задану порожньою.
|
# видати сертифікат на IP, чого той не робить, і сайт лишиться
|
||||||
ACME_EMAIL: ${ACME_EMAIL:-admin@${NETPULSE_DOMAIN}}
|
# без TLS зовсім.
|
||||||
|
NETPULSE_TLS: ${ACME_EMAIL:-internal}
|
||||||
volumes:
|
volumes:
|
||||||
- ./deploy/Caddyfile:/etc/caddy/Caddyfile:ro
|
- ./deploy/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||||
- caddy-data:/data
|
- caddy-data:/data
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@ func run() error {
|
||||||
var (
|
var (
|
||||||
dsn = flag.String("dsn", os.Getenv("NETPULSE_DSN"), "DSN PostgreSQL")
|
dsn = flag.String("dsn", os.Getenv("NETPULSE_DSN"), "DSN PostgreSQL")
|
||||||
tenant = flag.String("tenant", "", "slug або uuid тенанта")
|
tenant = flag.String("tenant", "", "slug або uuid тенанта")
|
||||||
|
newTenant = flag.String("create-tenant", "",
|
||||||
|
"створити тенанта з таким іменем, якщо його ще немає")
|
||||||
login = flag.String("login", "", "логін користувача")
|
login = flag.String("login", "", "логін користувача")
|
||||||
email = flag.String("email", "", "email (необовʼязково — для сповіщень)")
|
email = flag.String("email", "", "email (необовʼязково — для сповіщень)")
|
||||||
fullName = flag.String("name", "", "повне ім'я")
|
fullName = flag.String("name", "", "повне ім'я")
|
||||||
|
|
@ -57,7 +59,7 @@ func run() error {
|
||||||
}
|
}
|
||||||
defer conn.Close(ctx)
|
defer conn.Close(ctx)
|
||||||
|
|
||||||
tenantID, tenantName, err := resolveTenant(ctx, conn, *tenant)
|
tenantID, tenantName, err := resolveTenant(ctx, conn, *tenant, *newTenant)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -156,15 +158,36 @@ func run() error {
|
||||||
return nil
|
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, `
|
err = conn.QueryRow(ctx, `
|
||||||
SELECT id::text, name FROM core.tenants
|
SELECT id::text, name FROM core.tenants
|
||||||
WHERE (slug::text = $1 OR id::text = $1) AND deleted_at IS NULL
|
WHERE (slug::text = $1 OR id::text = $1) AND deleted_at IS NULL
|
||||||
`, ref).Scan(&id, &name)
|
`, ref).Scan(&id, &name)
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
if !errors.Is(err, pgx.ErrNoRows) {
|
||||||
return "", "", fmt.Errorf("тенанта %q не знайдено", ref)
|
return id, name, err
|
||||||
}
|
}
|
||||||
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 {
|
func listUsers(ctx context.Context, conn *pgx.Conn, tenantID, tenantName string) error {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue