Netpulse_SasS/docker-compose.yml
byrsapty 25da4ffea8
All checks were successful
CI / hygiene (push) Successful in 7s
CI / web (push) Successful in 44s
CI / server (push) Successful in 1m23s
CI / agent (push) Successful in 58s
Кеш: стеля памʼяті вимагає ще й обмеження потоків
Учора додав Dragonfly `--maxmemory`, щоб він не рахував ту саму
памʼять, яку вже порахував PG_SHARED_BUFFERS. На стенді це поклало
кеш у цикл перезапусків:

    There are 4 threads, so 1.00GiB are required. Exiting...

Dragonfly вимагає 256 МБ на КОЖЕН потік вводу-виводу, а їх за
замовчуванням стільки ж, скільки ядер. На 4-ядерній машині стеля 512 МБ
означає «потрібен 1 ГіБ» — і відмову стартувати. Тепер потоків два:
для кешу сесій з запасом, і 512 МБ їм вистачає.

Друга половина помилки тихіша: `command:` ЗАМІНЮЄ команду образу, а не
доповнює її, тож разом зі стелею я загубив `--logtostderr`. Журнал
пішов у файл усередині контейнера — тобто `docker logs` мовчав би, і
контейнер, що падає в циклі, виглядав би як контейнер без проблем.
Обидва слова повернуто явно.

Знайдено відкриттям порту трапів: кеш ліг при тому ж перезапуску, і
якби не порожній `docker logs`, я шукав би причину значно довше.
2026-08-27 20:07:12 +03:00

328 lines
18 KiB
YAML
Raw 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.

