Поднял pgvector на Docker для переезда gbrain на отдельный хост. Сначала сделал внешний доступ к Postgres с обязательным TLS и без IP-allowlist, но это оказалось промежуточной архитектурой.

Что это значит простыми словами#

GBrain нужен как база знаний и память для AI-агентов, но это не повод открывать PostgreSQL в интернет. База должна оставаться внутренним слоем, а наружу лучше отдавать узкий API с понятными правами.

Такой подход полезен и для одного инженера, и для команды: данные остаются в контролируемом месте, агент получает нужный контекст, а инфраструктура не расширяет публичную поверхность без необходимости.

Финальный вариант проще и безопаснее: наружу смотрит не база, а HTTP MCP URL gbrain через Caddy. Postgres снова закрыт от интернета и доступен только локально сервису gbrain.

Зачем это понадобилось#

Готовил миграцию gbrain с текущего сервера на новый. В первой версии казалось, что нескольким клиентам (Hermes, Cursor и т. д.) нужен прямой внешний доступ к одной Postgres-базе.

Требования получились жёсткими:

  • pgvector/pgvector:pg16 в Docker
  • Внешний доступ к базе как временный вариант
  • Нестандартный высокий порт вместо стандартного 5432/5433
  • Обязательный TLS + проверка, что клиент подключился именно к нужному серверу
  • Переиспользовать существующие сертификаты Caddy (автопродление уже работает)

Потом появилась более правильная точка входа: gbrain serve --http как MCP-сервер. После этого необходимость открывать Postgres наружу исчезла.

Архитектура#

Промежуточная схема была такой: Caddy получает сертификат для db.your-domain.com, но не проксирует Postgres-трафик (L7 vs TCP). Клиенты подключаются напрямую к высокому случайному порту с TLS.

Схема:

  • Caddy видит новый hostname → получает LE-сертификат
  • Postgres через bind-mount читает этот сертификат (read-only)
  • Клиенты идут на db.your-domain.com:<RANDOM_HIGH_PORT> с sslmode=verify-full
  • Caddy автоматически продлевает сертификат каждые ~90 дней

Проблема была в правах: сертификаты Caddy лежат под root:root 0700. Postgres внутри контейнера работает от непривилегированного пользователя.

Решение — точечный chmod 755 + setfacl с default ACL на директорию конкретного поддомена. Новые сертификаты после ротации автоматически получают правильные права.

Но это не финальная архитектура. После подъёма gbrain по HTTP наружу стала нужна только MCP-точка входа:

https://gbrain.your-domain.com/mcp

А Postgres снова можно держать за закрытой дверью: bind на 127.0.0.1, порт в firewall закрыт, внешнее подключение к базе больше не нужно.

Настройка по шагам#

1. Firewall#

ufw status numbered

У меня уже стоял default deny incoming. В промежуточной версии открыл только случайный высокий порт, а не стандартный 5432/5433:

ufw allow <RANDOM_HIGH_PORT>/tcp comment "pgvector for gbrain"
ufw reload

После перехода на gbrain MCP URL это правило убрал: база больше не должна принимать внешние соединения.

2. Строгий пароль и конфиги#

В .env:

POSTGRES_PASSWORD=$(openssl rand -base64 32)
PGVECTOR_HOST_PORT=<RANDOM_HIGH_PORT>

Создал config/pg_hba.conf (обязательный TLS):

hostssl all all 0.0.0.0/0 scram-sha-256
host    all all 0.0.0.0/0 reject

И config/postgresql.conf:

ssl = on
ssl_cert_file = '/etc/ssl/certs/server.crt'
ssl_key_file = '/etc/ssl/private/server.key'
password_encryption = scram-sha-256

3. Caddy + права на сертификаты#

Добавил в Caddyfile:

db.your-domain.com {
    respond 404
}

После того как Caddy получил сертификат:

# Точечно ослабляем права только на нужную директорию
chmod 755 /path/to/caddy/data/certificates/acme-v02.api.letsencrypt.org-directory/db.your-domain.com

groupadd -g 70 postgres 2>/dev/null || true
setfacl -m g:postgres:rx /path/to/caddy/data/certificates/acme-v02.api.letsencrypt.org-directory/db.your-domain.com
setfacl -d -m g:postgres:rx /path/to/caddy/data/certificates/acme-v02.api.letsencrypt.org-directory/db.your-domain.com

4. docker-compose.yml#

version: "3.8"
services:
  pgvector:
    image: pgvector/pgvector:pg16
    restart: unless-stopped
    environment:
      - POSTGRES_DB=gbrain
      - POSTGRES_USER=gbrain
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
    ports:
      - "0.0.0.0:${PGVECTOR_HOST_PORT}:5432"
    volumes:
      - ./data/postgres:/var/lib/postgresql/data
      - ./config/postgresql.conf:/etc/postgresql/postgresql.conf:ro
      - ./config/pg_hba.conf:/etc/postgresql/pg_hba.conf:ro
      - /path/to/caddy/data/certificates/acme-v02.api.letsencrypt.org-directory/db.your-domain.com:/etc/ssl/certs:ro
    command: postgres -c config_file=/etc/postgresql/postgresql.conf
    user: "70:70"
