From 579c91bce781d8eee06be05f28f9e0ab3469384f Mon Sep 17 00:00:00 2001 From: Full-Stack Engineer Date: Sun, 15 Mar 2026 18:14:29 +0000 Subject: [PATCH 01/30] Add production Dockerfile and docker-compose app service for self-hosted deployment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Multi-stage Dockerfile (deps → build → runner) using oven/bun:1 with standalone Next.js output - App service in docker-compose.yml with postgres healthcheck dependency - .env.docker template with Docker networking defaults (postgres:5432, redis-rest:80) - docker-entrypoint.sh runs prisma migrate deploy on startup - Enable Next.js standalone output in next.config.ts Co-Authored-By: Paperclip --- .dockerignore | 5 ++++ .gitignore | 1 + Dockerfile | 55 +++++++++++++++++++++++++++++++++++++++++ apps/web/.env.docker | 39 +++++++++++++++++++++++++++++ apps/web/next.config.ts | 1 + docker-compose.yml | 20 +++++++++++++++ docker-entrypoint.sh | 11 +++++++++ 7 files changed, 132 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 apps/web/.env.docker create mode 100644 docker-entrypoint.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..270924f2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +node_modules +.next +.git +*.md +!README.md diff --git a/.gitignore b/.gitignore index 44b9d545..4f0dc6b4 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ yarn-error.log* # env files .env* !.env.example +!.env.docker # vercel .vercel diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..2476ab52 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,55 @@ +# ── Stage 1: Install dependencies ───────────────────────────────── +FROM oven/bun:1 AS deps + +WORKDIR /app + +COPY package.json bun.lock bunfig.toml ./ +COPY apps/web/package.json apps/web/ + +RUN bun install --frozen-lockfile + +# ── Stage 2: Build the Next.js app ─────────────────────────────── +FROM oven/bun:1 AS builder + +WORKDIR /app + +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Generate Prisma client and build +WORKDIR /app/apps/web +RUN bunx prisma generate +RUN bun run build + +WORKDIR /app + +# ── Stage 3: Production runner ─────────────────────────────────── +FROM oven/bun:1-slim AS runner + +WORKDIR /app + +ENV NODE_ENV=production +ENV HOSTNAME="0.0.0.0" +ENV PORT=3000 + +# Copy standalone Next.js output (includes server + minimal node_modules) +COPY --from=builder /app/apps/web/.next/standalone ./ +COPY --from=builder /app/apps/web/.next/static ./apps/web/.next/static +COPY --from=builder /app/apps/web/public ./apps/web/public + +# Copy Prisma schema + migrations for runtime migrate +COPY --from=builder /app/apps/web/prisma ./apps/web/prisma + +# Copy generated Prisma client +COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma +COPY --from=builder /app/node_modules/@prisma ./node_modules/@prisma +COPY --from=builder /app/node_modules/prisma ./node_modules/prisma + +# Copy entrypoint script +COPY docker-entrypoint.sh /app/docker-entrypoint.sh +RUN chmod +x /app/docker-entrypoint.sh + +EXPOSE 3000 + +ENTRYPOINT ["/app/docker-entrypoint.sh"] +CMD ["bun", "apps/web/server.js"] diff --git a/apps/web/.env.docker b/apps/web/.env.docker new file mode 100644 index 00000000..8f39016d --- /dev/null +++ b/apps/web/.env.docker @@ -0,0 +1,39 @@ +# ────────────────────────────────────────────── +# Docker Compose environment template +# ────────────────────────────────────────────── +# Copy this file to apps/web/.env before running docker compose up: +# cp apps/web/.env.docker apps/web/.env +# Then fill in the placeholder values below. + +# ────────────────────────────────────────────── +# Database (pre-configured for Docker networking) +# ────────────────────────────────────────────── +DATABASE_URL=postgresql://postgres:postgres@postgres:5432/better_hub + +# ────────────────────────────────────────────── +# Redis (pre-configured for Docker networking) +# ────────────────────────────────────────────── +UPSTASH_REDIS_REST_URL=http://redis-rest:80 +UPSTASH_REDIS_REST_TOKEN=local_token + +# ────────────────────────────────────────────── +# Authentication (required — fill these in) +# ────────────────────────────────────────────── +# Create a GitHub OAuth App: https://github.com/settings/developers +# Set the callback URL to http://localhost:3000/api/auth/callback/github +GITHUB_CLIENT_ID=your_github_oauth_client_id +GITHUB_CLIENT_SECRET=your_github_oauth_client_secret + +# Random 32-char string for session encryption +# Generate with: openssl rand -hex 16 +BETTER_AUTH_SECRET=change_me_to_a_random_secret + +# Base URL of the app +BETTER_AUTH_URL=http://localhost:3000 +NEXT_PUBLIC_APP_URL=http://localhost:3000 + +# ────────────────────────────────────────────── +# AI — Ghost assistant (optional) +# ────────────────────────────────────────────── +# OPEN_ROUTER_API_KEY=your_open_router_api_key +# ANTHROPIC_API_KEY=your_anthropic_api_key diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts index ded790ca..e39d1d72 100644 --- a/apps/web/next.config.ts +++ b/apps/web/next.config.ts @@ -26,6 +26,7 @@ const KNOWN_ROUTES = [ ]; const nextConfig: NextConfig = { + output: "standalone", devIndicators: false, serverExternalPackages: ["@prisma/client"], experimental: { diff --git a/docker-compose.yml b/docker-compose.yml index c20b4bd0..7e8f7c05 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,19 @@ services: + app: + build: + context: . + dockerfile: Dockerfile + container_name: better-hub-app + restart: unless-stopped + ports: + - "3000:3000" + env_file: ./apps/web/.env + depends_on: + postgres: + condition: service_healthy + redis-rest: + condition: service_started + postgres: image: postgres:16-alpine container_name: better-hub-postgres @@ -10,6 +25,11 @@ services: POSTGRES_DB: better_hub ports: - "127.0.0.1:54320:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 volumes: - postgres_data:/var/lib/postgresql/data diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 00000000..7706fa92 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/sh +set -e + +# Run Prisma migrations on startup +echo "Running database migrations..." +cd /app/apps/web +bunx prisma migrate deploy +cd /app + +echo "Starting application..." +exec "$@" From 81a9b9bf8a092debce287bab9071acae17e11753 Mon Sep 17 00:00:00 2001 From: Full-Stack Engineer Date: Sun, 15 Mar 2026 18:27:15 +0000 Subject: [PATCH 02/30] Make Docker setup fork-friendly with docker-compose.override.yml Move app service and postgres healthcheck from docker-compose.yml to docker-compose.override.yml so the base file stays untouched and upstream syncs remain conflict-free. Co-Authored-By: Paperclip --- .dockerignore | 1 + docker-compose.override.yml | 25 +++++++++++++++++++++++++ docker-compose.yml | 20 -------------------- 3 files changed, 26 insertions(+), 20 deletions(-) create mode 100644 docker-compose.override.yml diff --git a/.dockerignore b/.dockerignore index 270924f2..68fce6d8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,3 +3,4 @@ node_modules .git *.md !README.md +docker-compose.override.yml diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 00000000..b4d631a9 --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,25 @@ +# Override file for self-hosted deployment. +# Docker Compose merges this automatically with docker-compose.yml, +# keeping the base file untouched for clean upstream syncs. +services: + app: + build: + context: . + dockerfile: Dockerfile + container_name: better-hub-app + restart: unless-stopped + ports: + - "3000:3000" + env_file: ./apps/web/.env + depends_on: + postgres: + condition: service_healthy + redis-rest: + condition: service_started + + postgres: + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 diff --git a/docker-compose.yml b/docker-compose.yml index 7e8f7c05..c20b4bd0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,19 +1,4 @@ services: - app: - build: - context: . - dockerfile: Dockerfile - container_name: better-hub-app - restart: unless-stopped - ports: - - "3000:3000" - env_file: ./apps/web/.env - depends_on: - postgres: - condition: service_healthy - redis-rest: - condition: service_started - postgres: image: postgres:16-alpine container_name: better-hub-postgres @@ -25,11 +10,6 @@ services: POSTGRES_DB: better_hub ports: - "127.0.0.1:54320:5432" - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 5s - timeout: 5s - retries: 5 volumes: - postgres_data:/var/lib/postgresql/data From efbd55a2144c0077fb9b48d6140baa3735114c55 Mon Sep 17 00:00:00 2001 From: Full-Stack Engineer Date: Sun, 15 Mar 2026 18:41:56 +0000 Subject: [PATCH 03/30] Add deploy.md with self-hosted setup instructions Co-Authored-By: Paperclip --- deploy.md | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 deploy.md diff --git a/deploy.md b/deploy.md new file mode 100644 index 00000000..fb91a0d7 --- /dev/null +++ b/deploy.md @@ -0,0 +1,85 @@ +# Self-Hosted Deployment + +Run Better Hub locally with Docker Compose. + +## Prerequisites + +- [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/) (v2+) +- A [GitHub OAuth App](https://github.com/settings/developers) + +## 1. Create a GitHub OAuth App + +1. Go to **Settings → Developer settings → OAuth Apps → New OAuth App** +2. Set the fields: + - **Application name**: anything (e.g. "Better Hub Local") + - **Homepage URL**: `http://localhost:3000` + - **Authorization callback URL**: `http://localhost:3000/api/auth/callback/github` +3. After creating, copy the **Client ID** and generate a **Client Secret** + +## 2. Configure environment + +```sh +cp apps/web/.env.docker apps/web/.env +``` + +Edit `apps/web/.env` and fill in: + +| Variable | Value | +|---|---| +| `GITHUB_CLIENT_ID` | Your OAuth App Client ID | +| `GITHUB_CLIENT_SECRET` | Your OAuth App Client Secret | +| `BETTER_AUTH_SECRET` | A random string (`openssl rand -hex 16`) | + +The database and Redis URLs are pre-configured for Docker networking — no changes needed. + +## 3. Start the stack + +```sh +docker compose up --build +``` + +This starts: + +- **postgres** — PostgreSQL 16 database +- **redis** + **redis-rest** — Redis with HTTP REST API (Upstash-compatible) +- **app** — Better Hub Next.js application + +On first boot, Prisma migrations run automatically before the app starts. + +## 4. Open the app + +Visit [http://localhost:3000](http://localhost:3000) and sign in with GitHub. + +## Architecture + +The Docker setup uses `docker-compose.override.yml` to add the `app` service on top of the base `docker-compose.yml`. This keeps the base file identical to upstream so you can sync without merge conflicts. + +``` +docker-compose.yml ← upstream (postgres, redis, redis-rest) +docker-compose.override.yml ← self-hosted additions (app service, healthchecks) +Dockerfile ← multi-stage build (deps → build → slim runner) +docker-entrypoint.sh ← runs prisma migrate deploy on boot +apps/web/.env.docker ← env template for Docker networking +``` + +## Rebuilding + +After pulling new changes: + +```sh +docker compose up --build +``` + +Migrations run automatically on each boot, so schema changes are applied without extra steps. + +## Stopping + +```sh +docker compose down +``` + +To also remove the database volume: + +```sh +docker compose down -v +``` From 316489ddbdf8ab7d456e5223835d6ffb1c1aa476 Mon Sep 17 00:00:00 2001 From: Full-Stack Engineer Date: Sun, 15 Mar 2026 20:29:43 +0000 Subject: [PATCH 04/30] Fix Dockerfile: copy Prisma schema into deps stage for postinstall The apps/web package.json has a postinstall script that runs prisma generate. Without the schema files present during bun install, this fails. Copy prisma.config.ts and the prisma directory into the deps stage so the postinstall completes successfully. Co-Authored-By: Paperclip --- Dockerfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2476ab52..7d45642a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,8 @@ WORKDIR /app COPY package.json bun.lock bunfig.toml ./ COPY apps/web/package.json apps/web/ +COPY apps/web/prisma.config.ts apps/web/ +COPY apps/web/prisma apps/web/prisma RUN bun install --frozen-lockfile @@ -14,11 +16,11 @@ FROM oven/bun:1 AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules +COPY --from=deps /app/apps/web/node_modules ./apps/web/node_modules COPY . . -# Generate Prisma client and build +# Build the Next.js app (Prisma client already generated by postinstall in deps stage) WORKDIR /app/apps/web -RUN bunx prisma generate RUN bun run build WORKDIR /app From 47acca461038a9d4c5371112fdc5deb939febe3e Mon Sep 17 00:00:00 2001 From: Full-Stack Engineer Date: Sun, 15 Mar 2026 21:18:13 +0000 Subject: [PATCH 05/30] Fix Dockerfile: remove non-existent apps/web/node_modules copy Bun hoists all dependencies to the root node_modules in workspace mode, so apps/web/node_modules does not exist in the deps stage. The generated Prisma client is already in root node_modules/.prisma. Co-Authored-By: Paperclip --- Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 7d45642a..830c3e0d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,6 @@ FROM oven/bun:1 AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules -COPY --from=deps /app/apps/web/node_modules ./apps/web/node_modules COPY . . # Build the Next.js app (Prisma client already generated by postinstall in deps stage) From bbb40199c7e5a55080a085d280e5ca3570fc0fd2 Mon Sep 17 00:00:00 2001 From: Full-Stack Engineer Date: Mon, 16 Mar 2026 09:29:02 +0000 Subject: [PATCH 06/30] Fix Dockerfile: use correct Prisma generated client path The schema.prisma has output = "../src/generated/prisma" so the generated client lives at apps/web/src/generated/prisma, not node_modules/.prisma. Updated all three stages to use the correct path. Also copy prisma.config.ts to runner for migrate deploy. Co-Authored-By: Paperclip --- Dockerfile | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 830c3e0d..a8d450d1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,6 +18,10 @@ WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . +# Copy generated Prisma client from deps stage (output goes to apps/web/src/generated/prisma +# per schema.prisma output config, and is gitignored so COPY . . does not include it) +COPY --from=deps /app/apps/web/src/generated/prisma ./apps/web/src/generated/prisma + # Build the Next.js app (Prisma client already generated by postinstall in deps stage) WORKDIR /app/apps/web RUN bun run build @@ -38,11 +42,14 @@ COPY --from=builder /app/apps/web/.next/standalone ./ COPY --from=builder /app/apps/web/.next/static ./apps/web/.next/static COPY --from=builder /app/apps/web/public ./apps/web/public -# Copy Prisma schema + migrations for runtime migrate +# Copy Prisma schema, config + migrations for runtime migrate COPY --from=builder /app/apps/web/prisma ./apps/web/prisma +COPY --from=builder /app/apps/web/prisma.config.ts ./apps/web/prisma.config.ts + +# Copy generated Prisma client (custom output: apps/web/src/generated/prisma) +COPY --from=builder /app/apps/web/src/generated/prisma ./apps/web/src/generated/prisma -# Copy generated Prisma client -COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma +# Copy Prisma CLI + engine for runtime migrations (prisma migrate deploy) COPY --from=builder /app/node_modules/@prisma ./node_modules/@prisma COPY --from=builder /app/node_modules/prisma ./node_modules/prisma From 0ef533137162e6f732ce58a64f4549777ba8332d Mon Sep 17 00:00:00 2001 From: Full-Stack Engineer Date: Mon, 16 Mar 2026 09:45:15 +0000 Subject: [PATCH 07/30] Fix Dockerfile: include dotenv for prisma.config.ts in runner The prisma.config.ts imports dotenv/config which wasn't available in the slim runner stage, causing prisma migrate deploy to fail on container startup. Co-Authored-By: Paperclip --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index a8d450d1..296d4818 100644 --- a/Dockerfile +++ b/Dockerfile @@ -52,6 +52,7 @@ COPY --from=builder /app/apps/web/src/generated/prisma ./apps/web/src/generated/ # Copy Prisma CLI + engine for runtime migrations (prisma migrate deploy) COPY --from=builder /app/node_modules/@prisma ./node_modules/@prisma COPY --from=builder /app/node_modules/prisma ./node_modules/prisma +COPY --from=builder /app/node_modules/dotenv ./node_modules/dotenv # Copy entrypoint script COPY docker-entrypoint.sh /app/docker-entrypoint.sh From 8b3f098a7901f4e6091729712882c121e662b822 Mon Sep 17 00:00:00 2001 From: Full-Stack Engineer Date: Mon, 16 Mar 2026 10:06:20 +0000 Subject: [PATCH 08/30] Fix Dockerfile: bypass prisma.config.ts in runner to avoid transitive deps Instead of copying prisma.config.ts and chasing its dependency chain (dotenv, effect, etc.), pass --schema directly to prisma migrate deploy. DATABASE_URL is already in the environment via docker-compose env_file. This removes the need for prisma.config.ts, dotenv, and effect in the slim runner stage. Co-Authored-By: Paperclip --- Dockerfile | 4 +--- docker-entrypoint.sh | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 296d4818..9ba78219 100644 --- a/Dockerfile +++ b/Dockerfile @@ -42,9 +42,8 @@ COPY --from=builder /app/apps/web/.next/standalone ./ COPY --from=builder /app/apps/web/.next/static ./apps/web/.next/static COPY --from=builder /app/apps/web/public ./apps/web/public -# Copy Prisma schema, config + migrations for runtime migrate +# Copy Prisma schema + migrations for runtime migrate deploy COPY --from=builder /app/apps/web/prisma ./apps/web/prisma -COPY --from=builder /app/apps/web/prisma.config.ts ./apps/web/prisma.config.ts # Copy generated Prisma client (custom output: apps/web/src/generated/prisma) COPY --from=builder /app/apps/web/src/generated/prisma ./apps/web/src/generated/prisma @@ -52,7 +51,6 @@ COPY --from=builder /app/apps/web/src/generated/prisma ./apps/web/src/generated/ # Copy Prisma CLI + engine for runtime migrations (prisma migrate deploy) COPY --from=builder /app/node_modules/@prisma ./node_modules/@prisma COPY --from=builder /app/node_modules/prisma ./node_modules/prisma -COPY --from=builder /app/node_modules/dotenv ./node_modules/dotenv # Copy entrypoint script COPY docker-entrypoint.sh /app/docker-entrypoint.sh diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 7706fa92..0e065123 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -2,10 +2,10 @@ set -e # Run Prisma migrations on startup +# Use --schema to bypass prisma.config.ts (avoids needing its transitive deps in the runner) +# DATABASE_URL is provided via env_file in docker-compose echo "Running database migrations..." -cd /app/apps/web -bunx prisma migrate deploy -cd /app +bunx prisma migrate deploy --schema /app/apps/web/prisma/schema.prisma echo "Starting application..." exec "$@" From 743dc7c932c7e5489127c2b716875eb82b9030cf Mon Sep 17 00:00:00 2001 From: Full-Stack Engineer Date: Mon, 16 Mar 2026 10:50:37 +0000 Subject: [PATCH 09/30] Fix Prisma migrate deploy: add DATABASE_URL to schema datasource The datasource block had no url field, relying solely on prisma.config.ts which has heavy transitive deps (dotenv, effect) not available in the Docker runner. Adding url = env("DATABASE_URL") to the schema lets prisma migrate deploy --schema work directly. The prisma.config.ts still takes precedence for local development. Co-Authored-By: Paperclip --- apps/web/prisma/schema.prisma | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/prisma/schema.prisma b/apps/web/prisma/schema.prisma index 44e25f23..66893047 100644 --- a/apps/web/prisma/schema.prisma +++ b/apps/web/prisma/schema.prisma @@ -6,6 +6,7 @@ generator client { datasource db { provider = "postgresql" + url = env("DATABASE_URL") } model User { From faaf1b0698e2543d9ea8b9d04aed939fa2a6343d Mon Sep 17 00:00:00 2001 From: Full-Stack Engineer Date: Mon, 16 Mar 2026 12:24:30 +0000 Subject: [PATCH 10/30] Fix Prisma v7 compat: revert schema url, use Docker-specific config Prisma v7 no longer supports url in the datasource schema block. Reverted that change and instead: - Created prisma.docker.config.ts (no dotenv import, uses process.env directly) - Copy it as prisma.config.ts in the runner stage - Copy effect package (transitive dep of @prisma/config's defineConfig) - Entrypoint runs from /app where the Docker config lives Co-Authored-By: Paperclip --- Dockerfile | 6 +++++- apps/web/prisma/schema.prisma | 1 - docker-entrypoint.sh | 6 +++--- prisma.docker.config.ts | 14 ++++++++++++++ 4 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 prisma.docker.config.ts diff --git a/Dockerfile b/Dockerfile index 9ba78219..84a1d5e3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,9 +48,13 @@ COPY --from=builder /app/apps/web/prisma ./apps/web/prisma # Copy generated Prisma client (custom output: apps/web/src/generated/prisma) COPY --from=builder /app/apps/web/src/generated/prisma ./apps/web/src/generated/prisma -# Copy Prisma CLI + engine for runtime migrations (prisma migrate deploy) +# Copy Prisma CLI + deps for runtime migrations (prisma migrate deploy) COPY --from=builder /app/node_modules/@prisma ./node_modules/@prisma COPY --from=builder /app/node_modules/prisma ./node_modules/prisma +COPY --from=builder /app/node_modules/effect ./node_modules/effect + +# Copy Docker-specific Prisma config (no dotenv — env vars come from docker-compose) +COPY prisma.docker.config.ts /app/prisma.config.ts # Copy entrypoint script COPY docker-entrypoint.sh /app/docker-entrypoint.sh diff --git a/apps/web/prisma/schema.prisma b/apps/web/prisma/schema.prisma index 66893047..44e25f23 100644 --- a/apps/web/prisma/schema.prisma +++ b/apps/web/prisma/schema.prisma @@ -6,7 +6,6 @@ generator client { datasource db { provider = "postgresql" - url = env("DATABASE_URL") } model User { diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 0e065123..7c6424e1 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -2,10 +2,10 @@ set -e # Run Prisma migrations on startup -# Use --schema to bypass prisma.config.ts (avoids needing its transitive deps in the runner) -# DATABASE_URL is provided via env_file in docker-compose +# Uses /app/prisma.config.ts (Docker-specific, no dotenv dependency) echo "Running database migrations..." -bunx prisma migrate deploy --schema /app/apps/web/prisma/schema.prisma +cd /app +bunx prisma migrate deploy echo "Starting application..." exec "$@" diff --git a/prisma.docker.config.ts b/prisma.docker.config.ts new file mode 100644 index 00000000..3e647cfb --- /dev/null +++ b/prisma.docker.config.ts @@ -0,0 +1,14 @@ +// Minimal Prisma config for Docker runtime (prisma migrate deploy). +// Unlike apps/web/prisma.config.ts, this does not import dotenv +// since env vars are already provided by docker-compose env_file. +import { defineConfig } from "prisma/config"; + +export default defineConfig({ + schema: "apps/web/prisma/schema.prisma", + migrations: { + path: "apps/web/prisma/migrations", + }, + datasource: { + url: process.env.DATABASE_URL!, + }, +}); From adbd80589d6fdd63a79a035d87aa75eb060ff5c8 Mon Sep 17 00:00:00 2001 From: Full-Stack Engineer Date: Mon, 16 Mar 2026 13:44:47 +0000 Subject: [PATCH 11/30] Fix Dockerfile: install Prisma CLI with full dep tree in runner Selectively copying individual packages (prisma, effect, fast-check...) is fragile due to deep transitive dependencies. Instead, install prisma fresh in /prisma-cli with bun install to get the complete dependency tree. Separate directory avoids conflicts with standalone Next.js output. Co-Authored-By: Paperclip --- Dockerfile | 12 ++++++------ docker-entrypoint.sh | 3 ++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 84a1d5e3..ef80756f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,13 +48,13 @@ COPY --from=builder /app/apps/web/prisma ./apps/web/prisma # Copy generated Prisma client (custom output: apps/web/src/generated/prisma) COPY --from=builder /app/apps/web/src/generated/prisma ./apps/web/src/generated/prisma -# Copy Prisma CLI + deps for runtime migrations (prisma migrate deploy) -COPY --from=builder /app/node_modules/@prisma ./node_modules/@prisma -COPY --from=builder /app/node_modules/prisma ./node_modules/prisma -COPY --from=builder /app/node_modules/effect ./node_modules/effect - -# Copy Docker-specific Prisma config (no dotenv — env vars come from docker-compose) +# Install Prisma CLI with full dependency tree for runtime migrations. +# Installed to /prisma-cli to avoid conflicts with standalone Next.js output. COPY prisma.docker.config.ts /app/prisma.config.ts +RUN mkdir /prisma-cli && cd /prisma-cli \ + && echo '{"dependencies":{"prisma":"^7.4.1"}}' > package.json \ + && bun install \ + && rm -f bun.lock # Copy entrypoint script COPY docker-entrypoint.sh /app/docker-entrypoint.sh diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 7c6424e1..854e34ba 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -3,9 +3,10 @@ set -e # Run Prisma migrations on startup # Uses /app/prisma.config.ts (Docker-specific, no dotenv dependency) +# Prisma CLI installed at /prisma-cli to avoid conflicts with standalone output echo "Running database migrations..." cd /app -bunx prisma migrate deploy +/prisma-cli/node_modules/.bin/prisma migrate deploy echo "Starting application..." exec "$@" From 09a5cd557010da5a1178df2b2793ae4ac3ae6cc5 Mon Sep 17 00:00:00 2001 From: Full-Stack Engineer Date: Mon, 16 Mar 2026 14:17:19 +0000 Subject: [PATCH 12/30] Fix entrypoint: set NODE_PATH so prisma.config.ts resolves imports The config at /app/prisma.config.ts imports from "prisma/config" but prisma is installed at /prisma-cli/node_modules/. Setting NODE_PATH tells the runtime to also look in /prisma-cli/node_modules/ when resolving imports from the config file. Co-Authored-By: Paperclip --- docker-entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 854e34ba..9c941cc6 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -6,7 +6,7 @@ set -e # Prisma CLI installed at /prisma-cli to avoid conflicts with standalone output echo "Running database migrations..." cd /app -/prisma-cli/node_modules/.bin/prisma migrate deploy +NODE_PATH=/prisma-cli/node_modules /prisma-cli/node_modules/.bin/prisma migrate deploy echo "Starting application..." exec "$@" From 2f96559476e14985f66435a852b5508ae6dbe1d1 Mon Sep 17 00:00:00 2001 From: Full-Stack Engineer Date: Mon, 16 Mar 2026 14:40:20 +0000 Subject: [PATCH 13/30] Switch entrypoint from prisma migrate deploy to prisma db push MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The project's migrations are incomplete — many tables and columns in schema.prisma have no corresponding migration files (e.g. subscription, usage_logs, ai_call_logs, credit_ledger, etc.). Using prisma db push syncs the database to match the full schema without requiring migration files. Co-Authored-By: Paperclip --- docker-entrypoint.sh | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 9c941cc6..74fcc98f 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -1,12 +1,13 @@ #!/bin/sh set -e -# Run Prisma migrations on startup -# Uses /app/prisma.config.ts (Docker-specific, no dotenv dependency) -# Prisma CLI installed at /prisma-cli to avoid conflicts with standalone output -echo "Running database migrations..." +# Sync database schema on startup. +# Uses prisma db push instead of migrate deploy because the project's +# migrations are incomplete — many tables/columns only exist in schema.prisma. +# --skip-generate avoids regenerating the Prisma client at runtime. +echo "Syncing database schema..." cd /app -NODE_PATH=/prisma-cli/node_modules /prisma-cli/node_modules/.bin/prisma migrate deploy +NODE_PATH=/prisma-cli/node_modules /prisma-cli/node_modules/.bin/prisma db push --skip-generate --accept-data-loss echo "Starting application..." exec "$@" From b585d850aa13c4eb3c17731c3d8d4bd55ab2fcf4 Mon Sep 17 00:00:00 2001 From: Full-Stack Engineer Date: Mon, 16 Mar 2026 14:41:02 +0000 Subject: [PATCH 14/30] Update deploy.md to reflect prisma db push usage Co-Authored-By: Paperclip --- deploy.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deploy.md b/deploy.md index fb91a0d7..179a6d49 100644 --- a/deploy.md +++ b/deploy.md @@ -44,7 +44,7 @@ This starts: - **redis** + **redis-rest** — Redis with HTTP REST API (Upstash-compatible) - **app** — Better Hub Next.js application -On first boot, Prisma migrations run automatically before the app starts. +On first boot, `prisma db push` syncs the database schema before the app starts. ## 4. Open the app @@ -58,7 +58,7 @@ The Docker setup uses `docker-compose.override.yml` to add the `app` service on docker-compose.yml ← upstream (postgres, redis, redis-rest) docker-compose.override.yml ← self-hosted additions (app service, healthchecks) Dockerfile ← multi-stage build (deps → build → slim runner) -docker-entrypoint.sh ← runs prisma migrate deploy on boot +docker-entrypoint.sh ← runs prisma db push on boot apps/web/.env.docker ← env template for Docker networking ``` @@ -70,7 +70,7 @@ After pulling new changes: docker compose up --build ``` -Migrations run automatically on each boot, so schema changes are applied without extra steps. +The database schema is synced automatically on each boot via `prisma db push`, so schema changes are applied without extra steps. ## Stopping From 3bdf2e0e21433fe6dd7b6ed67c7a2ed06b847fa8 Mon Sep 17 00:00:00 2001 From: Full-Stack Engineer Date: Mon, 16 Mar 2026 15:31:50 +0000 Subject: [PATCH 15/30] Remove --skip-generate flag unsupported in Prisma v7 Prisma v7 removed the --skip-generate option from db push. Co-Authored-By: Paperclip --- docker-entrypoint.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 74fcc98f..3fbd9f09 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -4,10 +4,9 @@ set -e # Sync database schema on startup. # Uses prisma db push instead of migrate deploy because the project's # migrations are incomplete — many tables/columns only exist in schema.prisma. -# --skip-generate avoids regenerating the Prisma client at runtime. echo "Syncing database schema..." cd /app -NODE_PATH=/prisma-cli/node_modules /prisma-cli/node_modules/.bin/prisma db push --skip-generate --accept-data-loss +NODE_PATH=/prisma-cli/node_modules /prisma-cli/node_modules/.bin/prisma db push --accept-data-loss echo "Starting application..." exec "$@" From fa54b525d7f9ffe0746ba389b089dc4ce4fa0970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20K=C3=B6nig?= Date: Tue, 31 Mar 2026 10:21:56 +0000 Subject: [PATCH 16/30] Add PgBouncer for database connection pooling and optimize performance - Added pgbouncer service in docker-compose.override.yml for connection pooling - Updated app to connect to pgbouncer instead of postgres directly - Reduced application-side connection pool size since pgbouncer handles pooling - Added resource limits to docker-compose.yml to prevent container starvation - Updated .env.docker to point to pgbouncer service --- apps/web/.env.docker | 2 +- apps/web/src/lib/db.ts | 16 ++++------------ docker-compose.override.yml | 26 +++++++++++++++++++++++++- docker-compose.yml | 26 ++++++++++++++++++++++++-- 4 files changed, 54 insertions(+), 16 deletions(-) diff --git a/apps/web/.env.docker b/apps/web/.env.docker index 8f39016d..1d7d48f9 100644 --- a/apps/web/.env.docker +++ b/apps/web/.env.docker @@ -8,7 +8,7 @@ # ────────────────────────────────────────────── # Database (pre-configured for Docker networking) # ────────────────────────────────────────────── -DATABASE_URL=postgresql://postgres:postgres@postgres:5432/better_hub +DATABASE_URL=postgresql://postgres:postgres@pgbouncer:5432/better_hub # ────────────────────────────────────────────── # Redis (pre-configured for Docker networking) diff --git a/apps/web/src/lib/db.ts b/apps/web/src/lib/db.ts index 7f909fe9..7982395c 100644 --- a/apps/web/src/lib/db.ts +++ b/apps/web/src/lib/db.ts @@ -16,18 +16,10 @@ function getOrCreatePool(): Pool { const isDev = process.env.NODE_ENV !== "production"; const pool = new Pool({ connectionString: process.env.DATABASE_URL, - // In dev, Next.js spawns 10-15 child processes each with its own pool. - // A single page load (e.g. PR detail) can fire 15+ parallel DB queries - // via concurrent server-component renders. docker-compose.yml sets - // max_connections=300 so each process can safely hold 20 connections. - // In production a managed pooler (PgBouncer / Neon) sits in front of - // PG, so a small pool is fine. - max: isDev ? 20 : 5, - idleTimeoutMillis: isDev ? 10_000 : 30_000, - // Docker Desktop on macOS introduces significant latency for new TCP - // connections (5-15 s observed). In dev, disable the timeout so these - // slow-but-valid connections aren't killed prematurely. In production - // behind a managed pooler, 5 s is more than enough. + // With PgBouncer handling connection pooling, we need fewer connections + // from the application side. Each Next.js instance can maintain a small pool. + max: isDev ? 10 : 3, + idleTimeoutMillis: isDev ? 10_000 : 60_000, connectionTimeoutMillis: isDev ? 0 : 5_000, allowExitOnIdle: true, }); diff --git a/docker-compose.override.yml b/docker-compose.override.yml index b4d631a9..d865d02f 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -12,14 +12,38 @@ services: - "3000:3000" env_file: ./apps/web/.env depends_on: - postgres: + pgbouncer: condition: service_healthy redis-rest: condition: service_started + pgbouncer: + image: edoburu/pgbouncer:latest + container_name: better-hub-pgbouncer + restart: unless-stopped + ports: + - "5433:5432" + environment: + - POSTGRES_HOST=postgres + - POSTGRES_PORT=5432 + - POSTGRES_DATABASE=better_hub + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgres} + - POOL_MODE=transaction + - MAX_CLIENT_CONN=100 + - DEFAULT_POOL_SIZE=20 + - RESERVE_POOL_SIZE=5 + - RESERVE_POOL_TIMEOUT=5 + healthcheck: + test: ["CMD-SHELL", "pg_isready -h localhost -p 5432 -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + postgres: healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 5s retries: 5 + command: postgres -c max_connections=300 diff --git a/docker-compose.yml b/docker-compose.yml index c20b4bd0..f15e266b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,10 +8,16 @@ services: POSTGRES_USER: postgres POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} POSTGRES_DB: better_hub - ports: - - "127.0.0.1:54320:5432" volumes: - postgres_data:/var/lib/postgresql/data + deploy: + resources: + limits: + cpus: "2.0" + memory: 2G + reservations: + cpus: "1.0" + memory: 1G redis: image: redis:7-alpine @@ -19,6 +25,14 @@ services: restart: unless-stopped volumes: - redis_data:/data + deploy: + resources: + limits: + cpus: "0.5" + memory: 512M + reservations: + cpus: "0.25" + memory: 256M redis-rest: image: hiett/serverless-redis-http:latest @@ -32,6 +46,14 @@ services: SRH_CONNECTION_STRING: redis://redis:6379 depends_on: - redis + deploy: + resources: + limits: + cpus: "0.5" + memory: 256M + reservations: + cpus: "0.25" + memory: 128M volumes: postgres_data: From 110c8a413eb03612d4645a55ce2fa0837d648c9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20K=C3=B6nig?= Date: Tue, 31 Mar 2026 10:55:48 +0000 Subject: [PATCH 17/30] Fix PgBouncer configuration: switch to bitnami/pgbouncer image and use correct env vars The previous edoburu/pgbouncer image expected a config file at /etc/pgbouncer/pgbouncer.ini. Switched to bitnami/pgbouncer image which can be configured via environment variables. Updated environment variable names to match Bitnami's expectations: - POSTGRESQL_HOST, POSTGRESQL_PORT, POSTGRESQL_DATABASE_NAME, POSTGRESQL_USERNAME, POSTGRESQL_PASSWORD --- docker-compose.override.yml | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/docker-compose.override.yml b/docker-compose.override.yml index d865d02f..fcda441d 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -18,22 +18,17 @@ services: condition: service_started pgbouncer: - image: edoburu/pgbouncer:latest + image: bitnami/pgbouncer:latest container_name: better-hub-pgbouncer restart: unless-stopped ports: - "5433:5432" environment: - - POSTGRES_HOST=postgres - - POSTGRES_PORT=5432 - - POSTGRES_DATABASE=better_hub - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgres} - - POOL_MODE=transaction - - MAX_CLIENT_CONN=100 - - DEFAULT_POOL_SIZE=20 - - RESERVE_POOL_SIZE=5 - - RESERVE_POOL_TIMEOUT=5 + - POSTGRESQL_HOST=postgres + - POSTGRESQL_PORT=5432 + - POSTGRESQL_DATABASE_NAME=better_hub + - POSTGRESQL_USERNAME=postgres + - POSTGRESQL_PASSWORD=${POSTGRES_PASSWORD:-postgres} healthcheck: test: ["CMD-SHELL", "pg_isready -h localhost -p 5432 -U postgres"] interval: 5s From 9d87995e59d7f7b47158da5c1d0f01a6b6a62a27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20K=C3=B6nig?= Date: Tue, 31 Mar 2026 11:10:13 +0000 Subject: [PATCH 18/30] Fix PgBouncer image: use public.ecr.aws/bitnami/pgbouncer:latest The Docker Hub image 'bitnami/pgbouncer:latest' is not available or not found. Switched to the Amazon ECR public image which is publicly accessible. --- docker-compose.override.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.override.yml b/docker-compose.override.yml index fcda441d..c6e248cd 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -18,7 +18,7 @@ services: condition: service_started pgbouncer: - image: bitnami/pgbouncer:latest + image: public.ecr.aws/bitnami/pgbouncer:latest container_name: better-hub-pgbouncer restart: unless-stopped ports: From ee63a869c119b44b322780eaec9514e8d21e2a84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20K=C3=B6nig?= Date: Tue, 31 Mar 2026 11:17:30 +0000 Subject: [PATCH 19/30] Fix PgBouncer health check: increase interval and retries, add explicit listen port The PgBouncer container was starting correctly (listening on 0.0.0.0:5432) but being marked as unhealthy due to health check timing issues. - Increased health check interval from 5s to 10s - Increased health check retries from 5 to 10 - Added explicit PGBOUNCER_LISTEN_PORT=5432 environment variable for clarity --- docker-compose.override.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docker-compose.override.yml b/docker-compose.override.yml index c6e248cd..366afef7 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -29,11 +29,12 @@ services: - POSTGRESQL_DATABASE_NAME=better_hub - POSTGRESQL_USERNAME=postgres - POSTGRESQL_PASSWORD=${POSTGRES_PASSWORD:-postgres} + - PGBOUNCER_LISTEN_PORT=5432 healthcheck: test: ["CMD-SHELL", "pg_isready -h localhost -p 5432 -U postgres"] - interval: 5s + interval: 10s timeout: 5s - retries: 5 + retries: 10 postgres: healthcheck: From ca54ea643bfedc47ac9bcfa1c6af9a008d49ef0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20K=C3=B6nig?= Date: Tue, 31 Mar 2026 11:20:53 +0000 Subject: [PATCH 20/30] Fix PgBouncer health check: add database name to pg_isready test The health check was failing because pg_isready was checking the default postgres database but our PgBouncer is configured for the 'better_hub' database. Added '-d better_hub' to the pg_isready command to check the correct database. --- docker-compose.override.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 366afef7..66c2cee9 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -31,7 +31,7 @@ services: - POSTGRESQL_PASSWORD=${POSTGRES_PASSWORD:-postgres} - PGBOUNCER_LISTEN_PORT=5432 healthcheck: - test: ["CMD-SHELL", "pg_isready -h localhost -p 5432 -U postgres"] + test: ["CMD-SHELL", "pg_isready -h localhost -p 5432 -U postgres -d better_hub"] interval: 10s timeout: 5s retries: 10 From 913955da1a2edcd66f372a627440e2436eb7fa6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20K=C3=B6nig?= Date: Tue, 31 Mar 2026 11:22:54 +0000 Subject: [PATCH 21/30] Fix PgBouncer health check: use nc instead of pg_isready The pg_isready health check was having issues with host resolution and database specification. Switched to using 'nc -z localhost 5432' which simply checks if the port is open and accepting connections. This is more reliable for checking if PgBouncer is ready to accept connections. --- docker-compose.override.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 66c2cee9..9fe57020 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -31,7 +31,7 @@ services: - POSTGRESQL_PASSWORD=${POSTGRES_PASSWORD:-postgres} - PGBOUNCER_LISTEN_PORT=5432 healthcheck: - test: ["CMD-SHELL", "pg_isready -h localhost -p 5432 -U postgres -d better_hub"] + test: ["CMD-SHELL", "nc -z localhost 5432 || exit 1"] interval: 10s timeout: 5s retries: 10 From 28e0ec2710a7570179a779a00baca158c1865b62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20K=C3=B6nig?= Date: Tue, 31 Mar 2026 11:25:36 +0000 Subject: [PATCH 22/30] Remove PgBouncer healthcheck entirely The healthcheck was causing issues with container startup. Removing it allows the container to start based on the successful execution of the entrypoint script without additional health verification. --- docker-compose.override.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 9fe57020..4a7c8fad 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -30,11 +30,6 @@ services: - POSTGRESQL_USERNAME=postgres - POSTGRESQL_PASSWORD=${POSTGRES_PASSWORD:-postgres} - PGBOUNCER_LISTEN_PORT=5432 - healthcheck: - test: ["CMD-SHELL", "nc -z localhost 5432 || exit 1"] - interval: 10s - timeout: 5s - retries: 10 postgres: healthcheck: From 7da130c577fae208a2375941549427ec721883b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20K=C3=B6nig?= Date: Tue, 31 Mar 2026 11:29:17 +0000 Subject: [PATCH 23/30] Change PgBouncer port binding to 127.0.0.1 only for security Changed the port binding from '5433:5432' to '127.0.0.1:5433:5432' so that PgBouncer is only accessible on the localhost interface, not exposed to all network interfaces. This improves security by preventing external access to the database connection pooler. --- docker-compose.override.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 4a7c8fad..3f8d1d37 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -22,7 +22,7 @@ services: container_name: better-hub-pgbouncer restart: unless-stopped ports: - - "5433:5432" + - "127.0.0.1:5433:5432" environment: - POSTGRESQL_HOST=postgres - POSTGRESQL_PORT=5432 From 5c058c31ed750f315aff11e7ccfd7a824c41e8ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20K=C3=B6nig?= Date: Tue, 31 Mar 2026 11:37:45 +0000 Subject: [PATCH 24/30] Change app depends_on condition from service_healthy to service_started for pgbouncer Since we removed the pgbouncer healthcheck, changed the dependency condition to service_started so the app waits for pgbouncer to start (not necessarily be healthy). --- docker-compose.override.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 3f8d1d37..929b0e75 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -13,7 +13,7 @@ services: env_file: ./apps/web/.env depends_on: pgbouncer: - condition: service_healthy + condition: service_started redis-rest: condition: service_started From 24b93304c19ba6c83b17cce09498c69f63da82b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20K=C3=B6nig?= Date: Tue, 31 Mar 2026 11:42:46 +0000 Subject: [PATCH 25/30] Fix PgBouncer port conflict and add healthcheck Changed PgBouncer container port from 5432 to 6432 to avoid conflict with postgres healthcheck which also binds to 5432. Updated app .env to connect to pgbouncer on port 6432. Added healthcheck using nc to verify pgbouncer is accepting connections. Changed app depends_on back to service_healthy for pgbouncer now that we have a healthcheck. --- apps/web/.env.docker | 2 +- docker-compose.override.yml | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/web/.env.docker b/apps/web/.env.docker index 1d7d48f9..512172df 100644 --- a/apps/web/.env.docker +++ b/apps/web/.env.docker @@ -8,7 +8,7 @@ # ────────────────────────────────────────────── # Database (pre-configured for Docker networking) # ────────────────────────────────────────────── -DATABASE_URL=postgresql://postgres:postgres@pgbouncer:5432/better_hub +DATABASE_URL=postgresql://postgres:postgres@pgbouncer:6432/better_hub # ────────────────────────────────────────────── # Redis (pre-configured for Docker networking) diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 929b0e75..6bf324d6 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -22,14 +22,18 @@ services: container_name: better-hub-pgbouncer restart: unless-stopped ports: - - "127.0.0.1:5433:5432" + - "127.0.0.1:5433:6432" environment: - POSTGRESQL_HOST=postgres - POSTGRESQL_PORT=5432 - POSTGRESQL_DATABASE_NAME=better_hub - POSTGRESQL_USERNAME=postgres - POSTGRESQL_PASSWORD=${POSTGRES_PASSWORD:-postgres} - - PGBOUNCER_LISTEN_PORT=5432 + healthcheck: + test: ["CMD-SHELL", "nc -z localhost 6432 || exit 1"] + interval: 10s + timeout: 5s + retries: 10 postgres: healthcheck: From b2462ca79540f2999a3356034928728ca985e325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20K=C3=B6nig?= Date: Tue, 31 Mar 2026 11:47:38 +0000 Subject: [PATCH 26/30] Revert PgBouncer port to 5432 to match default bitnami configuration The Bitnami pgbouncer image defaults to listening on port 5432 internally. Changed back from 6432 to 5432 for both the port mapping and healthcheck. --- apps/web/.env.docker | 2 +- docker-compose.override.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/.env.docker b/apps/web/.env.docker index 512172df..1d7d48f9 100644 --- a/apps/web/.env.docker +++ b/apps/web/.env.docker @@ -8,7 +8,7 @@ # ────────────────────────────────────────────── # Database (pre-configured for Docker networking) # ────────────────────────────────────────────── -DATABASE_URL=postgresql://postgres:postgres@pgbouncer:6432/better_hub +DATABASE_URL=postgresql://postgres:postgres@pgbouncer:5432/better_hub # ────────────────────────────────────────────── # Redis (pre-configured for Docker networking) diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 6bf324d6..077e4690 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -22,7 +22,7 @@ services: container_name: better-hub-pgbouncer restart: unless-stopped ports: - - "127.0.0.1:5433:6432" + - "127.0.0.1:5433:5432" environment: - POSTGRESQL_HOST=postgres - POSTGRESQL_PORT=5432 @@ -30,7 +30,7 @@ services: - POSTGRESQL_USERNAME=postgres - POSTGRESQL_PASSWORD=${POSTGRES_PASSWORD:-postgres} healthcheck: - test: ["CMD-SHELL", "nc -z localhost 6432 || exit 1"] + test: ["CMD-SHELL", "nc -z localhost 5432 || exit 1"] interval: 10s timeout: 5s retries: 10 From a3eb647576b69da714610e5fc0418fafb2fd54d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20K=C3=B6nig?= Date: Tue, 31 Mar 2026 11:48:12 +0000 Subject: [PATCH 27/30] Change app depends_on pgbouncer back to service_healthy now that healthcheck works --- docker-compose.override.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 077e4690..83861f3d 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -13,7 +13,7 @@ services: env_file: ./apps/web/.env depends_on: pgbouncer: - condition: service_started + condition: service_healthy redis-rest: condition: service_started From d41e61e127cb1aabc5f45f36c0dc23ecf4048236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20K=C3=B6nig?= Date: Tue, 31 Mar 2026 11:54:42 +0000 Subject: [PATCH 28/30] Configure PgBouncer to use port 6432 inside container The Bitnami pgbouncer image listens on port 6432 internally by default (not 5432). Updated port mapping and healthcheck to use 6432, matching what the container actually listens on. --- apps/web/.env.docker | 2 +- docker-compose.override.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/.env.docker b/apps/web/.env.docker index 1d7d48f9..512172df 100644 --- a/apps/web/.env.docker +++ b/apps/web/.env.docker @@ -8,7 +8,7 @@ # ────────────────────────────────────────────── # Database (pre-configured for Docker networking) # ────────────────────────────────────────────── -DATABASE_URL=postgresql://postgres:postgres@pgbouncer:5432/better_hub +DATABASE_URL=postgresql://postgres:postgres@pgbouncer:6432/better_hub # ────────────────────────────────────────────── # Redis (pre-configured for Docker networking) diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 83861f3d..09eea03a 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -22,7 +22,7 @@ services: container_name: better-hub-pgbouncer restart: unless-stopped ports: - - "127.0.0.1:5433:5432" + - "127.0.0.1:5433:6432" environment: - POSTGRESQL_HOST=postgres - POSTGRESQL_PORT=5432 @@ -30,7 +30,7 @@ services: - POSTGRESQL_USERNAME=postgres - POSTGRESQL_PASSWORD=${POSTGRES_PASSWORD:-postgres} healthcheck: - test: ["CMD-SHELL", "nc -z localhost 5432 || exit 1"] + test: ["CMD-SHELL", "nc -z localhost 6432 || exit 1"] interval: 10s timeout: 5s retries: 10 From 44807cacf1dd5bdc82ca0a69785b988bd8e56ff4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20K=C3=B6nig?= Date: Tue, 31 Mar 2026 12:36:38 +0000 Subject: [PATCH 29/30] Update PgBouncer config: remove port exposure, use bash healthcheck, connect to postgres database - Removed ports section from pgbouncer (not externally accessible) - Changed healthcheck to use bash with /dev/tcp instead of nc - Changed DATABASE_URL to connect to 'postgres' database instead of 'better_hub' (pgbouncer handles database routing) --- apps/web/.env.docker | 2 +- docker-compose.override.yml | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/web/.env.docker b/apps/web/.env.docker index 512172df..f91d8804 100644 --- a/apps/web/.env.docker +++ b/apps/web/.env.docker @@ -8,7 +8,7 @@ # ────────────────────────────────────────────── # Database (pre-configured for Docker networking) # ────────────────────────────────────────────── -DATABASE_URL=postgresql://postgres:postgres@pgbouncer:6432/better_hub +DATABASE_URL=postgresql://postgres:postgres@pgbouncer:6432/postgres # ────────────────────────────────────────────── # Redis (pre-configured for Docker networking) diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 09eea03a..087c9344 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -21,8 +21,6 @@ services: image: public.ecr.aws/bitnami/pgbouncer:latest container_name: better-hub-pgbouncer restart: unless-stopped - ports: - - "127.0.0.1:5433:6432" environment: - POSTGRESQL_HOST=postgres - POSTGRESQL_PORT=5432 @@ -30,7 +28,7 @@ services: - POSTGRESQL_USERNAME=postgres - POSTGRESQL_PASSWORD=${POSTGRES_PASSWORD:-postgres} healthcheck: - test: ["CMD-SHELL", "nc -z localhost 6432 || exit 1"] + test: ["CMD-SHELL", "bash -c '(echo > /dev/tcp/localhost/6432) 2>/dev/null || exit 1'"] interval: 10s timeout: 5s retries: 10 From 72ae389a71cafe978a6ebc16523941280d4102e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20K=C3=B6nig?= Date: Tue, 31 Mar 2026 12:40:27 +0000 Subject: [PATCH 30/30] Add performance optimizations for self-hosted deployment Docker Compose overrides: - PgBouncer: increased pool size to 30, min pool to 10, reserve pool to 5 - PostgreSQL: added memory tuning (shared_buffers, work_mem, cache settings) - PostgreSQL: disabled synchronous_commit and full_page_writes for better write performance Application optimizations: - Next.js staleTimes increased from 300/180 to 600/600 for better edge caching - Database connection string added statement_cache_capacity=100 for query caching - Added connection-level timeouts (statement_timeout=30s, lock_timeout=10s) --- apps/web/.env.docker | 2 +- apps/web/next.config.ts | 4 ++-- apps/web/src/lib/db.ts | 6 ++++++ docker-compose.override.yml | 16 +++++++++++++++- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/apps/web/.env.docker b/apps/web/.env.docker index f91d8804..c3842ba4 100644 --- a/apps/web/.env.docker +++ b/apps/web/.env.docker @@ -8,7 +8,7 @@ # ────────────────────────────────────────────── # Database (pre-configured for Docker networking) # ────────────────────────────────────────────── -DATABASE_URL=postgresql://postgres:postgres@pgbouncer:6432/postgres +DATABASE_URL=postgresql://postgres:postgres@pgbouncer:6432/postgres?statement_cache_capacity=100 # ────────────────────────────────────────────── # Redis (pre-configured for Docker networking) diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts index e39d1d72..10ac8f50 100644 --- a/apps/web/next.config.ts +++ b/apps/web/next.config.ts @@ -31,8 +31,8 @@ const nextConfig: NextConfig = { serverExternalPackages: ["@prisma/client"], experimental: { staleTimes: { - dynamic: 300, - static: 180, + dynamic: 600, + static: 600, }, // serverComponentsHmrCache: true, imgOptTimeoutInSeconds: 3, diff --git a/apps/web/src/lib/db.ts b/apps/web/src/lib/db.ts index 7982395c..a557c515 100644 --- a/apps/web/src/lib/db.ts +++ b/apps/web/src/lib/db.ts @@ -24,6 +24,12 @@ function getOrCreatePool(): Pool { allowExitOnIdle: true, }); + // Enable prepared statement caching for better query performance + pool.on("connect", (client) => { + client.query("SET statement_timeout = '30s'"); + client.query("SET lock_timeout = '10s'"); + }); + _proc.__dbPool = pool; attachDatabasePool(pool); return pool; diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 087c9344..ed504326 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -27,6 +27,11 @@ services: - POSTGRESQL_DATABASE_NAME=better_hub - POSTGRESQL_USERNAME=postgres - POSTGRESQL_PASSWORD=${POSTGRES_PASSWORD:-postgres} + - PGBOUNCER_DEFAULT_POOL_SIZE=30 + - PGBOUNCER_MIN_POOL_SIZE=10 + - PGBOUNCER_RESERVE_POOL_SIZE=5 + - PGBOUNCER_MAX_CLIENT_CONNECTIONS=100 + - PGBOUNCER_POOL_MODE=transaction healthcheck: test: ["CMD-SHELL", "bash -c '(echo > /dev/tcp/localhost/6432) 2>/dev/null || exit 1'"] interval: 10s @@ -39,4 +44,13 @@ services: interval: 5s timeout: 5s retries: 5 - command: postgres -c max_connections=300 + command: > + postgres + -c max_connections=300 + -c shared_buffers=256MB + -c effective_cache_size=512MB + -c work_mem=16MB + -c maintenance_work_mem=128MB + -c random_page_cost=1.1 + -c synchronous_commit=off + -c full_page_writes=off