docs: update README and docs with strict Quick Start (docker-compose-prod), fix WebSocket paths, enforce strict tone
This commit is contained in:
113
release/docker-compose-prod.tmpl.yaml
Normal file
113
release/docker-compose-prod.tmpl.yaml
Normal file
@@ -0,0 +1,113 @@
|
||||
services:
|
||||
logboard:
|
||||
image: REGISTRY_PLACEHOLDER/IMAGE_NAME_PLACEHOLDER:IMAGE_TAG_PLACEHOLDER
|
||||
container_name: logboard
|
||||
environment:
|
||||
# ОСНОВНЫЕ НАСТРОЙКИ ПРИЛОЖЕНИЯ
|
||||
# Порт веб-интерфейса LogBoard+
|
||||
LOGBOARD_PORT: "${LOGBOARD_PORT}"
|
||||
# Количество строк логов по умолчанию (tail)
|
||||
LOGBOARD_TAIL: "${LOGBOARD_TAIL}"
|
||||
# Имя пользователя для входа
|
||||
LOGBOARD_USER: "${LOGBOARD_USER}"
|
||||
# Пароль для входа (обязательно поменять в продакшене)
|
||||
LOGBOARD_PASS: "${LOGBOARD_PASS}"
|
||||
# Директория для снимков логов (в контейнере)
|
||||
LOGBOARD_SNAPSHOT_DIR: "${LOGBOARD_SNAPSHOT_DIR}"
|
||||
# Директория для статических файлов
|
||||
LOGBOARD_STATIC_DIR: "${LOGBOARD_STATIC_DIR}"
|
||||
# Путь к HTML шаблону главной страницы
|
||||
LOGBOARD_INDEX_HTML: "${LOGBOARD_INDEX_HTML}"
|
||||
# Таймзона для временных меток (например: Europe/Moscow, UTC)
|
||||
TZ_TS: "${TZ_TS}"
|
||||
|
||||
# НАСТРОЙКИ DOCKER
|
||||
# Фильтр по проекту Docker Compose (опционально)
|
||||
COMPOSE_PROJECT_NAME: "${COMPOSE_PROJECT_NAME}"
|
||||
# Перечень проектов для отображения (через запятую)
|
||||
LOGBOARD_PROJECTS: "${LOGBOARD_PROJECTS}"
|
||||
# Путь к Docker socket / удалённому хосту
|
||||
DOCKER_HOST: "${DOCKER_HOST}"
|
||||
# Включить проверку TLS для Docker (удалённые хосты)
|
||||
DOCKER_TLS_VERIFY: "${DOCKER_TLS_VERIFY}"
|
||||
# Путь к TLS сертификатам Docker
|
||||
DOCKER_CERT_PATH: "${DOCKER_CERT_PATH}"
|
||||
# Внешние сети Docker (через запятую)
|
||||
DOCKER_NETWORKS: "${DOCKER_NETWORKS}"
|
||||
|
||||
# БЕЗОПАСНОСТЬ
|
||||
# Секретный ключ для JWT (обязательно поменять в продакшене)
|
||||
SECRET_KEY: "${SECRET_KEY}"
|
||||
# Ключ шифрования для чувствительных данных (обязательно поменять)
|
||||
ENCRYPTION_KEY: "${ENCRYPTION_KEY}"
|
||||
|
||||
# ЛОГИРОВАНИЕ
|
||||
# Уровень логирования (DEBUG, INFO, WARNING, ERROR)
|
||||
LOG_LEVEL: "${LOG_LEVEL}"
|
||||
# Формат логов (json, text)
|
||||
LOG_FORMAT: "${LOG_FORMAT}"
|
||||
|
||||
# ВЕБ-ИНТЕРФЕЙС
|
||||
# Заголовок веб-интерфейса
|
||||
WEB_TITLE: "${WEB_TITLE}"
|
||||
# Описание веб-интерфейса
|
||||
WEB_DESCRIPTION: "${WEB_DESCRIPTION}"
|
||||
# Версия веб-интерфейса
|
||||
WEB_VERSION: "${WEB_VERSION}"
|
||||
|
||||
# РЕЖИМ РАЗРАБОТКИ
|
||||
# Режим отладки (true/false)
|
||||
DEBUG_MODE: "${DEBUG_MODE}"
|
||||
|
||||
# ПРОИЗВОДИТЕЛЬНОСТЬ
|
||||
# Максимум одновременных подключений
|
||||
MAX_CONNECTIONS: "${MAX_CONNECTIONS}"
|
||||
# Таймаут подключения (сек)
|
||||
CONNECTION_TIMEOUT: "${CONNECTION_TIMEOUT}"
|
||||
# Таймаут чтения (сек)
|
||||
READ_TIMEOUT: "${READ_TIMEOUT}"
|
||||
|
||||
# ФИЛЬТРАЦИЯ КОНТЕЙНЕРОВ
|
||||
# Пропускать контейнеры с проблемным health check (true/false)
|
||||
LOGBOARD_SKIP_UNHEALTHY: "${LOGBOARD_SKIP_UNHEALTHY}"
|
||||
# Таймаут получения списка контейнеров (сек)
|
||||
LOGBOARD_CONTAINER_LIST_TIMEOUT: "${LOGBOARD_CONTAINER_LIST_TIMEOUT}"
|
||||
# Таймаут получения информации о контейнере (сек)
|
||||
LOGBOARD_CONTAINER_INFO_TIMEOUT: "${LOGBOARD_CONTAINER_INFO_TIMEOUT}"
|
||||
# Таймаут health check контейнера (сек)
|
||||
LOGBOARD_HEALTH_CHECK_TIMEOUT: "${LOGBOARD_HEALTH_CHECK_TIMEOUT}"
|
||||
|
||||
# АУТЕНТИФИКАЦИЯ
|
||||
# Включить аутентификацию (true/false)
|
||||
AUTH_ENABLED: "${AUTH_ENABLED}"
|
||||
# Метод аутентификации (jwt)
|
||||
AUTH_METHOD: "${AUTH_METHOD}"
|
||||
# Время жизни сессии (сек)
|
||||
SESSION_TIMEOUT: "${SESSION_TIMEOUT}"
|
||||
|
||||
# УВЕДОМЛЕНИЯ
|
||||
# Включить уведомления по email (true/false)
|
||||
NOTIFICATIONS_ENABLED: "${NOTIFICATIONS_ENABLED}"
|
||||
# SMTP сервер
|
||||
SMTP_HOST: "${SMTP_HOST}"
|
||||
# Порт SMTP
|
||||
SMTP_PORT: "${SMTP_PORT}"
|
||||
# Пользователь SMTP
|
||||
SMTP_USER: "${SMTP_USER}"
|
||||
# Пароль SMTP
|
||||
SMTP_PASS: "${SMTP_PASS}"
|
||||
# Email отправителя
|
||||
SMTP_FROM: "${SMTP_FROM}"
|
||||
|
||||
# AJAX ОБНОВЛЕНИЕ
|
||||
# Интервал AJAX обновления логов (мс)
|
||||
LOGBOARD_AJAX_UPDATE_INTERVAL: "${LOGBOARD_AJAX_UPDATE_INTERVAL}"
|
||||
|
||||
ports:
|
||||
- "${LOGBOARD_PORT}:${LOGBOARD_PORT}"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- ./snapshots:/app/snapshots
|
||||
restart: unless-stopped
|
||||
user: 0:0
|
||||
|
||||
101
release/generate_compose.py
Normal file
101
release/generate_compose.py
Normal file
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Генерация docker-compose-prod.yaml из шаблона и env.example.
|
||||
|
||||
Автор: Сергей Антропов
|
||||
Сайт: https://devops.org.ru
|
||||
|
||||
Поведение:
|
||||
- Читает env.example и подставляет значения в docker-compose-prod.tmpl.yaml
|
||||
- Запрашивает (или берет из ENV/CLI) REGISTRY_HOST, IMAGE_NAME_FULL, IMAGE_TAG
|
||||
- Заменяет плейсхолдеры REGISTRY_PLACEHOLDER/IMAGE_NAME_PLACEHOLDER/IMAGE_TAG_PLACEHOLDER
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
from typing import Dict
|
||||
|
||||
|
||||
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
|
||||
|
||||
|
||||
def parse_env_file(env_path: str) -> Dict[str, str]:
|
||||
variables: Dict[str, str] = {}
|
||||
with io.open(env_path, "r", encoding="utf-8") as fh:
|
||||
for raw in fh:
|
||||
line = raw.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
if "=" not in line:
|
||||
continue
|
||||
key, val = line.split("=", 1)
|
||||
key = key.strip()
|
||||
val = val.strip()
|
||||
if (val.startswith('"') and val.endswith('"')) or (
|
||||
val.startswith("'") and val.endswith("'")
|
||||
):
|
||||
val = val[1:-1]
|
||||
variables[key] = val
|
||||
return variables
|
||||
|
||||
|
||||
def substitute_placeholders(text: str, env: Dict[str, str]) -> str:
|
||||
pattern = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}")
|
||||
return pattern.sub(lambda m: env.get(m.group(1), m.group(0)), text)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Генерация docker-compose-prod.yaml")
|
||||
parser.add_argument("--template", default=os.path.join(ROOT, "release", "docker-compose-prod.tmpl.yaml"))
|
||||
parser.add_argument("--output", default=os.path.join(ROOT, "docker-compose-prod.yaml"))
|
||||
parser.add_argument("--env", dest="env_path", default=os.path.join(ROOT, "env.example"))
|
||||
parser.add_argument("--registry", default=os.getenv("REGISTRY_HOST", ""))
|
||||
parser.add_argument("--image", dest="image", default=os.getenv("IMAGE_NAME_FULL", ""))
|
||||
parser.add_argument("--tag", dest="tag", default=os.getenv("IMAGE_TAG", ""))
|
||||
args = parser.parse_args()
|
||||
|
||||
env = parse_env_file(args.env_path)
|
||||
# Подстановка ${VAR} из env.example
|
||||
with io.open(args.template, "r", encoding="utf-8") as fh:
|
||||
content = fh.read()
|
||||
content = substitute_placeholders(content, env)
|
||||
|
||||
# Комментирование строк, где остались неразрешенные ${VAR}
|
||||
# Ищем строки формата 'KEY: "${VAR}"' или 'KEY: ${VAR}' и комментируем их
|
||||
lines = content.splitlines()
|
||||
commented: list[str] = []
|
||||
unresolved_pattern = re.compile(r"^([ \t-]*[^:#\n]+:\s*)(\"?\$\{[A-Za-z_][A-Za-z0-9_]*\}\"?)\s*$")
|
||||
for line in lines:
|
||||
if unresolved_pattern.match(line):
|
||||
commented.append("# " + line)
|
||||
else:
|
||||
commented.append(line)
|
||||
content = "\n".join(commented) + ("\n" if content.endswith("\n") else "")
|
||||
|
||||
# Плейсхолдеры реестра/имени/тега
|
||||
registry = args.registry.strip() or input("Введите Docker Registry (например, ghcr.io или docker.io): ").strip() or "docker.io"
|
||||
if registry == "registry.hub.docker.com":
|
||||
registry = "docker.io"
|
||||
image = args.image.strip() or input(f"Введите имя образа (например, inecs/logboard) (по умолчанию: logboard): ").strip() or "logboard"
|
||||
tag = args.tag.strip() or input("Введите тег образа (по умолчанию: latest): ").strip() or "latest"
|
||||
|
||||
content = content.replace("REGISTRY_PLACEHOLDER", registry)
|
||||
content = content.replace("IMAGE_NAME_PLACEHOLDER", image)
|
||||
content = content.replace("IMAGE_TAG_PLACEHOLDER", tag)
|
||||
|
||||
with io.open(args.output, "w", encoding="utf-8") as fh:
|
||||
fh.write(content)
|
||||
|
||||
print(f"Файл {os.path.relpath(args.output, ROOT)} сгенерирован")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
|
||||
142
release/publish_image.py
Normal file
142
release/publish_image.py
Normal file
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Скрипт публикации Docker-образа (multi-arch) в реестр.
|
||||
|
||||
Автор: Сергей Антропов
|
||||
Сайт: https://devops.org.ru
|
||||
|
||||
Поведение:
|
||||
- Собирает multi-arch образ с использованием docker buildx
|
||||
- Логинится в реестр (пароль можно передать через аргумент/ENV, либо будет запрошен интерактивно)
|
||||
- Публикует образ
|
||||
|
||||
Параметры через аргументы/ENV:
|
||||
- --registry / $REGISTRY_HOST (например, docker.io, ghcr.io)
|
||||
- --image / $IMAGE_NAME_FULL (например, inecs/logboard)
|
||||
- --tag / $IMAGE_TAG (например, v1 или короткий sha)
|
||||
- --user / $REG_USER
|
||||
- --password / $REG_PASS
|
||||
- --platforms / $PLATFORMS (по умолчанию: linux/amd64,linux/arm64)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import getpass
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import List
|
||||
|
||||
|
||||
def run(cmd: List[str], check: bool = True, quiet: bool = False) -> subprocess.CompletedProcess:
|
||||
stdout = subprocess.DEVNULL if quiet else None
|
||||
stderr = subprocess.STDOUT if quiet else None
|
||||
return subprocess.run(cmd, check=check, stdout=stdout, stderr=stderr)
|
||||
|
||||
|
||||
def git_short_sha_default() -> str:
|
||||
try:
|
||||
out = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"], stderr=subprocess.DEVNULL)
|
||||
return out.decode("utf-8").strip() or "latest"
|
||||
except Exception:
|
||||
return "latest"
|
||||
|
||||
|
||||
def ensure_buildx_builder(builder_name: str) -> None:
|
||||
try:
|
||||
run(["docker", "buildx", "inspect", builder_name], check=True, quiet=True)
|
||||
except subprocess.CalledProcessError:
|
||||
run(["docker", "buildx", "create", "--name", builder_name, "--use"], check=True)
|
||||
run(["docker", "buildx", "use", builder_name], check=True)
|
||||
|
||||
|
||||
def ensure_binfmt() -> None:
|
||||
try:
|
||||
run(["docker", "run", "--privileged", "--rm", "tonistiigi/binfmt", "--install", "all"], check=False, quiet=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Публикация Docker-образа в реестр (multi-arch)")
|
||||
parser.add_argument("--registry", default=os.getenv("REGISTRY_HOST", ""))
|
||||
parser.add_argument("--image", dest="image", default=os.getenv("IMAGE_NAME_FULL", ""))
|
||||
parser.add_argument("--tag", dest="tag", default=os.getenv("IMAGE_TAG", ""))
|
||||
parser.add_argument("--user", dest="user", default=os.getenv("REG_USER", ""))
|
||||
parser.add_argument("--password", dest="password", default=os.getenv("REG_PASS", ""))
|
||||
parser.add_argument("--platforms", default=os.getenv("PLATFORMS", "linux/amd64,linux/arm64"))
|
||||
parser.add_argument("--builder", default=os.getenv("BUILDX_BUILDER", "logboard_builder"))
|
||||
args = parser.parse_args()
|
||||
|
||||
# 1) Определяем реестр, поддерживая ввод вида "host" или "host/namespace"
|
||||
registry_input = (args.registry or os.getenv("REGISTRY_HOST", "")).strip()
|
||||
if not registry_input:
|
||||
registry_input = input(
|
||||
"Введите Docker Registry (например, ghcr.io, docker.io, registry.hub.docker.com/namespace): "
|
||||
).strip()
|
||||
registry_host = registry_input.split("/", 1)[0] if registry_input else "docker.io"
|
||||
registry_path = ""
|
||||
if "/" in registry_input:
|
||||
registry_path = registry_input.split("/", 1)[1]
|
||||
if not registry_host or registry_host == "registry.hub.docker.com":
|
||||
registry_host = "docker.io"
|
||||
|
||||
# 2) Имя образа. Если не задано, спрашиваем пользователя (по умолчанию logboard)
|
||||
image_input = (args.image or os.getenv("IMAGE_NAME_FULL", "")).strip()
|
||||
if not image_input:
|
||||
image_input = input(
|
||||
"Введите имя образа (по умолчанию: logboard): "
|
||||
).strip() or "logboard"
|
||||
# Если пользователь ввёл только имя без namespace, но registry_path задан, добавим префикс
|
||||
if "/" not in image_input and registry_path:
|
||||
image_full = f"{registry_path}/{image_input}"
|
||||
else:
|
||||
image_full = image_input
|
||||
|
||||
# 3) Тег: если не задан, спросим. По умолчанию короткий SHA
|
||||
tag_input = (args.tag or os.getenv("IMAGE_TAG", "")).strip()
|
||||
if not tag_input:
|
||||
default_tag = git_short_sha_default()
|
||||
tag_input = input(f"Введите тег образа (по умолчанию: {default_tag}): ").strip() or default_tag
|
||||
|
||||
user = (args.user or os.getenv("REG_USER", "")).strip()
|
||||
password = args.password
|
||||
if not user:
|
||||
user = input("Введите имя пользователя реестра: ").strip()
|
||||
if not password:
|
||||
password = getpass.getpass("Введите пароль реестра: ")
|
||||
|
||||
image_remote = f"{registry_host}/{image_full}:{tag_input}"
|
||||
|
||||
print(f"Логин в реестр: {registry_host}")
|
||||
p = subprocess.Popen(["docker", "login", registry_host, "-u", user, "--password-stdin"], stdin=subprocess.PIPE)
|
||||
p.communicate(input=password.encode("utf-8"))
|
||||
if p.returncode != 0:
|
||||
print("Ошибка логина в реестр", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print("Установка binfmt (для multi-arch)...")
|
||||
ensure_binfmt()
|
||||
print(f"Подготовка buildx builder: {args.builder}")
|
||||
ensure_buildx_builder(args.builder)
|
||||
|
||||
print(f"Сборка и публикация образа: {image_remote}")
|
||||
run([
|
||||
"docker", "buildx", "build",
|
||||
"--platform", args.platforms,
|
||||
"-t", image_remote,
|
||||
"--push",
|
||||
".",
|
||||
], check=True)
|
||||
|
||||
print("Образ опубликован успешно!")
|
||||
print("Готово. Для генерации compose выполните: make release-compose")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
|
||||
Reference in New Issue
Block a user