Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
579c91b
Add production Dockerfile and docker-compose app service for self-hos…
Mar 15, 2026
81a9b9b
Make Docker setup fork-friendly with docker-compose.override.yml
Mar 15, 2026
efbd55a
Add deploy.md with self-hosted setup instructions
Mar 15, 2026
316489d
Fix Dockerfile: copy Prisma schema into deps stage for postinstall
Mar 15, 2026
47acca4
Fix Dockerfile: remove non-existent apps/web/node_modules copy
Mar 15, 2026
bbb4019
Fix Dockerfile: use correct Prisma generated client path
Mar 16, 2026
0ef5331
Fix Dockerfile: include dotenv for prisma.config.ts in runner
Mar 16, 2026
8b3f098
Fix Dockerfile: bypass prisma.config.ts in runner to avoid transitive…
Mar 16, 2026
743dc7c
Fix Prisma migrate deploy: add DATABASE_URL to schema datasource
Mar 16, 2026
faaf1b0
Fix Prisma v7 compat: revert schema url, use Docker-specific config
Mar 16, 2026
adbd805
Fix Dockerfile: install Prisma CLI with full dep tree in runner
Mar 16, 2026
09a5cd5
Fix entrypoint: set NODE_PATH so prisma.config.ts resolves imports
Mar 16, 2026
2f96559
Switch entrypoint from prisma migrate deploy to prisma db push
Mar 16, 2026
b585d85
Update deploy.md to reflect prisma db push usage
Mar 16, 2026
3bdf2e0
Remove --skip-generate flag unsupported in Prisma v7
Mar 16, 2026
fa54b52
Add PgBouncer for database connection pooling and optimize performance
akoenig Mar 31, 2026
110c8a4
Fix PgBouncer configuration: switch to bitnami/pgbouncer image and us…
akoenig Mar 31, 2026
9d87995
Fix PgBouncer image: use public.ecr.aws/bitnami/pgbouncer:latest
akoenig Mar 31, 2026
ee63a86
Fix PgBouncer health check: increase interval and retries, add explic…
akoenig Mar 31, 2026
ca54ea6
Fix PgBouncer health check: add database name to pg_isready test
akoenig Mar 31, 2026
913955d
Fix PgBouncer health check: use nc instead of pg_isready
akoenig Mar 31, 2026
28e0ec2
Remove PgBouncer healthcheck entirely
akoenig Mar 31, 2026
7da130c
Change PgBouncer port binding to 127.0.0.1 only for security
akoenig Mar 31, 2026
5c058c3
Change app depends_on condition from service_healthy to service_start…
akoenig Mar 31, 2026
24b9330
Fix PgBouncer port conflict and add healthcheck
akoenig Mar 31, 2026
b2462ca
Revert PgBouncer port to 5432 to match default bitnami configuration
akoenig Mar 31, 2026
a3eb647
Change app depends_on pgbouncer back to service_healthy now that heal…
akoenig Mar 31, 2026
d41e61e
Configure PgBouncer to use port 6432 inside container
akoenig Mar 31, 2026
44807ca
Update PgBouncer config: remove port exposure, use bash healthcheck, …
akoenig Mar 31, 2026
72ae389
Add performance optimizations for self-hosted deployment
akoenig Mar 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules
.next
.git
*.md
!README.md
docker-compose.override.yml
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ yarn-error.log*
# env files
.env*
!.env.example
!.env.docker

# vercel
.vercel
Expand Down
66 changes: 66 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# ── 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/
COPY apps/web/prisma.config.ts apps/web/
COPY apps/web/prisma apps/web/prisma

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 . .

# 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

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 deploy
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

# 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
RUN chmod +x /app/docker-entrypoint.sh

EXPOSE 3000

ENTRYPOINT ["/app/docker-entrypoint.sh"]
CMD ["bun", "apps/web/server.js"]
39 changes: 39 additions & 0 deletions apps/web/.env.docker
Original file line number Diff line number Diff line change
@@ -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@pgbouncer:6432/postgres?statement_cache_capacity=100

# ──────────────────────────────────────────────
# 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
5 changes: 3 additions & 2 deletions apps/web/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@ const KNOWN_ROUTES = [
];

const nextConfig: NextConfig = {
output: "standalone",
devIndicators: false,
serverExternalPackages: ["@prisma/client"],
experimental: {
staleTimes: {
dynamic: 300,
static: 180,
dynamic: 600,
static: 600,
},
// serverComponentsHmrCache: true,
imgOptTimeoutInSeconds: 3,
Expand Down
22 changes: 10 additions & 12 deletions apps/web/src/lib/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,20 @@ 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,
});

// 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;
Expand Down
85 changes: 85 additions & 0 deletions deploy.md
Original file line number Diff line number Diff line change
@@ -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 db push` syncs the database schema 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 db push on boot
apps/web/.env.docker ← env template for Docker networking
```

## Rebuilding

After pulling new changes:

```sh
docker compose up --build
```

The database schema is synced automatically on each boot via `prisma db push`, so schema changes are applied without extra steps.

## Stopping

```sh
docker compose down
```

To also remove the database volume:

```sh
docker compose down -v
```
56 changes: 56 additions & 0 deletions docker-compose.override.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# 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:
pgbouncer:
condition: service_healthy
redis-rest:
condition: service_started

pgbouncer:
image: public.ecr.aws/bitnami/pgbouncer:latest
container_name: better-hub-pgbouncer
restart: unless-stopped
environment:
- POSTGRESQL_HOST=postgres
- POSTGRESQL_PORT=5432
- 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
timeout: 5s
retries: 10

postgres:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
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
26 changes: 24 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,31 @@ 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
container_name: better-hub-redis
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
Expand All @@ -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:
Expand Down
12 changes: 12 additions & 0 deletions docker-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/bin/sh
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.
echo "Syncing database schema..."
cd /app
NODE_PATH=/prisma-cli/node_modules /prisma-cli/node_modules/.bin/prisma db push --accept-data-loss

echo "Starting application..."
exec "$@"
14 changes: 14 additions & 0 deletions prisma.docker.config.ts
Original file line number Diff line number Diff line change
@@ -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!,
},
});