# Повний стек NetPulse: БД, кеш, міграції, API з інтерфейсом, колектор
# зондів і зворотний проксі з автоматичним TLS.
#
# Швидкий старт:
# cp deploy/.env.example .env # і заповнити секрети
# docker compose up -d
# docker compose run --rm --entrypoint netpulse-user cli \
# -tenant default -login admin -role owner
#
# netpulse-user і netpulse-secret заводять кабінети, людей і секрети —
# тобто роблять рівно те, чого роль під RLS робити не має. Тому для них
# є окрема служба cli з DSN власника: під роллю застосунку вони не
# падають з помилкою, а мовчки нічого не знаходять.
#
# Тільки стенд для розробки (БД і кеш, решта — з go run):
# docker compose up -d db cache
name: netpulse
# ---------------------------------------------------------------------
# DSN трьох ролей
# ---------------------------------------------------------------------
#
# Три ролі, три різні відповіді на «що цій ролі вільно бачити» (0063).
#
# netpulse власник схеми. Ним ходять migrate і cli.
# netpulse_app API і колектор. Без BYPASSRLS — під політиками RLS.
# netpulse_worker фонові такти поверх усіх кабінетів. З BYPASSRLS.
#
# Заповнені NETPULSE_APP_PASSWORD і NETPULSE_WORKER_PASSWORD означають
# «застосунок ходить під RLS». На ЧИСТІЙ базі їх пише установник, а
# netpulse-migrate сам видає ці паролі ролям одразу після накочування
# схеми — тобто нова інсталяція працює під політиками з першої секунди,
# і слова «cutover» клієнт не бачить взагалі.
#
# Порожні означають «як до 0063»: DSN згортається до ролі власника,
# DSN воркера лишається порожнім, store.UseWorkerDSN другого пулу не
# відкриває. Саме на цьому тримається безпека оновлення наявного стенду:
# у його .env цих рядків немає, вирази нижче дають той самий DSN, що й
# раніше, а мігратор на непорожній базі паролів ролям не видає взагалі.
# Перехід для таких інсталяцій — deploy/RLS-EXISTING-INSTALL.md.
#
# Ім'я ролі більше НЕ окрема змінна, і це виправлення, а не спрощення.
# Раніше їх було дві — NETPULSE_APP_USER і NETPULSE_APP_PASSWORD, — і
# будь-яка половина без другої давала DSN, який не встановлюється: нова
# роль зі старим паролем або стара роль з новим. Виглядає це не як
# помилка конфігурації, а як «RLS усе зламав». Тепер прапорець один: є
# пароль — є й роль netpulse_app, немає пароля — немає й ролі.
#
# Вираз читається так. Перша частина додає «netpulse_app:» лише тоді,
# коли пароль заданий. Друга віддає або сам пароль (коли заданий), або
# цілу пару «netpulse:<пароль власника>» (коли ні). Обидві частини
# дивляться на ОДНУ змінну, тому проміжного стану не буває.
x-app-dsn: &app-dsn postgres://${NETPULSE_APP_PASSWORD:+netpulse_app:}${NETPULSE_APP_PASSWORD:-netpulse:$POSTGRES_PASSWORD}@db:5432/netpulse?sslmode=disable
x-worker-dsn: &worker-dsn ${NETPULSE_WORKER_PASSWORD:+postgres://netpulse_worker:${NETPULSE_WORKER_PASSWORD}@db:5432/netpulse?sslmode=disable}
x-owner-dsn: &owner-dsn postgres://netpulse:${POSTGRES_PASSWORD:?потрібен POSTGRES_PASSWORD}@db:5432/netpulse?sslmode=disable
x-server-env: &server-env
NETPULSE_DSN: *app-dsn
NETPULSE_DSN_WORKER: *worker-dsn
NETPULSE_DEK: ${NETPULSE_DEK:?потрібен NETPULSE_DEK — див. deploy/README.md}
NETPULSE_LOG_LEVEL: ${NETPULSE_LOG_LEVEL:-info}
TZ: ${TZ:-Europe/Kyiv}
services:
db:
image: timescale/timescaledb:2.17.2-pg16
environment:
POSTGRES_USER: netpulse
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?потрібен POSTGRES_PASSWORD}
POSTGRES_DB: netpulse
TIMESCALEDB_TELEMETRY: "off"
# Без цього initdb бере кодування з локалі середовища й цілком
# може створити базу як SQL_ASCII. Тексти при цьому виглядають
# цілими, а ламається інше: jsonb із екранованим не-ASCII не
# вставляється, а lower() і сортування працюють як над латиницею.
# Виправити можна лише перестворенням бази, тож задаємо одразу.
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C.UTF-8"
command:
- postgres
- -c
- shared_preload_libraries=timescaledb
- -c
- max_connections=200
- -c
- shared_buffers=${PG_SHARED_BUFFERS:-512MB}
- -c
# Вісім фонових робітників TimescaleDB — розумно на чотирьох
# ядрах і забагато на двох: вони конкурують за ті самі ядра з
# самими запитами, і стиснення чанків починає заважати опитуванню.
# Установник підставляє число за фактичною кількістю ядер.
- timescaledb.max_background_workers=${PG_BG_WORKERS:-8}
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U netpulse -d netpulse"]
interval: 5s
timeout: 5s
retries: 20
restart: unless-stopped
cache:
image: docker.dragonflydb.io/dragonflydb/dragonfly:v1.25.5
# memlock без обмеження прискорює Dragonfly, але дозволений не всюди:
# у контейнерній віртуалізації (LXC, частина VPS) ядро відмовляє, і
# контейнер не стартує взагалі з «error setting rlimit type 8».
# Швидкість того не варта — обмеження знято.
#
# Стеля пам'яті задається явно, бо без неї Dragonfly бере її з
# доступної на машині — і рахує ту саму пам'ять, яку вже порахував
# PG_SHARED_BUFFERS. На машині з 4 ГБ два незалежні розрахунки
# чверті одного пирога закінчуються тим, що ядро вбиває когось
# третього, найімовірніше API. Установник задає обидві цифри разом.
# Штатна команда образу — `dragonfly --logtostderr`; command її
# ЗАМІНЮЄ, а не доповнює, тому обидва слова тут обов'язкові. Без
# --logtostderr журнал іде у файл усередині контейнера, тобто
# `docker logs` мовчить — і контейнер, що падає в циклі, виглядає
# як контейнер без проблем.
#
# --proactor_threads не косметика, а умова роботи: Dragonfly вимагає
# 256 МБ на КОЖЕН потік вводу-виводу й відмовляється стартувати,
# якщо стеля менша. За замовчуванням потоків стільки ж, скільки
# ядер, тож на 4-ядерній машині стеля 512 МБ дає «потрібен 1 ГіБ,
# виходжу». Два потоки для кешу сесій — з запасом.
command:
- dragonfly
- --logtostderr
- --maxmemory=${DRAGONFLY_MAXMEMORY:-512mb}
- --proactor_threads=${DRAGONFLY_THREADS:-2}
volumes:
- cache-data:/data
restart: unless-stopped
# Міграції окремою службою, а не на старті API.
#
# API запускається в кількох примірниках, і накочування схеми зі старту
# означало б гонку між ними. Тут же — один запуск, який мусить
# завершитись успіхом, перш ніж піднімуться API й колектор.
migrate:
build: &server-build
context: .
dockerfile: deploy/Dockerfile.server
args:
VERSION: ${NETPULSE_VERSION:-dev}
COMMIT: ${NETPULSE_COMMIT:-none}
image: netpulse/server:${NETPULSE_VERSION:-dev}
entrypoint: ["netpulse-migrate"]
environment:
<<: *server-env
# Схему котить власник, а не netpulse_app: міграції створюють
# об'єкти, правлять самі політики й іноді переливають дані поверх
# кабінетів. Роль під RLS не змогла б жодного з трьох, а 0063 ще й
# перевіряє права netpulse_app — тобто мусить їх бачити.
NETPULSE_DSN: *owner-dsn
# А цими двома мігратор схему не котить — він ними ЗАХОДИТЬ.
#
# По-перше, на чистій базі саме звідси беруться паролі ролей:
# мігратор витягує пароль із самого DSN, тому в базу лягає рівно
# той рядок, який потім надішле pgx. Розійтись вони не можуть за
# побудовою — це той самий рядок і той самий розбирач.
#
# По-друге, перед тим як пустити api й collector, мігратор кожним
# з цих DSN підключається й дивиться, під якою роллю опинився.
# Саме цієї перевірки бракувало 0063: там перевіряли, що не видно
# чужого, а зламався вхід — тобто «чи видно своє».
NETPULSE_DSN_APP: *app-dsn
NETPULSE_DSN_WORKER: *worker-dsn
depends_on:
db:
condition: service_healthy
restart: "no"
# Утиліти командного рядка під роллю власника: netpulse-user (кабінети
# й люди), netpulse-secret (секрети в core.secrets).
#
# Окрема служба, а не `run --rm api`, бо DSN тут інший. Обидві утиліти
# роблять рівно те, чого роль під RLS робити не має: заводять кабінет
# (INSERT у core.tenants під політикою tenant_self неможливий за
# означенням) і додають людину в чужі за контекстом рядки. Під
# netpulse_app вони не падають, а мовчки нічого не знаходять — тобто
# на новій інсталяції перша ж команда «створити адміністратора»
# виглядала б як успіх, а адміністратора не було б.
#
# profiles: службу не піднімає ні `up`, ні `up -d`; `docker compose run`
# вмикає її профіль сам.
#
# docker compose run --rm --entrypoint netpulse-user cli \
# -tenant default -login admin -role owner
cli:
build: *server-build
image: netpulse/server:${NETPULSE_VERSION:-dev}
profiles: ["cli"]
environment:
<<: *server-env
NETPULSE_DSN: *owner-dsn
NETPULSE_DSN_WORKER: ""
volumes:
- git-data:/var/lib/netpulse
depends_on:
db:
condition: service_healthy
restart: "no"
api:
build: *server-build
image: netpulse/server:${NETPULSE_VERSION:-dev}
entrypoint: ["netpulse-api"]
command:
- -listen=:8080
- -alert-interval=${NETPULSE_ALERT_INTERVAL:-30s}
environment:
<<: *server-env
NETPULSE_JWT_SECRET: ${NETPULSE_JWT_SECRET:?потрібен NETPULSE_JWT_SECRET — не менше 32 байтів}
# Історія конфігів у Git. Спільний том для API й колектора:
# колектор туди пише під час бекапу, API звідти читає для порівняння
# версій. Різні томи означали б, що половина історії видна одному
# процесу, а половина другому.
volumes:
- git-data:/var/lib/netpulse
depends_on:
migrate:
condition: service_completed_successfully
cache:
condition: service_started
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/healthz"]
interval: 10s
timeout: 3s
retries: 6
start_period: 15s
restart: unless-stopped
# Колектор зондів. TLS знімає Caddy, всередині мережі — h2c, тому
# -insecure. Порт назовні сам не публікує: до нього ходять через
# проксі, який має справжній сертифікат.
collector:
build: *server-build
image: netpulse/server:${NETPULSE_VERSION:-dev}
entrypoint: ["netpulse-server"]
command:
- -listen=:9443
- -insecure
environment: *server-env
# Історія конфігів у Git. Спільний том для API й колектора:
# колектор туди пише під час бекапу, API звідти читає для порівняння
# версій. Різні томи означали б, що половина історії видна одному
# процесу, а половина другому.
volumes:
- git-data:/var/lib/netpulse
depends_on:
migrate:
condition: service_completed_successfully
restart: unless-stopped
proxy:
image: caddy:2.8-alpine
environment:
NETPULSE_DOMAIN: ${NETPULSE_DOMAIN:?потрібен NETPULSE_DOMAIN}
# Порожня пошта означає «домену ще немає» — тоді самопідписаний
# сертифікат. Інакше Caddy нескінченно проситиме Let's Encrypt
# видати сертифікат на IP, чого той не робить, і сайт лишиться
# без TLS зовсім.
NETPULSE_TLS: ${ACME_EMAIL:-internal}
volumes:
- ./deploy/Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
ports:
- "80:80"
- "443:443"
# Окремий порт для зондів: вони говорять gRPC, а не HTTP, і
# ділити з ним 443 означало б розрізняти протоколи за шляхом —
# зайва крихкість там, де порт коштує нічого.
- "9443:9443"
depends_on:
- api
- collector
restart: unless-stopped
# Зонд на тому ж хості, що й сервер: базовий моніторинг самої
# інсталяції. Профіль, а не звичайна служба: у типовому розгортанні
# зонди стоять у мережах клієнтів, а не поруч із сервером.
agent:
build:
context: .
dockerfile: deploy/Dockerfile.agent
args:
VERSION: ${NETPULSE_VERSION:-dev}
COMMIT: ${NETPULSE_COMMIT:-none}
image: netpulse/agent:${NETPULSE_VERSION:-dev}
profiles: ["agent"]
command:
- -server=collector:9443
- -insecure
- -modules=icmp,snmp,topology,ncm,traps
- -traps-listen=${NETPULSE_TRAPS_LISTEN:-:162}
environment:
NETPULSE_ENROLL: ${NETPULSE_ENROLL:-}
NETPULSE_NAME: ${NETPULSE_AGENT_NAME:-локальний зонд}
TZ: ${TZ:-Europe/Kyiv}
# Порт трапів назовні. Прив'язка задається змінною, а не зашита:
# 162/udp без автентифікації приймає будь-кого, хто знає адресу, і
# на машині з публічним інтерфейсом «усі інтерфейси» — це рішення, а
# не типове значення. Порожній NETPULSE_TRAPS_BIND лишає службу
# всередині мережі docker, і зовні вона недосяжна.
ports:
- "${NETPULSE_TRAPS_BIND:-127.0.0.1}:162:162/udp"
cap_add:
- NET_RAW
# 162 нижче 1024, а процес усередині — не root. Без цього зонд
# порт не займе; він про це кричить у журнал, але приймати трапи
# все одно не буде.
- NET_BIND_SERVICE
volumes:
- agent-identity:/var/lib/netpulse
depends_on:
- collector
restart: unless-stopped
volumes:
db-data:
git-data:
cache-data:
caddy-data:
caddy-config:
agent-identity: