Compare commits

..

3 Commits

Author SHA1 Message Date
root 1c19bac8bc docs(deploy): document pgbouncer rollout
Deployment Config CI / Deployment Config (push) Successful in 24s
Backend CI / Backend (push) Successful in 14m49s
2026-05-03 15:04:17 +08:00
root cec7fa25e3 fix(config): harden database pool settings 2026-05-03 15:04:08 +08:00
root 774b907da8 feat(deploy): add pgbouncer poolers 2026-05-03 15:03:59 +08:00
23 changed files with 436 additions and 55 deletions
+1
View File
@@ -14,6 +14,7 @@ JWT_SECRET=geo-web-Aa123456-change-me-in-production
OPS_JWT_SECRET=geo-ops-Aa123456-change-me-in-production
OPS_DEFAULT_ADMIN_PASSWORD=Aa123456
SCHEDULER_INTERNAL_METRICS_TOKEN=geo-scheduler-Aa123456-change-me-in-production
POSTGRES_PASSWORD=geo_dev
# ─── Optional: MinIO credentials ─────────────────────────────────────────────
MINIO_ROOT_USER=admin
+2
View File
@@ -20,6 +20,8 @@ bash deploy.sh
docker compose -f docker-compose.yaml -f docker-compose.offline.yaml --env-file .env up -d
```
Runtime services connect to Postgres through PgBouncer in `session` pooling mode. The one-shot `migrate` job still connects directly to `postgres` and `monitoring-postgres` so DDL and seed work do not go through the pooler.
On a k3s host:
```bash
+8 -8
View File
@@ -3,24 +3,24 @@ server:
mode: release
database:
host: postgres
host: pgbouncer
port: 5432
user: geo
password: geo_dev
password: ${POSTGRES_PASSWORD:geo_dev}
dbname: geo
sslmode: disable
max_open_conns: 25
max_idle_conns: 5
max_open_conns: 12
max_idle_conns: 3
monitoring_database:
host: monitoring-postgres
host: monitoring-pgbouncer
port: 5432
user: geo
password: geo_dev
password: ${POSTGRES_PASSWORD:geo_dev}
dbname: geo_monitoring
sslmode: disable
max_open_conns: 25
max_idle_conns: 5
max_open_conns: 8
max_idle_conns: 2
rabbitmq:
url: amqp://geo:geo_dev@rabbitmq:5672/geo
+6
View File
@@ -54,6 +54,12 @@ services:
monitoring-postgres:
pull_policy: never
pgbouncer:
pull_policy: never
monitoring-pgbouncer:
pull_policy: never
rabbitmq:
pull_policy: never
+79 -9
View File
@@ -11,6 +11,7 @@ name: geo-rankly
x-app-env: &app-env
CONFIG_PATH: /app/configs/config.yaml
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-geo_dev}
JWT_SECRET: ${JWT_SECRET:-geo-Aa123456-change-me-in-production}
JWT_ACCESS_TTL: ${JWT_ACCESS_TTL:-15m}
JWT_REFRESH_TTL: ${JWT_REFRESH_TTL:-720h}
@@ -23,9 +24,9 @@ x-app-volumes: &app-volumes
- ./prompts.yml:/app/configs/prompts.yml:ro
x-app-depends: &app-depends
postgres:
pgbouncer:
condition: service_healthy
monitoring-postgres:
monitoring-pgbouncer:
condition: service_healthy
rabbitmq:
condition: service_healthy
@@ -49,7 +50,7 @@ services:
environment:
POSTGRES_DB: geo
POSTGRES_USER: geo
POSTGRES_PASSWORD: geo_dev
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-geo_dev}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
@@ -65,7 +66,7 @@ services:
environment:
POSTGRES_DB: geo_monitoring
POSTGRES_USER: geo
POSTGRES_PASSWORD: geo_dev
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-geo_dev}
volumes:
- monitoring_pgdata:/var/lib/postgresql/data
healthcheck:
@@ -74,6 +75,72 @@ services:
timeout: 5s
retries: 10
pgbouncer:
image: edoburu/pgbouncer:v1.24.1-p1
container_name: geo-pgbouncer
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
DB_HOST: postgres
DB_PORT: 5432
DB_USER: geo
DB_PASSWORD: ${POSTGRES_PASSWORD:-geo_dev}
DB_NAME: geo
AUTH_TYPE: scram-sha-256
POOL_MODE: session
MAX_CLIENT_CONN: 200
DEFAULT_POOL_SIZE: 30
MIN_POOL_SIZE: 2
RESERVE_POOL_SIZE: 5
RESERVE_POOL_TIMEOUT: 5
SERVER_IDLE_TIMEOUT: 60
SERVER_LIFETIME: 1800
SERVER_CONNECT_TIMEOUT: 15
CLIENT_LOGIN_TIMEOUT: 15
IGNORE_STARTUP_PARAMETERS: extra_float_digits
ADMIN_USERS: geo
STATS_USERS: geo
healthcheck:
test: ["CMD-SHELL", "PGPASSWORD=\"$${DB_PASSWORD}\" psql -h 127.0.0.1 -p 5432 -U geo -d geo -c 'SELECT 1' >/dev/null"]
interval: 5s
timeout: 5s
retries: 10
monitoring-pgbouncer:
image: edoburu/pgbouncer:v1.24.1-p1
container_name: geo-monitoring-pgbouncer
restart: unless-stopped
depends_on:
monitoring-postgres:
condition: service_healthy
environment:
DB_HOST: monitoring-postgres
DB_PORT: 5432
DB_USER: geo
DB_PASSWORD: ${POSTGRES_PASSWORD:-geo_dev}
DB_NAME: geo_monitoring
AUTH_TYPE: scram-sha-256
POOL_MODE: session
MAX_CLIENT_CONN: 200
DEFAULT_POOL_SIZE: 30
MIN_POOL_SIZE: 2
RESERVE_POOL_SIZE: 5
RESERVE_POOL_TIMEOUT: 5
SERVER_IDLE_TIMEOUT: 60
SERVER_LIFETIME: 1800
SERVER_CONNECT_TIMEOUT: 15
CLIENT_LOGIN_TIMEOUT: 15
IGNORE_STARTUP_PARAMETERS: extra_float_digits
ADMIN_USERS: geo
STATS_USERS: geo
healthcheck:
test: ["CMD-SHELL", "PGPASSWORD=\"$${DB_PASSWORD}\" psql -h 127.0.0.1 -p 5432 -U geo -d geo_monitoring -c 'SELECT 1' >/dev/null"]
interval: 5s
timeout: 5s
retries: 10
rabbitmq:
image: rabbitmq:3.13-management-alpine
container_name: geo-rabbitmq
@@ -161,9 +228,10 @@ services:
- ./prompts.yml:/app/configs/prompts.yml:ro
entrypoint: >
/bin/sh -c "
/usr/local/bin/migrate -path /migrations -database 'postgres://geo:geo_dev@postgres:5432/geo?sslmode=disable' up &&
/usr/local/bin/migrate -path /migrations_monitoring -database 'postgres://geo:geo_dev@monitoring-postgres:5432/geo_monitoring?sslmode=disable' up &&
/usr/local/bin/migrate -path /migrations_ops -database 'postgres://geo:geo_dev@postgres:5432/geo?sslmode=disable&x-migrations-table=schema_migrations_ops' up &&
export PGPASSWORD='$${POSTGRES_PASSWORD:-geo_dev}' &&
/usr/local/bin/migrate -path /migrations -database 'postgres://geo@postgres:5432/geo?sslmode=disable' up &&
/usr/local/bin/migrate -path /migrations_monitoring -database 'postgres://geo@monitoring-postgres:5432/geo_monitoring?sslmode=disable' up &&
/usr/local/bin/migrate -path /migrations_ops -database 'postgres://geo@postgres:5432/geo?sslmode=disable&x-migrations-table=schema_migrations_ops' up &&
/usr/local/bin/seed-platform-templates
"
restart: "no"
@@ -250,6 +318,8 @@ services:
restart: unless-stopped
environment:
CONFIG_PATH: /app/configs/ops-config.yaml
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-geo_dev}
OPS_POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-geo_dev}
OPS_JWT_SECRET: ${OPS_JWT_SECRET:-change-me-in-production}
OPS_DEFAULT_ADMIN_USERNAME: ${OPS_DEFAULT_ADMIN_USERNAME:-admin}
OPS_DEFAULT_ADMIN_PASSWORD: ${OPS_DEFAULT_ADMIN_PASSWORD:-}
@@ -258,9 +328,9 @@ services:
volumes:
- ./ops-config.yaml:/app/configs/ops-config.yaml:ro
depends_on:
postgres:
pgbouncer:
condition: service_healthy
monitoring-postgres:
monitoring-pgbouncer:
condition: service_healthy
migrate:
condition: service_completed_successfully
+2 -2
View File
@@ -2,7 +2,7 @@
这套清单用于把当前 Docker Compose 拓扑搬到单节点或小集群 k3s 中。默认包含:
- infra: `postgres`, `monitoring-postgres`, `rabbitmq`, `redis`, `qdrant`, `minio`
- infra: `postgres`, `monitoring-postgres`, `pgbouncer`, `monitoring-pgbouncer`, `rabbitmq`, `redis`, `qdrant`, `minio`
- jobs: `migrate`, `minio-init`
- apps: `tenant-api`, `worker-generate`, `kol-assist-worker`, `scheduler`, `ops-api`, `frontend`, `ops-web`
- ingress: `geo-rankly.local` -> `frontend`, `ops.geo-rankly.local` -> `ops-web`
@@ -38,7 +38,7 @@ sudo k3s ctr images import images.tar
- `config/config.yaml` 里的 LLM / SiliconFlow API key
- `config.local.yaml` 里对应的数据库、RabbitMQ、MinIO、JWT 配置
注意:`ops-api` 目前从 `deploy/k3s/config/ops-config.yaml` 读取数据库连接信息;如果改了 Postgres 密码,也同步改这个文件
注意:应用运行时通过 `pgbouncer` / `monitoring-pgbouncer` 访问数据库;`migrate` 任务仍然直连 `postgres` / `monitoring-postgres`。数据库密码统一来自 `POSTGRES_PASSWORD` secret,应用配置和 PgBouncer 都会跟随这个 secret
## 3. 部署
+30
View File
@@ -38,6 +38,11 @@ spec:
env:
- name: CONFIG_PATH
value: /app/configs/config.yaml
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: geo-rankly-app-secret
key: POSTGRES_PASSWORD
- name: TZ
value: Asia/Shanghai
- name: JWT_SECRET
@@ -125,6 +130,11 @@ spec:
env:
- name: CONFIG_PATH
value: /app/configs/config.yaml
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: geo-rankly-app-secret
key: POSTGRES_PASSWORD
- name: TZ
value: Asia/Shanghai
- name: JWT_SECRET
@@ -193,6 +203,11 @@ spec:
env:
- name: CONFIG_PATH
value: /app/configs/config.yaml
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: geo-rankly-app-secret
key: POSTGRES_PASSWORD
- name: TZ
value: Asia/Shanghai
- name: JWT_SECRET
@@ -278,6 +293,11 @@ spec:
env:
- name: CONFIG_PATH
value: /app/configs/config.yaml
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: geo-rankly-app-secret
key: POSTGRES_PASSWORD
- name: TZ
value: Asia/Shanghai
- name: SCHEDULER_HTTP_HOST
@@ -384,6 +404,16 @@ spec:
env:
- name: CONFIG_PATH
value: /app/configs/ops-config.yaml
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: geo-rankly-app-secret
key: POSTGRES_PASSWORD
- name: OPS_POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: geo-rankly-app-secret
key: POSTGRES_PASSWORD
- name: TZ
value: Asia/Shanghai
- name: OPS_JWT_SECRET
+8 -8
View File
@@ -3,24 +3,24 @@ server:
mode: release
database:
host: postgres
host: pgbouncer
port: 5432
user: geo
password: geo_dev
password: ${POSTGRES_PASSWORD:geo_dev}
dbname: geo
sslmode: disable
max_open_conns: 25
max_idle_conns: 5
max_open_conns: 12
max_idle_conns: 3
monitoring_database:
host: monitoring-postgres
host: monitoring-pgbouncer
port: 5432
user: geo
password: geo_dev
password: ${POSTGRES_PASSWORD:geo_dev}
dbname: geo_monitoring
sslmode: disable
max_open_conns: 25
max_idle_conns: 5
max_open_conns: 8
max_idle_conns: 2
rabbitmq:
url: amqp://geo:geo_dev@rabbitmq:5672/geo
+6 -6
View File
@@ -3,23 +3,23 @@ server:
mode: release
database:
host: postgres
host: pgbouncer
port: 5432
user: geo
password: geo_dev
password: ${OPS_POSTGRES_PASSWORD:geo_dev}
dbname: geo
sslmode: disable
max_open_conns: 10
max_open_conns: 5
max_idle_conns: 2
monitoring_database:
host: monitoring-postgres
host: monitoring-pgbouncer
port: 5432
user: geo
password: geo_dev
password: ${OPS_POSTGRES_PASSWORD:geo_dev}
dbname: geo_monitoring
sslmode: disable
max_open_conns: 10
max_open_conns: 5
max_idle_conns: 2
redis:
+203
View File
@@ -0,0 +1,203 @@
apiVersion: v1
kind: Service
metadata:
name: pgbouncer
labels:
app.kubernetes.io/name: pgbouncer
spec:
selector:
app.kubernetes.io/name: pgbouncer
ports:
- name: postgres
port: 5432
targetPort: postgres
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: pgbouncer
labels:
app.kubernetes.io/name: pgbouncer
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: pgbouncer
template:
metadata:
labels:
app.kubernetes.io/name: pgbouncer
spec:
containers:
- name: pgbouncer
image: edoburu/pgbouncer:v1.24.1-p1
imagePullPolicy: IfNotPresent
ports:
- name: postgres
containerPort: 5432
env:
- name: DB_HOST
value: postgres
- name: DB_PORT
value: "5432"
- name: DB_USER
value: geo
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: geo-rankly-app-secret
key: POSTGRES_PASSWORD
- name: DB_NAME
value: geo
- name: AUTH_TYPE
value: scram-sha-256
- name: POOL_MODE
value: session
- name: MAX_CLIENT_CONN
value: "200"
- name: DEFAULT_POOL_SIZE
value: "30"
- name: MIN_POOL_SIZE
value: "2"
- name: RESERVE_POOL_SIZE
value: "5"
- name: RESERVE_POOL_TIMEOUT
value: "5"
- name: SERVER_IDLE_TIMEOUT
value: "60"
- name: SERVER_LIFETIME
value: "1800"
- name: SERVER_CONNECT_TIMEOUT
value: "15"
- name: CLIENT_LOGIN_TIMEOUT
value: "15"
- name: IGNORE_STARTUP_PARAMETERS
value: extra_float_digits
- name: ADMIN_USERS
value: geo
- name: STATS_USERS
value: geo
readinessProbe:
exec:
command:
- /bin/sh
- -ec
- PGPASSWORD="${DB_PASSWORD}" psql -h 127.0.0.1 -p 5432 -U geo -d geo -c 'SELECT 1' >/dev/null
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 5
livenessProbe:
exec:
command: ["pg_isready", "-h", "127.0.0.1", "-p", "5432", "-U", "geo", "-d", "geo"]
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 500m
memory: 256Mi
---
apiVersion: v1
kind: Service
metadata:
name: monitoring-pgbouncer
labels:
app.kubernetes.io/name: monitoring-pgbouncer
spec:
selector:
app.kubernetes.io/name: monitoring-pgbouncer
ports:
- name: postgres
port: 5432
targetPort: postgres
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: monitoring-pgbouncer
labels:
app.kubernetes.io/name: monitoring-pgbouncer
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: monitoring-pgbouncer
template:
metadata:
labels:
app.kubernetes.io/name: monitoring-pgbouncer
spec:
containers:
- name: pgbouncer
image: edoburu/pgbouncer:v1.24.1-p1
imagePullPolicy: IfNotPresent
ports:
- name: postgres
containerPort: 5432
env:
- name: DB_HOST
value: monitoring-postgres
- name: DB_PORT
value: "5432"
- name: DB_USER
value: geo
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: geo-rankly-app-secret
key: POSTGRES_PASSWORD
- name: DB_NAME
value: geo_monitoring
- name: AUTH_TYPE
value: scram-sha-256
- name: POOL_MODE
value: session
- name: MAX_CLIENT_CONN
value: "200"
- name: DEFAULT_POOL_SIZE
value: "30"
- name: MIN_POOL_SIZE
value: "2"
- name: RESERVE_POOL_SIZE
value: "5"
- name: RESERVE_POOL_TIMEOUT
value: "5"
- name: SERVER_IDLE_TIMEOUT
value: "60"
- name: SERVER_LIFETIME
value: "1800"
- name: SERVER_CONNECT_TIMEOUT
value: "15"
- name: CLIENT_LOGIN_TIMEOUT
value: "15"
- name: IGNORE_STARTUP_PARAMETERS
value: extra_float_digits
- name: ADMIN_USERS
value: geo
- name: STATS_USERS
value: geo
readinessProbe:
exec:
command:
- /bin/sh
- -ec
- PGPASSWORD="${DB_PASSWORD}" psql -h 127.0.0.1 -p 5432 -U geo -d geo_monitoring -c 'SELECT 1' >/dev/null
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 5
livenessProbe:
exec:
command: ["pg_isready", "-h", "127.0.0.1", "-p", "5432", "-U", "geo", "-d", "geo_monitoring"]
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 500m
memory: 256Mi
+4 -3
View File
@@ -70,9 +70,10 @@ spec:
- /bin/sh
- -ec
- |
/usr/local/bin/migrate -path /migrations -database "postgres://geo:${POSTGRES_PASSWORD}@postgres:5432/geo?sslmode=disable" up
/usr/local/bin/migrate -path /migrations_monitoring -database "postgres://geo:${POSTGRES_PASSWORD}@monitoring-postgres:5432/geo_monitoring?sslmode=disable" up
/usr/local/bin/migrate -path /migrations_ops -database "postgres://geo:${POSTGRES_PASSWORD}@postgres:5432/geo?sslmode=disable&x-migrations-table=schema_migrations_ops" up
export PGPASSWORD="${POSTGRES_PASSWORD}"
/usr/local/bin/migrate -path /migrations -database "postgres://geo@postgres:5432/geo?sslmode=disable" up
/usr/local/bin/migrate -path /migrations_monitoring -database "postgres://geo@monitoring-postgres:5432/geo_monitoring?sslmode=disable" up
/usr/local/bin/migrate -path /migrations_ops -database "postgres://geo@postgres:5432/geo?sslmode=disable&x-migrations-table=schema_migrations_ops" up
/usr/local/bin/seed-platform-templates
volumes:
- name: app-config
+1
View File
@@ -8,6 +8,7 @@ resources:
- secrets.yaml
- infra-postgres.yaml
- infra-monitoring-postgres.yaml
- infra-pgbouncer.yaml
- infra-rabbitmq.yaml
- infra-redis.yaml
- infra-qdrant-minio.yaml
+2 -2
View File
@@ -14,10 +14,10 @@ stringData:
SCHEDULER_INTERNAL_METRICS_TOKEN: geo-metrics-token
config.local.yaml: |
database:
password: geo_dev
password: ${POSTGRES_PASSWORD:geo_dev}
monitoring_database:
password: geo_dev
password: ${POSTGRES_PASSWORD:geo_dev}
rabbitmq:
url: amqp://geo:geo_dev@rabbitmq:5672/geo
+6 -6
View File
@@ -3,23 +3,23 @@ server:
mode: release
database:
host: postgres
host: pgbouncer
port: 5432
user: geo
password: geo_dev
password: ${OPS_POSTGRES_PASSWORD:geo_dev}
dbname: geo
sslmode: disable
max_open_conns: 10
max_open_conns: 5
max_idle_conns: 2
monitoring_database:
host: monitoring-postgres
host: monitoring-pgbouncer
port: 5432
user: geo
password: geo_dev
password: ${OPS_POSTGRES_PASSWORD:geo_dev}
dbname: geo_monitoring
sslmode: disable
max_open_conns: 10
max_open_conns: 5
max_idle_conns: 2
redis:
+2
View File
@@ -108,6 +108,7 @@ fi
echo "==> [2/5] 拉取基础设施镜像 (platform: ${PLATFORM})"
INFRA_IMAGES=(
"postgres:16-alpine"
"edoburu/pgbouncer:v1.24.1-p1"
"rabbitmq:3.13-management-alpine"
"redis:7-alpine"
"qdrant/qdrant:v1.13.2"
@@ -134,6 +135,7 @@ ALL_IMAGES=(
"${LOCAL_PREFIX}/frontend:${IMAGE_TAG}"
"${LOCAL_PREFIX}/ops-web:${IMAGE_TAG}"
"postgres:16-alpine"
"edoburu/pgbouncer:v1.24.1-p1"
"rabbitmq:3.13-management-alpine"
"redis:7-alpine"
"qdrant/qdrant:v1.13.2"
+9
View File
@@ -236,3 +236,12 @@
- Gitea Registry secret name is `REGISTRY_PASSWORD`; Gitea rejects secret names beginning with `GITEA_`.
- CD workflows verify required commit8 image tags in Gitea Registry before pulling or deploying. Missing images are treated as a failed prerequisite, not as a signal to build during CD.
- Offline packaging pulls previously published Gitea Registry images into the package; it does not rebuild business images when `SOURCE_IMAGE_REGISTRY` is set by the workflow.
## Stable PgBouncer Deployment - 2026-05-03
- Before this change the deployment had no PgBouncer/pooler service. Runtime services used `pgxpool` and connected directly to `postgres` / `monitoring-postgres`.
- Stable-first decision: add PgBouncer in `session` pooling mode, not transaction pooling, so current `pgx` behavior and any session-level assumptions remain compatible.
- Runtime services now target `pgbouncer` and `monitoring-pgbouncer`; migration and seed jobs continue to connect directly to the underlying Postgres services.
- Application-side pools are intentionally smaller: tenant runtime config uses 12 main DB connections and 8 monitoring DB connections per process; ops runtime uses 5 for each DB.
- Offline packages must include `edoburu/pgbouncer:v1.24.1-p1` alongside the existing infra images.
- Postgres passwords are passed through environment placeholders instead of hard-coded in deploy configs. The Go DSN builder now URL-escapes credentials so strong passwords containing `@`, `:`, `/`, or `?` remain valid.
- Local smoke validation used the existing `server/docker-compose.yaml` Postgres containers on `server_default`; PgBouncer successfully returned `SELECT 1` through the pooler and the temporary test container was removed.
+17
View File
@@ -706,3 +706,20 @@
- Added commit8 image verification to compose/k3s/offline deployment paths so CD fails if CI has not produced the required image.
- Changed offline package defaults to use commit8 instead of tag names and to pull business images from the Gitea Registry.
- Verification passed: workflow YAML parse, deploy script `bash -n`, and `sh -n` over all workflow run blocks.
## 2026-05-03T03:03:09Z - Stable PgBouncer Deployment
- Confirmed the existing repository topology uses application-side `pgxpool` and direct Postgres service names; no PgBouncer service/config existed before this change.
- Started Phase 40 to add stable PgBouncer deployment with session pooling, app runtime traffic through PgBouncer, and migration jobs remaining direct to Postgres.
- Added `pgbouncer` and `monitoring-pgbouncer` to production Docker Compose and k3s using `edoburu/pgbouncer:v1.24.1-p1`, `POOL_MODE=session`, and `AUTH_TYPE=scram-sha-256`.
- Changed runtime app configs to target PgBouncer while keeping Docker/k3s migration jobs direct to `postgres` / `monitoring-postgres`.
- Lowered application-side Postgres pools: tenant runtime uses 12 main / 8 monitoring connections per process; ops runtime uses 5 / 5.
- Made deployment passwords flow from `POSTGRES_PASSWORD` / `OPS_POSTGRES_PASSWORD` placeholders and changed `DatabaseConfig.DSN()` to URL-escape credentials for stronger production passwords.
- Included PgBouncer in offline packaging and docs.
- Verification passed:
- `docker compose -f deploy/docker-compose.yaml --env-file deploy/.env.example config`
- `kubectl kustomize deploy/k3s` plus local YAML parse
- `bash -n deploy/scripts/package.sh`
- `git diff --check`
- `go test ./internal/shared/config ./internal/shared/repository/postgres ./internal/ops/config`
- Temporary PgBouncer container attached to existing `server_default` network returned `SELECT 1` through PgBouncer, then was removed.
+5 -2
View File
@@ -11,6 +11,9 @@ database:
user: geo
password: geo_dev
dbname: geo
sslmode: disable
max_open_conns: 12
max_idle_conns: 3
monitoring_database:
host: localhost
@@ -19,8 +22,8 @@ monitoring_database:
password: geo_dev
dbname: geo_monitoring
sslmode: disable
max_open_conns: 25
max_idle_conns: 5
max_open_conns: 8
max_idle_conns: 2
rabbitmq:
url: amqp://geo:geo_dev@localhost:5672/geo
+4 -4
View File
@@ -9,8 +9,8 @@ database:
password: geo_dev
dbname: geo
sslmode: disable
max_open_conns: 25
max_idle_conns: 5
max_open_conns: 12
max_idle_conns: 3
monitoring_database:
host: localhost
@@ -19,8 +19,8 @@ monitoring_database:
password: geo_dev
dbname: geo_monitoring
sslmode: disable
max_open_conns: 25
max_idle_conns: 5
max_open_conns: 8
max_idle_conns: 2
rabbitmq:
url: amqp://geo:geo_dev@localhost:5672/geo
+2 -2
View File
@@ -9,7 +9,7 @@ database:
password: geo_dev
dbname: geo
sslmode: disable
max_open_conns: 10
max_open_conns: 5
max_idle_conns: 2
monitoring_database:
@@ -19,7 +19,7 @@ monitoring_database:
password: geo_dev
dbname: geo_monitoring
sslmode: disable
max_open_conns: 10
max_open_conns: 5
max_idle_conns: 2
redis:
+12 -2
View File
@@ -4,6 +4,8 @@ import (
"crypto/tls"
"encoding/json"
"fmt"
"net"
"net/url"
"os"
"strings"
"time"
@@ -53,8 +55,16 @@ type DatabaseConfig struct {
}
func (d DatabaseConfig) DSN() string {
return fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=%s",
d.User, d.Password, d.Host, d.Port, d.DBName, d.SSLMode)
u := url.URL{
Scheme: "postgres",
User: url.UserPassword(d.User, d.Password),
Host: net.JoinHostPort(d.Host, fmt.Sprintf("%d", d.Port)),
Path: "/" + d.DBName,
}
q := u.Query()
q.Set("sslmode", d.SSLMode)
u.RawQuery = q.Encode()
return u.String()
}
type RedisConfig struct {
@@ -27,6 +27,23 @@ llm:
}
}
func TestDatabaseConfigDSNEscapesCredentials(t *testing.T) {
cfg := DatabaseConfig{
Host: "db.internal",
Port: 5432,
User: "geo",
Password: "p@ss:word/with?symbols",
DBName: "geo",
SSLMode: "disable",
}
got := cfg.DSN()
want := "postgres://geo:p%40ss%3Aword%2Fwith%3Fsymbols@db.internal:5432/geo?sslmode=disable"
if got != want {
t.Fatalf("DSN() = %q, want %q", got, want)
}
}
func TestLoadFallsBackToConfigLLMAPIKey(t *testing.T) {
t.Setenv("LLM_API_KEY", "")
+10 -1
View File
@@ -4,7 +4,7 @@
Continue the desktop AI monitoring implementation by first adding a client-side local scheduler with durable queueing and adaptive execution policy, then adding a reusable hidden Playwright CDP execution layer that attaches to Electron Chromium and reuses existing account partitions.
## Current Phase
Phase 37
Phase 40
## Phases
### Phase 1: Progress Verification
@@ -276,6 +276,14 @@ Phase 37
- [x] Run workflow YAML and shell syntax checks
- **Status:** complete
### Phase 40: Stable PgBouncer Deployment
- [x] Add PgBouncer infrastructure to Docker Compose and k3s
- [x] Route runtime services through PgBouncer while keeping migrations direct to Postgres
- [x] Lower application-side connection pools for stability
- [x] Include PgBouncer in offline packaging and deployment docs
- [x] Validate rendered deployment/config files
- **Status:** complete
## Key Questions
1. How should the desktop client defer and locally optimize monitor-task execution without violating one-task-per-platform serialism?
2. What is the smallest durable local cache that allows same-day resume while safely dropping stale next-day monitor tasks?
@@ -343,6 +351,7 @@ Phase 37
| `connectOverCDP()` is not wired yet even though `playwright-core` is installed | 1 | Add a dedicated hidden-browser manager instead of pushing CDP code directly into adapters or the runtime controller |
| `pnpm --filter @geo/desktop-client typecheck` fails in `electron.vite.config.ts` due to a pre-existing Vite 5/8 plugin type mismatch | 1 | Verified this change with `pnpm --filter @geo/desktop-client build` and `pnpm --filter @geo/desktop-client test`; leave config dependency alignment as a separate fix |
| `kubectl apply --dry-run=client` still attempted to contact the local kube-apiserver, which is not running at `127.0.0.1:26443` | 1 | Verified with `kubectl kustomize`, parsed the rendered YAML locally, and recorded that live API validation should run on the target k3s cluster |
| Production compose PgBouncer smoke test could not start bundled Postgres containers because existing `server/docker-compose.yaml` containers already own `geo-postgres` / `geo-monitoring-postgres` names | 1 | Did not stop user/local DB containers; verified PgBouncer with a temporary test container attached to the existing `server_default` network and removed it after `SELECT 1` passed |
## Notes
- Re-check task_plan.md before major implementation decisions.