# named volumes не использую: данные лежат рядом со стеком в ./data/postgres

Обратите внимание на два момента. Сертификаты монтируются директорией, а не отдельными файлами — так Caddy сможет обновлять их, а Postgres будет видеть актуальные. Данные Postgres лежат в локальном bind mount ./data/postgres, а не в named volume: путь видно глазами, его проще бэкапить обычным rsync/tar, и не нужно выкапывать состояние из /var/lib/docker/volumes/.... По той же причине я делал bind mount в статье про Firecrawl.

5. Проверка#

Локально:

psql "host=localhost port=<RANDOM_HIGH_PORT> dbname=gbrain user=gbrain sslmode=require"

Внешне (с другого хоста):

# Должно работать
psql "host=db.your-domain.com port=<RANDOM_HIGH_PORT> dbname=gbrain user=gbrain sslmode=verify-full sslrootcert=/etc/ssl/certs/ca-certificates.crt"

# Должно отклоняться
psql "host=db.your-domain.com port=<RANDOM_HIGH_PORT> dbname=gbrain user=gbrain sslmode=disable"

CREATE EXTENSION vector; — получил версию 0.8.4.

Что проверить после копипасты#

  1. Если всё ещё используете прямой внешний доступ к Postgres — порт должен быть случайным высоким, а не стандартным 5432/5433.
  2. pg_hba.conf содержит hostssl ... scram-sha-256 + host ... reject — plaintext должен отваливаться жёстко.
  3. Сертификаты Caddy примонтированы и доступны пользователю внутри контейнера (ls -l /etc/ssl/certs внутри контейнера должен показывать нужные файлы).
  4. sslmode=verify-full с системным CA проходит, sslmode=disable — падает.
  5. После ротации сертификата Caddy не забывай выполнить SELECT pg_reload_conf(); в Postgres (или перезапустить контейнер).
  6. Пароль 32+ символа сгенерирован через openssl rand -base64 32.
  7. Данные лежат в локальном bind mount (./data/postgres), чтобы бэкапы были обычной файловой операцией, а не раскопками named volume.

Осознанные компромиссы#

  • Без allowlist — любой IP может стучаться. Защита только на пароле и TLS-валидации. Сделал сознательно, чтобы не мучаться с синхронизацией списков при переезде клиентов.
  • Ручной reload после ротации сертификата — Caddy обновляет файлы автоматически, но Postgres нужно сказать pg_reload_conf(). Риск минимальный (сертификат живёт 90 дней), но это точка, за которой надо следить.
  • Без fail2ban — не потому что это красивая модель безопасности, а потому что я просто не захотел городить ещё один сервис. Потом необходимость отпала: после перехода на gbrain MCP URL внешний Postgres-порт закрыт.

Финальная мутация: gbrain MCP URL вместо открытой базы#

Когда поднял gbrain как отдельный HTTP MCP-сервис, схема стала нормальнее:

gbrain serve --http --port 3131 --bind 127.0.0.1

Снаружи доступен только адрес через Caddy:

https://gbrain.your-domain.com/mcp

Hermes, Cursor и другие клиенты подключаются к нему по HTTPS с bearer-токеном. Postgres остаётся внутренней базой gbrain, а не публичным сервисом.

После этого внешний Postgres-порт закрыл:

ufw delete allow <RANDOM_HIGH_PORT>/tcp

И вернул bind в compose на loopback:

ports:
  - "127.0.0.1:${PGVECTOR_HOST_PORT}:5432"

То есть статья начиналась как инструкция про безопасный внешний Postgres, но итог получился другим: если у gbrain есть HTTP MCP-адрес, прямой доступ к базе наружу не нужен.

Что получилось#

Во временной схеме я проверил, что внешний Postgres работает как задумано:

  • pgvector работает, extension 0.8.4 установлен;
  • TLS обязателен, клиент проверяет сервер через настоящий LE-сертификат;
  • plaintext-подключения отклоняются на уровне pg_hba.conf;
  • нестандартный высокий порт работает и не конфликтует со стандартными Postgres-портами.

Но в финальной схеме это всё осталось внутренней деталью. Снаружи теперь живёт gbrain MCP URL, а не Postgres. Можно подключать несколько клиентов — Hermes, Cursor и другие — к одной общей базе знаний через MCP, не открывая БД наружу.

Следующий шаг — настроить регулярные бэкапы ./data/postgres. А доступ к базе снаружи больше не нужен: внешний слой теперь gbrain MCP, а не Postgres.

Если Caddy уже стоит, это самый чистый способ дать gbrain внешний MCP-адрес и при этом не светить Postgres наружу.


Если вам нужна база знаний для AI-агентов, команды или личной инфраструктуры, не обязательно выставлять наружу всю базу данных. Напишите мне в Telegram или на почту — помогу спроектировать gbrain/MCP-контур так, чтобы знания были доступны агентам, а Postgres оставался внутренней деталью.