Я использую Supabase в self-hosted режиме: сервисы работают в Docker Compose, включая Postgres, Kong, GoTrue, PostgREST, Storage, Realtime, Edge Functions, Logflare и Vector. Перед Kong стоит Nginx, который завершает TLS и проксирует публичное API.Мне нужно было закрыть две задачи Читать далее
Я развернул self-hosted Supabase в Docker и собрал для него отдельный observability-контур. Метрики собирает
vmagentи хранит VictoriaMetrics, логи собирает Vector и отправляет в VictoriaLogs, а Grafana стала единым интерфейсом. Сначала я прокидывал логи через HTTP-адаптер на Edge Function, но затем перенёс ingestion в Vector: он уже входит в Supabase-стек, читает docker-логи, умеет нормализовать их в VRL и отправляет данные батчами на отдельный сервер мониторинга.
Я использую Supabase в self-hosted режиме: сервисы работают в Docker Compose, включая Postgres, Kong, GoTrue, PostgREST, Storage, Realtime, Edge Functions, Logflare и Vector. Перед Kong стоит Nginx, который завершает TLS и проксирует публичное API.
Мне нужно было закрыть две задачи:
Собирать метрики хоста, PostgreSQL и доступности компонентов Supabase.
Видеть логи всех контейнеров, в первую очередь Edge Functions, и быстро искать ошибки в Grafana.
В self-hosted варианте Supabase нет готового managed Metrics API, поэтому метрики нужно снимать напрямую с инфраструктурных компонентов и exporter’ов.
Инфраструктура разделена на две машины. На сервере приложения работает Supabase и штатный Vector; на сервере мониторинга - VictoriaMetrics, VictoriaLogs, Grafana и Nginx.
┌────────────────────────────────────────────────────┐
│ Сервер приложения │
│ │
│ Docker Compose: │
│ Postgres / Kong / Auth / REST / Storage / Realtime │
│ Edge Functions / Logflare / Vector │
│ │
│ docker json-file logs │
│ │ │
│ ▼ │
│ Vector: docker_logs + VRL │
└─────────────────────────┬──────────────────────────┘
│ HTTPS + JSONLine batches
│
▼
┌────────────────────────────────────────────────────┐
│ Сервер мониторинга │
│ │
│ Nginx: allow <APP_SERVER_IP>; deny all; │
│ │ localhost HTTP │
│ ▼ │
│ VictoriaLogs ◄────────────────────── Grafana │
│ │
│ vmagent ───► VictoriaMetrics ◄─────── Grafana │
└────────────────────────────────────────────────────┘
VictoriaMetrics хранит метрики - числовые временные ряды. VictoriaLogs хранит и индексирует логи, а Grafana подключается к обоим системам отдельными datasource.
vmagent работает по привычной Prometheus-модели: он скрейпит endpoint’ы по конфигурации и передаёт данные в удалённое хранилище через remote write. Он также умеет фильтровать данные и менять labels перед отправкой.
Это означает, что можно использовать стандартный scrape.yml, PromQL и готовые Grafana-дашборды для node-exporter или PostgreSQL.
Для self-hosted Supabase я снимаю метрики с трёх уровней:
node-exporter - CPU, RAM, диск, сеть и состояние самого сервера.
postgres-exporter - метрики PostgreSQL.
blackbox-exporter - HTTP-проверки доступности Kong, Auth, PostgREST и Storage.
PostgreSQL exporter в примерах статьи доступен на digest.the-signal.ru:9187; этот instance можно зафиксировать в Grafana-дашборде, чтобы панели не подхватывали лишние значения.
global:
scrape_interval: 15s
scrape_timeout: 10s
scrape_configs:
- job_name: node_exporter
static_configs:
- targets:
- digest.the-signal.ru:9100
- job_name: postgres_exporter
static_configs:
- targets:
- digest.the-signal.ru:9187
- job_name: supabase_health
metrics_path: /probe
params:
module:
- http_2xx
static_configs:
- targets:
- http://kong:8000/health
- http://auth:9999/health
- http://rest:3000/ready
- http://storage:5000/status
relabel_configs:
- source_labels:
- __address__
target_label: __param_target
- source_labels:
- __param_target
target_label: instance
- target_label: __address__
replacement: blackbox:9115
Blackbox exporter публикует probe_success = 1, если target вернул успешный HTTP-ответ; при сетевой ошибке, таймауте или неподходящем статус-коде метрика будет равна 0.
Первой рабочей схемой был простой HTTP-адаптер на Edge Function.
Supabase / Logflare
│ HTTP POST
▼
Edge Function-адаптер
│ HTTP JSONLine
▼
VictoriaLogs
Функция принимала входящий JSON, приводила поля к ожидаемому формату и пересылала записи в VictoriaLogs. Это был быстрый способ проверить путь от генерации лога до его появления в Grafana.
Пример отправки одной записи:
curl -X POST \
"https://logs.example.com/insert/jsonline?\_stream\_fields=appname,project&\_msg\_field=event\_message&\_time\_field=timestamp" \
-H "Content-Type: application/stream+json" \
-d '{
"timestamp": "2026-06-04T14:24:26.545Z",
"event_message": "edge function invoked",
"appname": "supabase-edge-functions",
"project": "default",
"metadata": {
"level": "INFO"
}
}'
VictoriaLogs поддерживает HTTP ingestion и позволяет задать поле текстового сообщения через msgfield, время через timefield, а идентификаторы логового потока - через streamfields.
HTTP-адаптер оказался полезным proof of concept, но плохим кандидатом на постоянную архитектуру.
Во-первых, он добавлял ещё один сервис и сетевой переход: нужно поддерживать Edge Function, маршрут в Kong и отдельно искать ошибки доставки. Во-вторых, разные компоненты Supabase используют разные форматы логов: структурированный JSON, обычные строки, вложенные поля. Логика нормализации быстро начинала расползаться по коду адаптера.
Главное же - Vector уже присутствует в self-hosted Docker Compose Supabase и уже умеет читать docker-логи. Значит отдельный ingestion-сервис дублировал часть существующего pipeline.
В новой схеме Vector взял на себя три роли:
Читать stdout/stderr контейнеров через Docker API.
Превращать разные форматы записей в единый контракт через VRL.
Отправлять нормализованные логи батчами в VictoriaLogs.
Docker containers
│ stdout / stderr
▼
Docker json-file driver
│
▼
Vector: docker_logs source
│
▼
VRL transforms
│
▼
HTTP sink
│
▼
Nginx на сервере мониторинга
│
▼
VictoriaLogs
docker_logs - source Vector для чтения контейнерных логов. Он добавляет к событиям Docker-контекст, а сбор можно ограничить через include_containers и exclude_containers.
VictoriaLogs официально поддерживает приём логов из Vector, в том числе через HTTP JSON.
VictoriaLogs находится на сервере мониторинга, отдельно от Supabase. Поэтому Vector не обращается к внутреннему Docker hostname, а отправляет логи по HTTPS на Nginx.
Сам VictoriaLogs я не открываю наружу: он слушает только loopback-интерфейс.
services:
victoria-logs:
image: victoriametrics/victoria-logs:latest
container_name: victoria-logs
restart: unless-stopped
command:
- "-storageDataPath=/victoria-logs-data"
- "-retentionPeriod=14d"
- "-httpListenAddr=127.0.0.1:9428"
volumes:
- victoria-logs-data:/victoria-logs-data
В реальной конфигурации лучше использовать конкретную версию образа вместо latest, а retention выбирать по фактическому потоку логов, объёму диска и нужной глубине истории.
Nginx на сервере мониторинга принимает запросы Vector по HTTPS и пропускает их только с внешнего IP сервера приложения.
server {
listen 443 ssl http2;
server_name logs.example.com;
ssl_certificate /etc/letsencrypt/live/logs.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/logs.example.com/privkey.pem;
location /insert/ {
# Внешний IP сервера с Supabase и Vector
allow 1.2.3.4;
deny all;
proxy_pass http://127.0.0.1:9428;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
client_max_body_size 20m;
proxy_request_buffering off;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
}
}
Такой allowlist не отменяет firewall, но добавляет прикладной сетевой барьер: запросы с любого IP, кроме разрешённого, Nginx отклонит с 403 Forbidden.
Порт VictoriaLogs 9428 в интернет не публикуется: внешний ingestion идёт только к Nginx по HTTPS, а он проксирует трафик локально на 127.0.0.1:9428.
VRL (Vector Remap Language) - встроенный язык преобразования событий в Vector. Он выполняется для каждой записи отдельно и позволяет извлекать и добавлять поля, приводить типы, разбирать JSON, фильтровать записи и формировать стандартизированный payload перед отправкой.
После docker_logs событие может выглядеть примерно так:
{
"message": "{\"level\":\"error\",\"msg\":\"payment webhook failed\"}",
"container_name": "supabase-edge-functions",
"timestamp": "2026-06-07T18:11:42.123Z"
}
После обработки я хочу получить стабильную структуру:
{
"timestamp": "2026-06-07T18:11:42.123Z",
"project": "default",
"appname": "supabase-edge-functions",
"service": "functions",
"event_message": "payment webhook failed",
"metadata": {
"level": "ERROR"
}
}
Тогда запросы и дашборды не зависят от формата stdout конкретного контейнера: достаточно полей service, appname, event_message и metadata.level.
В конфигурации Vector есть три типа компонентов:
БлокНазначениеВ этой конфигурацииsourcesПолучают событияdocker_logs забирает логи контейнеровtransformsМеняют или фильтруют событияVRL разбирает и нормализует записиsinksДоставляют результатHTTP JSONLine в VictoriaLogs
Ниже упрощённый volumes/logs/vector.yml.
api:
enabled: true
address: 0.0.0.0:9001
sources:
docker_host:
type: docker_logs
exclude_containers:
- supabase-vector
transforms:
project_logs:
type: remap
inputs:
- docker_host
source: |-
.appname = .container_name ?? "unknown"
.project = "default"
.timestamp = .timestamp ?? now()
.event_message = string!(.message)
functions_logs:
type: remap
inputs:
- project_logs
source: |-
if .appname != "supabase-edge-functions" {
abort
}
.service = "functions"
parsed, err = parse_json(.event_message)
if err == null {
.event_message = parsed.event_message ?? parsed.msg ?? .event_message
.metadata = parsed.metadata ?? {}
}
if is_null(.event_message) || .event_message == "" {
.event_message = "empty log"
}
.metadata.level = .metadata.level ?? "INFO"
message = downcase(string!(.event_message))
if contains(message, "error") {
.metadata.level = "ERROR"
} else if contains(message, "warn") {
.metadata.level = "WARN"
} else if contains(message, "debug") {
.metadata.level = "DEBUG"
}
other_supabase_logs:
type: remap
inputs:
- project_logs
source: |-
if .appname == "supabase-edge-functions" {
abort
}
.service = .appname
.metadata.level = "INFO"
if is_null(.event_message) || .event_message == "" {
.event_message = "empty log"
}
sinks:
victoria_logs:
type: http
inputs:
- functions_logs
- other_supabase_logs
method: post
uri: "https://logs.example.com/insert/jsonline?\_stream\_fields=service,appname,project&\_msg\_field=event\_message&\_time\_field=timestamp"
encoding:
codec: json
framing:
method: newline_delimited
compression: gzip
request:
retry_max_duration_secs: 10
headers:
Content-Type: application/stream+json
Здесь HTTP sink отправляет newline-delimited JSON на endpoint VictoriaLogs. Параметры streamfields, msgfield и timefield указывают, какие поля образуют поток, содержат текст сообщения и время события.
Главная ценность Vector в этой схеме - не просто отправка логов, а нормализация разнородных сообщений с помощью VRL.
if .appname != "supabase-edge-functions" {
abort
}
Поле .appname выше сформировано из имени Docker-контейнера. abort прекращает обработку текущего события в ветке functions_logs: лог Kong, Postgres или Storage не попадёт в transform, предназначенный для Edge Functions.
.service = "functions"
Имя контейнера - инфраструктурная деталь, а service - стабильное доменное поле. В Grafana можно искать так:
service:functions
Даже если впоследствии контейнер получит другое имя, запросы к логам не нужно будет переписывать.
parsed, err = parse_json(.event_message)
if err == null {
.event_message = parsed.event_message ?? parsed.msg ?? .event_message
.metadata = parsed.metadata ?? {}
}
Логи Edge Functions не всегда имеют одинаковую форму: некоторые уже являются JSON, другие - простой текст. parse_json возвращает распарсенное значение и ошибку. Если это не JSON, err не равен null, transform сохраняет исходное сообщение и не ломает pipeline.
Оператор ?? означает fallback: взять значение слева, но если его нет, попробовать следующее. Здесь сообщение выбирается из event_message, затем msg, затем сохраняется исходная строка.
if is_null(.event_message) || .event_message == "" {
.event_message = "empty log"
}
В sink задано:
_msg_field=event_message
Значит VictoriaLogs ожидает, что в каждой записи есть непустой event_message. Если поле сообщения отсутствует, будет предупреждение missing _msg field.
Иногда полезнее отбрасывать пустые записи:
if is_null(.event_message) || .event_message == "" {
abort
}
Я оставил значение empty log, потому что при отладке Edge Functions полезно не терять факт технического события. Для шумных системных логов лучше использовать abort.
.metadata.level = .metadata.level ?? "INFO"
message = downcase(string!(.event_message))
if contains(message, "error") {
.metadata.level = "ERROR"
} else if contains(message, "warn") {
.metadata.level = "WARN"
} else if contains(message, "debug") {
.metadata.level = "DEBUG"
}
Если приложение уже прислало структурированный уровень, он используется. Если нет - применяется INFO, а затем простая эвристика по тексту. Такой подход не заменяет структурированные application logs, но делает обычные сообщения stdout/stderr удобнее для фильтрации.
VRL не позволяет игнорировать ошибки потенциально опасных функций. Такие функции называются fallible: например, JSON может не распарситься, преобразование может получить значение неверного типа, а поле может отсутствовать. VRL требует явной обработки этих ошибок.
Проблемный код обычно выглядит так:
.message = strip_whitespace(parsed.msg)
Здесь parsed.msg может отсутствовать или не быть строкой. У меня похожее использование strip_whitespace(parsed.msg) и привело к ошибке E103.
Надёжнее сначала обработать parsing и тип:
parsed, err = parse_json(.event_message)
if err == null && exists(parsed.msg) {
value, trim_err = strip_whitespace(to_string(parsed.msg))
if trim_err == null {
.event_message = value
}
}
Суффикс !, например в string!(.event_message), означает, что при ошибке обработка конкретного события завершится. Его стоит использовать там, где тип гарантирован предыдущими проверками или потеря одной записи приемлема.
После изменения vector.yml я сначала валидирую конфигурацию:
docker exec supabase-vector \
vector validate /etc/vector/vector.yml
После успешной проверки перезапускаю только Vector:
docker compose restart vector
docker logs supabase-vector --tail=100 -f
На сервере мониторинга можно проверить, что записи действительно попадают в VictoriaLogs:
curl -G \
"http://127.0.0.1:9428/select/logsql/query" \
--data-urlencode "query=*" \
--data-urlencode "limit=20" | jq
VictoriaLogs поддерживает LogSQL-запросы через endpoint /select/logsql/query.
Для диагностики Edge Functions я использую запросы такого вида:
service:functions
service:functions AND metadata.level:ERROR
appname:supabase-edge-functions AND _msg:timeout
Основной текст лога в VictoriaLogs находится в _msg, а fields позволяет вывести нужные поля в результатах запроса.
В Grafana я подключил два datasource:
VictoriaMetrics через Prometheus datasource - графики, PromQL и алерты.
VictoriaLogs datasource - Explore и поиск по логам.
В результате сценарий расследования выглядит так: сначала на графике видно падение probe_success у одного из сервисов, затем в Grafana Explore можно открыть логи нужного сервиса и отфильтровать ERROR за тот же интервал времени.
Получился самостоятельный контур наблюдаемости для self-hosted Supabase:
VictoriaMetrics хранит метрики хоста, PostgreSQL и health-check’ов.
VictoriaLogs хранит логи Supabase.
Vector собирает docker-логи, нормализует их через VRL и отправляет на отдельный сервер.
Nginx принимает ingestion по HTTPS и ограничивает доступ по IP.
Grafana объединяет метрики и логи в одном интерфейсе.
Главный вывод: Edge Function с HTTP-адаптером хорошо подходит, чтобы быстро проверить ingestion и формат сообщений. Но если инфраструктура уже Docker-ориентированная, а Vector входит в стек Supabase, удобнее оставить ему роль единого агента сбора и нормализации: меньше компонентов, меньше дополнительной логики и понятнее путь каждой записи от контейнера до Grafana.
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | Своя self-hosted двухсерверная VPN-архитектура. Подробный путь разработки и ошибки, с которыми вы можете столкнуться | 7 | 8 | 13-07-2026 |
| 2 | Памятник kubelet, Kubernetes != CRI | 0 | 7 | 13-07-2026 |
| 3 | Gateway API против Ingress: как выбрать реализацию и не пожалеть | 0 | 7 | 09-07-2026 |
| 4 | Инверсия приоритетов в Kubernetes: Когда планировщик убивает бизнес, спасая «важные» поды | 0 | 7 | 13-07-2026 |
| 5 | Организовал весь пентест-арсенал в одном месте: всё под рукой, офлайн и на русском | 5 | 7 | 28-06-2026 |
| 6 | Инвентаризируем контейнеры с помощью Wazuh-агента | 0 | 5 | 26-06-2026 |
| 7 | ContentCombine: как я сделал мультинишевый контент-комбайн и запустил ежедневный SEO-дайджест | 5 | 7 | 28-06-2026 |
| 8 | 6 pro-фишек FluxCD. Выжимаем все соки из GitOps | 2 | 6 | 06-07-2026 |
| 9 | Как мы научили реляционую базу хранить оргструктуру в виде графа на 500к пользователей | 0 | 8 | 23-06-2026 |
| 10 | ставим 6 прoкси в 2 клика за 5 минут на 1 VPS | 5 | 7 | 03-07-2026 |