Поднял 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.
Что проверить после копипасты#
- Если всё ещё используете прямой внешний доступ к Postgres — порт должен быть случайным высоким, а не стандартным 5432/5433.
pg_hba.confсодержитhostssl ... scram-sha-256+host ... reject— plaintext должен отваливаться жёстко.- Сертификаты Caddy примонтированы и доступны пользователю внутри контейнера (
ls -l /etc/ssl/certsвнутри контейнера должен показывать нужные файлы). sslmode=verify-fullс системным CA проходит,sslmode=disable— падает.- После ротации сертификата Caddy не забывай выполнить
SELECT pg_reload_conf();в Postgres (или перезапустить контейнер). - Пароль 32+ символа сгенерирован через
openssl rand -base64 32. - Данные лежат в локальном 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 оставался внутренней деталью.



