diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 5db9e300..cd6216d2 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -6,9 +6,6 @@ on: - main tags: - 'v*' - pull_request: - branches: - - main workflow_dispatch: env: @@ -30,7 +27,6 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Log in to Container Registry - if: github.event_name != 'pull_request' uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} @@ -44,7 +40,6 @@ jobs: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=ref,event=branch - type=ref,event=pr type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=raw,value=latest,enable={{is_default_branch}} @@ -54,7 +49,7 @@ jobs: with: context: . file: ./docker/Dockerfile - push: ${{ github.event_name != 'pull_request' }} + push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha diff --git a/DOCKER.md b/DOCKER.md index 26e6ffc6..6467d970 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -1,10 +1,8 @@ # Docker Deployment Guide -Run prompts.chat with a single command using Docker. +Run your own prompts.chat instance with a single command. -## Quick Start (All-in-One Image) - -The easiest way to run prompts.chat - a single container with Node.js and PostgreSQL: +## Quick Start ```bash docker run -d \ @@ -14,73 +12,86 @@ docker run -d \ ghcr.io/f/prompts.chat ``` +**First run:** The container will clone the repository and build the app (~3-5 minutes). +**Subsequent runs:** Starts immediately using the cached build. + Open http://localhost in your browser. -## Whitelabel / Custom Branding +## Custom Branding -Build your own branded image with custom name, logo, and colors: +Customize your instance with environment variables: ```bash -docker build \ - --build-arg BRAND_NAME="My Prompt Library" \ - --build-arg BRAND_DESCRIPTION="Our team's AI prompts" \ - --build-arg BRAND_COLOR="#ff6600" \ - --build-arg AUTH_PROVIDERS="github,google" \ - --build-arg LOCALES="en,es,fr" \ - -f docker/Dockerfile \ - -t my-prompts . - -docker run -p 80:80 -v prompts-data:/data my-prompts -``` - -### Build Arguments - -| Argument | Description | Default | -|----------|-------------|---------| -| `BRAND_NAME` | App name shown in UI | `My Prompt Library` | -| `BRAND_DESCRIPTION` | App description | `Collect, organize, and share AI prompts` | -| `BRAND_LOGO` | Logo path (in public/) | `/logo.svg` | -| `BRAND_LOGO_DARK` | Dark mode logo | Same as `BRAND_LOGO` | -| `BRAND_FAVICON` | Favicon path | `/logo.svg` | -| `BRAND_COLOR` | Primary color (hex) | `#6366f1` | -| `THEME_RADIUS` | Border radius: `none\|sm\|md\|lg` | `sm` | -| `THEME_VARIANT` | UI style: `default\|flat\|brutal` | `default` | -| `THEME_DENSITY` | Spacing: `compact\|default\|comfortable` | `default` | -| `AUTH_PROVIDERS` | Auth providers (comma-separated) | `credentials` | -| `ALLOW_REGISTRATION` | Allow public signup | `true` | -| `LOCALES` | Supported locales (comma-separated) | `en` | -| `DEFAULT_LOCALE` | Default locale | `en` | -| `FEATURE_PRIVATE_PROMPTS` | Enable private prompts | `true` | -| `FEATURE_CHANGE_REQUESTS` | Enable versioning | `true` | -| `FEATURE_CATEGORIES` | Enable categories | `true` | -| `FEATURE_TAGS` | Enable tags | `true` | -| `FEATURE_COMMENTS` | Enable comments | `true` | -| `FEATURE_AI_SEARCH` | Enable AI search | `false` | -| `FEATURE_AI_GENERATION` | Enable AI generation | `false` | -| `FEATURE_MCP` | Enable MCP features | `false` | - -### Adding Custom Logo - -1. Create your logo file (SVG recommended) -2. Mount it when running: - -```bash -docker run -p 80:80 \ - -v ./my-logo.svg:/app/public/logo.svg \ +docker run -d \ + --name my-prompts \ + -p 80:80 \ -v prompts-data:/data \ - my-prompts + -e PCHAT_NAME="Acme Prompts" \ + -e PCHAT_DESCRIPTION="Our team's AI prompt library" \ + -e PCHAT_COLOR="#ff6600" \ + -e PCHAT_AUTH_PROVIDERS="github,google" \ + -e PCHAT_LOCALES="en,es,fr" \ + ghcr.io/f/prompts.chat ``` -Or include it in your own Dockerfile: +> **Note:** Branding is applied during the first build. To change branding later, delete the volume and re-run: +> ```bash +> docker rm -f my-prompts +> docker volume rm prompts-data +> docker run ... # with new env vars +> ``` -```dockerfile -FROM ghcr.io/f/prompts.chat -COPY my-logo.svg /app/public/logo.svg -``` +## Configuration Variables -## Configuration +All variables are prefixed with `PCHAT_` to avoid conflicts. -### Environment Variables +#### Branding (`branding.*` in prompts.config.ts) + +| Env Variable | Config Path | Description | Default | +|--------------|-------------|-------------|---------| +| `PCHAT_NAME` | `branding.name` | App name shown in UI | `My Prompt Library` | +| `PCHAT_DESCRIPTION` | `branding.description` | App description | `Collect, organize...` | +| `PCHAT_LOGO` | `branding.logo` | Logo path (in public/) | `/logo.svg` | +| `PCHAT_LOGO_DARK` | `branding.logoDark` | Dark mode logo | Same as `PCHAT_LOGO` | +| `PCHAT_FAVICON` | `branding.favicon` | Favicon path | `/logo.svg` | + +#### Theme (`theme.*` in prompts.config.ts) + +| Env Variable | Config Path | Description | Default | +|--------------|-------------|-------------|---------| +| `PCHAT_COLOR` | `theme.colors.primary` | Primary color (hex) | `#6366f1` | +| `PCHAT_THEME_RADIUS` | `theme.radius` | Border radius: `none\|sm\|md\|lg` | `sm` | +| `PCHAT_THEME_VARIANT` | `theme.variant` | UI style: `default\|flat\|brutal` | `default` | +| `PCHAT_THEME_DENSITY` | `theme.density` | Spacing: `compact\|default\|comfortable` | `default` | + +#### Authentication (`auth.*` in prompts.config.ts) + +| Env Variable | Config Path | Description | Default | +|--------------|-------------|-------------|---------| +| `PCHAT_AUTH_PROVIDERS` | `auth.providers` | Providers: `github,google,credentials` | `credentials` | +| `PCHAT_ALLOW_REGISTRATION` | `auth.allowRegistration` | Allow public signup | `true` | + +#### Internationalization (`i18n.*` in prompts.config.ts) + +| Env Variable | Config Path | Description | Default | +|--------------|-------------|-------------|---------| +| `PCHAT_LOCALES` | `i18n.locales` | Supported locales (comma-separated) | `en` | +| `PCHAT_DEFAULT_LOCALE` | `i18n.defaultLocale` | Default locale | `en` | + +#### Features (`features.*` in prompts.config.ts) + +| Env Variable | Config Path | Description | Default | +|--------------|-------------|-------------|---------| +| `PCHAT_FEATURE_PRIVATE_PROMPTS` | `features.privatePrompts` | Enable private prompts | `true` | +| `PCHAT_FEATURE_CHANGE_REQUESTS` | `features.changeRequests` | Enable versioning | `true` | +| `PCHAT_FEATURE_CATEGORIES` | `features.categories` | Enable categories | `true` | +| `PCHAT_FEATURE_TAGS` | `features.tags` | Enable tags | `true` | +| `PCHAT_FEATURE_COMMENTS` | `features.comments` | Enable comments | `true` | +| `PCHAT_FEATURE_AI_SEARCH` | `features.aiSearch` | Enable AI search | `false` | +| `PCHAT_FEATURE_AI_GENERATION` | `features.aiGeneration` | Enable AI generation | `false` | +| `PCHAT_FEATURE_MCP` | `features.mcp` | Enable MCP features | `false` | + +## System Environment Variables | Variable | Description | Default | |----------|-------------|---------| @@ -88,34 +99,29 @@ COPY my-logo.svg /app/public/logo.svg | `PORT` | Port to run the app on | `80` | | `DATABASE_URL` | PostgreSQL connection string | Internal DB | -### Production Setup +## Production Setup -For production, always set `AUTH_SECRET`: +For production, set `AUTH_SECRET` explicitly: ```bash docker run -d \ --name prompts \ -p 80:80 \ -v prompts-data:/data \ - -e AUTH_SECRET="your-secret-key-min-32-chars-long" \ + -e AUTH_SECRET="$(openssl rand -base64 32)" \ + -e PCHAT_NAME="My Company Prompts" \ ghcr.io/f/prompts.chat ``` -Generate a secure secret: -```bash -openssl rand -base64 32 -``` - ### With OAuth Providers -Enable GitHub/Google authentication: - ```bash docker run -d \ --name prompts \ -p 80:80 \ -v prompts-data:/data \ -e AUTH_SECRET="your-secret-key" \ + -e PCHAT_AUTH_PROVIDERS="github,google" \ -e AUTH_GITHUB_ID="your-github-client-id" \ -e AUTH_GITHUB_SECRET="your-github-client-secret" \ -e AUTH_GOOGLE_ID="your-google-client-id" \ @@ -123,69 +129,30 @@ docker run -d \ ghcr.io/f/prompts.chat ``` -### With AI Search (OpenAI) - -Enable semantic search with OpenAI embeddings: +### With AI Features (OpenAI) ```bash docker run -d \ --name prompts \ -p 80:80 \ -v prompts-data:/data \ - -e AUTH_SECRET="your-secret-key" \ + -e PCHAT_FEATURE_AI_SEARCH="true" \ -e OPENAI_API_KEY="sk-..." \ ghcr.io/f/prompts.chat ``` -## Docker Compose (Separate Containers) +## Custom Logo -For more control, use Docker Compose with separate app and database containers: +Mount your logo file: ```bash -# Clone the repository -git clone https://github.com/f/awesome-chatgpt-prompts.git -cd awesome-chatgpt-prompts - -# Create .env file -echo "AUTH_SECRET=$(openssl rand -base64 32)" > .env - -# Start services -docker compose up -d -``` - -### docker-compose.yml - -```yaml -services: - app: - build: - context: . - dockerfile: docker/Dockerfile.app - ports: - - "80:3000" - environment: - - DATABASE_URL=postgresql://prompts:prompts@db:5432/prompts - - AUTH_SECRET=${AUTH_SECRET} - depends_on: - db: - condition: service_healthy - - db: - image: postgres:16-alpine - environment: - - POSTGRES_USER=prompts - - POSTGRES_PASSWORD=prompts - - POSTGRES_DB=prompts - volumes: - - postgres_data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U prompts"] - interval: 10s - timeout: 5s - retries: 5 - -volumes: - postgres_data: +docker run -d \ + --name prompts \ + -p 80:80 \ + -v prompts-data:/data \ + -v ./my-logo.svg:/data/app/public/logo.svg \ + -e PCHAT_NAME="My App" \ + ghcr.io/f/prompts.chat ``` ## Data Persistence @@ -215,17 +182,11 @@ docker exec -i prompts psql -U prompts prompts < backup.sql ## Building Locally -Build the all-in-one image: +Build and run locally: ```bash docker build -f docker/Dockerfile -t prompts.chat . -docker run -p 80:80 prompts.chat -``` - -Build the app-only image (for docker-compose): - -```bash -docker build -f docker/Dockerfile.app -t prompts.chat-app . +docker run -p 80:80 -v prompts-data:/data prompts.chat ``` ## Health Check diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index ee2e1719..00000000 --- a/docker-compose.yml +++ /dev/null @@ -1,46 +0,0 @@ -# Docker Compose for prompts.chat -# Alternative to the all-in-one image - separates app and database -# -# Usage: -# docker compose up -d -# docker compose down - -services: - app: - build: - context: . - dockerfile: docker/Dockerfile.app - ports: - - "80:3000" - environment: - - NODE_ENV=production - - DATABASE_URL=postgresql://prompts:prompts@db:5432/prompts?schema=public - - AUTH_SECRET=${AUTH_SECRET:-change-me-in-production} - depends_on: - db: - condition: service_healthy - restart: unless-stopped - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - - db: - image: postgres:16-alpine - environment: - - POSTGRES_USER=prompts - - POSTGRES_PASSWORD=prompts - - POSTGRES_DB=prompts - volumes: - - postgres_data:/var/lib/postgresql/data - restart: unless-stopped - healthcheck: - test: ["CMD-SHELL", "pg_isready -U prompts -d prompts"] - interval: 10s - timeout: 5s - retries: 5 - -volumes: - postgres_data: diff --git a/docker/Dockerfile b/docker/Dockerfile index 862f8072..97f61ea7 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,107 +1,22 @@ -# prompts.chat All-in-One Docker Image -# Contains Node.js + PostgreSQL for single-container deployment +# prompts.chat Bootstrap Docker Image +# Lightweight image that clones and builds on first run # # Usage: -# docker build -t my-prompts . -# docker build --build-arg BRAND_NAME="My App" --build-arg BRAND_COLOR="#ff0000" -t my-prompts . -# docker run -p 80:80 my-prompts -# -# Build Arguments (all optional): -# BRAND_NAME, BRAND_DESCRIPTION, BRAND_LOGO, BRAND_COLOR -# AUTH_PROVIDERS (comma-separated: github,google,credentials) -# LOCALES (comma-separated: en,es,fr) +# docker run -p 80:80 -v prompts-data:/data ghcr.io/f/prompts.chat +# docker run -p 80:80 -v prompts-data:/data -e PCHAT_NAME="My App" ghcr.io/f/prompts.chat -FROM node:24-bookworm-slim AS builder - -# Build arguments for whitelabel branding -ARG BRAND_NAME="My Prompt Library" -ARG BRAND_DESCRIPTION="Collect, organize, and share AI prompts" -ARG BRAND_LOGO="/logo.svg" -ARG BRAND_LOGO_DARK="" -ARG BRAND_FAVICON="/logo.svg" -ARG BRAND_COLOR="#6366f1" -ARG THEME_RADIUS="sm" -ARG THEME_VARIANT="default" -ARG THEME_DENSITY="default" -ARG AUTH_PROVIDERS="credentials" -ARG ALLOW_REGISTRATION="true" -ARG LOCALES="en" -ARG DEFAULT_LOCALE="en" -ARG FEATURE_PRIVATE_PROMPTS="true" -ARG FEATURE_CHANGE_REQUESTS="true" -ARG FEATURE_CATEGORIES="true" -ARG FEATURE_TAGS="true" -ARG FEATURE_COMMENTS="true" -ARG FEATURE_AI_SEARCH="false" -ARG FEATURE_AI_GENERATION="false" -ARG FEATURE_MCP="false" - -WORKDIR /app - -# Copy package files first for better caching -COPY package.json package-lock.json ./ -COPY prisma ./prisma/ - -# Install all dependencies (including dev for build) -RUN npm ci - -# Copy application files -COPY . . - -# Remove unnecessary files -RUN rm -rf .github .claude packages .git - -# Run docker-setup.js to generate prompts.config.ts with branding -ENV BRAND_NAME=${BRAND_NAME} -ENV BRAND_DESCRIPTION=${BRAND_DESCRIPTION} -ENV BRAND_LOGO=${BRAND_LOGO} -ENV BRAND_LOGO_DARK=${BRAND_LOGO_DARK} -ENV BRAND_FAVICON=${BRAND_FAVICON} -ENV BRAND_COLOR=${BRAND_COLOR} -ENV THEME_RADIUS=${THEME_RADIUS} -ENV THEME_VARIANT=${THEME_VARIANT} -ENV THEME_DENSITY=${THEME_DENSITY} -ENV AUTH_PROVIDERS=${AUTH_PROVIDERS} -ENV ALLOW_REGISTRATION=${ALLOW_REGISTRATION} -ENV LOCALES=${LOCALES} -ENV DEFAULT_LOCALE=${DEFAULT_LOCALE} -ENV FEATURE_PRIVATE_PROMPTS=${FEATURE_PRIVATE_PROMPTS} -ENV FEATURE_CHANGE_REQUESTS=${FEATURE_CHANGE_REQUESTS} -ENV FEATURE_CATEGORIES=${FEATURE_CATEGORIES} -ENV FEATURE_TAGS=${FEATURE_TAGS} -ENV FEATURE_COMMENTS=${FEATURE_COMMENTS} -ENV FEATURE_AI_SEARCH=${FEATURE_AI_SEARCH} -ENV FEATURE_AI_GENERATION=${FEATURE_AI_GENERATION} -ENV FEATURE_MCP=${FEATURE_MCP} - -RUN node scripts/docker-setup.js - -# Generate Prisma client and build -# DATABASE_URL is needed at build time for Prisma, but not used -ENV NEXT_TELEMETRY_DISABLED=1 -ENV DATABASE_URL="postgresql://dummy:dummy@localhost:5432/dummy" -RUN npx prisma generate && npm run build - -# Production image FROM node:24-bookworm-slim -# Labels for GitHub Container Registry LABEL org.opencontainers.image.source="https://github.com/f/awesome-chatgpt-prompts" LABEL org.opencontainers.image.description="prompts.chat - Self-hosted AI prompt library" LABEL org.opencontainers.image.licenses="MIT" -# Environment variables -ENV DEBIAN_FRONTEND=noninteractive -ENV NODE_ENV=production -ENV PORT=80 -ENV HOSTNAME="0.0.0.0" -ENV DATABASE_URL="postgresql://prompts:prompts@localhost:5432/prompts?schema=public" - -# Install PostgreSQL, supervisor, and utilities +# Install PostgreSQL, git, and utilities RUN apt-get update && apt-get install -y --no-install-recommends \ postgresql-15 \ postgresql-contrib-15 \ supervisor \ + git \ curl \ openssl \ ca-certificates \ @@ -109,38 +24,25 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && mkdir -p /var/run/postgresql /var/log/supervisor \ && chown -R postgres:postgres /var/run/postgresql +# Create directories WORKDIR /app +RUN mkdir -p /data/postgres /data/app && chown -R postgres:postgres /data/postgres -# Copy built application from builder -COPY --from=builder /app/public ./public -COPY --from=builder /app/prisma ./prisma -COPY --from=builder /app/prompts.csv ./prompts.csv -COPY --from=builder /app/messages ./messages -COPY --from=builder /app/prompts.config.ts ./prompts.config.ts -COPY --from=builder /app/package.json ./package.json -COPY --from=builder /app/.next/standalone ./ -COPY --from=builder /app/.next/static ./.next/static - -# Copy node_modules for prisma CLI (needed for migrations) -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 Docker configuration files +# Copy bootstrap scripts +COPY docker/bootstrap.sh /bootstrap.sh COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf -COPY docker/entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh +RUN chmod +x /bootstrap.sh -# Create data directory for PostgreSQL -RUN mkdir -p /data/postgres && chown -R postgres:postgres /data/postgres +# Environment defaults +ENV NODE_ENV=production +ENV PORT=80 +ENV HOSTNAME="0.0.0.0" +ENV DATABASE_URL="postgresql://prompts:prompts@localhost:5432/prompts?schema=public" +ENV REPO_URL="https://github.com/f/awesome-chatgpt-prompts.git" -# Expose port EXPOSE 80 -# Health check -HEALTHCHECK --interval=30s --timeout=10s --start-period=90s --retries=3 \ +HEALTHCHECK --interval=30s --timeout=10s --start-period=180s --retries=3 \ CMD curl -f http://localhost:80/api/health || exit 1 -# Entrypoint -ENTRYPOINT ["/entrypoint.sh"] -CMD [] +ENTRYPOINT ["/bootstrap.sh"] diff --git a/docker/Dockerfile.app b/docker/Dockerfile.app deleted file mode 100644 index a2ab2b9a..00000000 --- a/docker/Dockerfile.app +++ /dev/null @@ -1,101 +0,0 @@ -# prompts.chat App-Only Dockerfile -# For use with docker-compose (separate database container) -# -# Build with branding: -# docker build --build-arg BRAND_NAME="My App" -f docker/Dockerfile.app -t my-prompts . - -FROM node:24-alpine AS base - -# Install dependencies only when needed -FROM base AS deps -RUN apk add --no-cache libc6-compat -WORKDIR /app - -# Copy package files -COPY package.json package-lock.json ./ -COPY prisma ./prisma/ - -# Install dependencies -RUN npm ci - -# Rebuild the source code only when needed -FROM base AS builder - -# Build arguments for whitelabel branding -ARG BRAND_NAME="My Prompt Library" -ARG BRAND_DESCRIPTION="Collect, organize, and share AI prompts" -ARG BRAND_LOGO="/logo.svg" -ARG BRAND_LOGO_DARK="" -ARG BRAND_FAVICON="/logo.svg" -ARG BRAND_COLOR="#6366f1" -ARG THEME_RADIUS="sm" -ARG THEME_VARIANT="default" -ARG AUTH_PROVIDERS="credentials" -ARG LOCALES="en" - -WORKDIR /app -COPY --from=deps /app/node_modules ./node_modules -COPY . . - -# Remove unnecessary files -RUN rm -rf .github .claude packages docker .git - -# Run docker-setup.js to generate prompts.config.ts with branding -ENV BRAND_NAME=${BRAND_NAME} \ - BRAND_DESCRIPTION=${BRAND_DESCRIPTION} \ - BRAND_LOGO=${BRAND_LOGO} \ - BRAND_LOGO_DARK=${BRAND_LOGO_DARK} \ - BRAND_FAVICON=${BRAND_FAVICON} \ - BRAND_COLOR=${BRAND_COLOR} \ - THEME_RADIUS=${THEME_RADIUS} \ - THEME_VARIANT=${THEME_VARIANT} \ - AUTH_PROVIDERS=${AUTH_PROVIDERS} \ - LOCALES=${LOCALES} - -RUN node scripts/docker-setup.js - -# DATABASE_URL is needed at build time for Prisma, but not used -ENV NEXT_TELEMETRY_DISABLED=1 -ENV DATABASE_URL="postgresql://dummy:dummy@localhost:5432/dummy" - -# Generate Prisma client -RUN npx prisma generate - -# Build Next.js -RUN npm run build - -# Production image -FROM base AS runner -WORKDIR /app - -ENV NODE_ENV=production -ENV NEXT_TELEMETRY_DISABLED=1 - -# Install netcat for health checks -RUN apk add --no-cache netcat-openbsd curl - -RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 nextjs - -# Copy built files -COPY --from=builder /app/public ./public -COPY --from=builder /app/prisma ./prisma -COPY --from=builder /app/prompts.csv ./prompts.csv - -# Copy standalone build -COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ -COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static - -# Copy entrypoint -COPY docker/entrypoint-app.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh - -USER nextjs - -EXPOSE 3000 - -ENV PORT=3000 -ENV HOSTNAME="0.0.0.0" - -ENTRYPOINT ["/entrypoint.sh"] -CMD ["node", "server.js"] diff --git a/docker/entrypoint.sh b/docker/bootstrap.sh similarity index 62% rename from docker/entrypoint.sh rename to docker/bootstrap.sh index 775e8a4f..9de7736f 100644 --- a/docker/entrypoint.sh +++ b/docker/bootstrap.sh @@ -9,25 +9,28 @@ echo "║ ║" echo "╚═══════════════════════════════════════════════════════════════╝" echo "" -# Generate AUTH_SECRET if not provided -if [ -z "$AUTH_SECRET" ]; then - export AUTH_SECRET=$(openssl rand -base64 32) - echo "⚠ AUTH_SECRET not provided, generated random secret" - echo " For production, set AUTH_SECRET environment variable" -fi - -# PostgreSQL paths +# Paths +APP_DIR="/data/app" PGDATA="/data/postgres" PGBIN="/usr/lib/postgresql/15/bin" +BUILD_MARKER="/data/.built" -# Initialize PostgreSQL data directory if needed +# Generate AUTH_SECRET if not provided +if [ -z "$AUTH_SECRET" ]; then + if [ -f "/data/.auth_secret" ]; then + export AUTH_SECRET=$(cat /data/.auth_secret) + else + export AUTH_SECRET=$(openssl rand -base64 32) + echo "$AUTH_SECRET" > /data/.auth_secret + echo "⚠ AUTH_SECRET generated and saved" + fi +fi + +# Initialize PostgreSQL if needed if [ ! -f "$PGDATA/PG_VERSION" ]; then - echo "▶ Initializing PostgreSQL database..." - - # Initialize PostgreSQL + echo "▶ Initializing PostgreSQL..." su postgres -c "$PGBIN/initdb -D $PGDATA" - # Configure PostgreSQL cat >> "$PGDATA/postgresql.conf" << EOF listen_addresses = 'localhost' port = 5432 @@ -35,34 +38,77 @@ max_connections = 100 shared_buffers = 128MB EOF - # Configure authentication cat > "$PGDATA/pg_hba.conf" << EOF local all all trust host all all 127.0.0.1/32 trust host all all ::1/128 trust EOF - # Start PostgreSQL temporarily to create database su postgres -c "$PGBIN/pg_ctl -D $PGDATA -l /tmp/pg.log start" sleep 3 - - # Create database and user su postgres -c "$PGBIN/createuser -s prompts 2>/dev/null" || true su postgres -c "$PGBIN/createdb -O prompts prompts 2>/dev/null" || true - - # Stop PostgreSQL (supervisor will start it) su postgres -c "$PGBIN/pg_ctl -D $PGDATA stop" sleep 2 - echo "✓ PostgreSQL initialized" fi +# Clone and build on first run +if [ ! -f "$BUILD_MARKER" ]; then + echo "" + echo "▶ First run detected - building prompts.chat..." + echo "" + + # Clone repository + if [ ! -d "$APP_DIR/.git" ]; then + echo "▶ Cloning repository..." + rm -rf "$APP_DIR" + git clone --depth 1 "$REPO_URL" "$APP_DIR" + echo "✓ Repository cloned" + fi + + cd "$APP_DIR" + + # Clean up unnecessary files + rm -rf .github .claude packages .git + + # Install dependencies + echo "▶ Installing dependencies..." + npm ci + echo "✓ Dependencies installed" + + # Run docker-setup.js to generate config with branding + echo "▶ Generating configuration..." + node scripts/docker-setup.js + echo "✓ Configuration generated" + + # Generate Prisma client + echo "▶ Generating Prisma client..." + npx prisma generate + echo "✓ Prisma client generated" + + # Build Next.js + echo "▶ Building Next.js application (this may take a few minutes)..." + npm run build + echo "✓ Build complete" + + # Mark as built + touch "$BUILD_MARKER" + + echo "" + echo "✅ Build complete! Starting application..." + echo "" +else + echo "✓ Using existing build" + cd "$APP_DIR" +fi + # Start supervisor (manages PostgreSQL and Next.js) echo "▶ Starting services..." /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf & SUPERVISOR_PID=$! -# Wait for PostgreSQL to be ready +# Wait for PostgreSQL echo "▶ Waiting for PostgreSQL..." for i in $(seq 1 30); do if $PGBIN/pg_isready -h localhost -p 5432 >/dev/null 2>&1; then @@ -76,23 +122,17 @@ for i in $(seq 1 30); do sleep 1 done -# Run database migrations +# Run migrations echo "▶ Running database migrations..." -cd /app -./node_modules/prisma/build/index.js migrate deploy +npx prisma migrate deploy echo "✓ Migrations complete" -# Seed database if empty (check if Prompt table exists and has data) +# Seed if empty PROMPT_COUNT=$(su postgres -c "$PGBIN/psql -h localhost -U prompts -d prompts -t -c \"SELECT COUNT(*) FROM \\\"Prompt\\\"\"" 2>/dev/null | tr -d ' ' || echo "0") if [ "$PROMPT_COUNT" = "0" ] || [ -z "$PROMPT_COUNT" ]; then - echo "▶ Seeding database with prompts..." - # Run seed script if available - if [ -f "/app/prisma/seed.ts" ]; then - npx tsx /app/prisma/seed.ts || echo "⚠ Seeding skipped or failed" - fi - echo "✓ Database seeded" -else - echo "✓ Database already has $PROMPT_COUNT prompts" + echo "▶ Seeding database..." + npx tsx prisma/seed.ts 2>/dev/null || echo "⚠ Seeding skipped" + echo "✓ Database ready" fi echo "" @@ -105,5 +145,4 @@ echo "║ ║" echo "╚═══════════════════════════════════════════════════════════════╝" echo "" -# Keep container running and forward signals wait $SUPERVISOR_PID diff --git a/docker/entrypoint-app.sh b/docker/entrypoint-app.sh deleted file mode 100644 index 9a179c52..00000000 --- a/docker/entrypoint-app.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/sh -set -e - -echo "🚀 Starting prompts.chat..." - -# Wait for database to be ready -echo "⏳ Waiting for database..." -until nc -z ${DATABASE_HOST:-db} ${DATABASE_PORT:-5432}; do - sleep 1 -done -echo "✅ Database is ready" - -# Run migrations -echo "📦 Running database migrations..." -npx prisma migrate deploy - -# Check if database needs seeding -echo "🌱 Checking database..." -# Skip seed check in entrypoint, let the app handle it - -echo "✨ prompts.chat is starting on port ${PORT:-3000}" - -# Execute the main command -exec "$@" diff --git a/docker/supervisord.conf b/docker/supervisord.conf index 4ccb684b..f8e165ee 100644 --- a/docker/supervisord.conf +++ b/docker/supervisord.conf @@ -15,8 +15,8 @@ stdout_logfile=/var/log/supervisor/postgresql.log stderr_logfile=/var/log/supervisor/postgresql-error.log [program:nextjs] -command=node /app/server.js -directory=/app +command=node /data/app/.next/standalone/server.js +directory=/data/app user=root autostart=true autorestart=true diff --git a/src/lib/config/index.ts b/src/lib/config/index.ts index 2383f9cb..3599e874 100644 --- a/src/lib/config/index.ts +++ b/src/lib/config/index.ts @@ -83,17 +83,100 @@ export function defineConfig(config: PromptsConfig): PromptsConfig { // Load the user's config let cachedConfig: PromptsConfig | null = null; +/** + * Apply runtime environment variable overrides to config. + * This allows customization via Docker env vars without rebuilding. + * + * All env vars are prefixed with PCHAT_ to avoid conflicts. + * + * Supported env vars: + * PCHAT_NAME, PCHAT_DESCRIPTION, PCHAT_LOGO, PCHAT_LOGO_DARK, PCHAT_FAVICON, PCHAT_COLOR + * PCHAT_THEME_RADIUS (none|sm|md|lg), PCHAT_THEME_VARIANT (default|flat|brutal), PCHAT_THEME_DENSITY + * PCHAT_AUTH_PROVIDERS (comma-separated), PCHAT_ALLOW_REGISTRATION (true|false) + * PCHAT_LOCALES (comma-separated), PCHAT_DEFAULT_LOCALE + * PCHAT_FEATURE_* (true|false for each feature) + */ +function applyEnvOverrides(config: PromptsConfig): PromptsConfig { + const env = process.env; + + // Helper functions + const envBool = (key: string, fallback: boolean): boolean => { + const val = env[key]; + if (val === undefined) return fallback; + return val.toLowerCase() === 'true' || val === '1'; + }; + + const envArray = (key: string, fallback: string[]): string[] => { + const val = env[key]; + if (!val) return fallback; + return val.split(',').map(s => s.trim()).filter(Boolean); + }; + + return { + branding: { + name: env.PCHAT_NAME || config.branding.name, + description: env.PCHAT_DESCRIPTION || config.branding.description, + logo: env.PCHAT_LOGO || config.branding.logo, + logoDark: env.PCHAT_LOGO_DARK || env.PCHAT_LOGO || config.branding.logoDark, + favicon: env.PCHAT_FAVICON || config.branding.favicon, + appStoreUrl: config.branding.appStoreUrl, + chromeExtensionUrl: config.branding.chromeExtensionUrl, + }, + theme: { + radius: (env.PCHAT_THEME_RADIUS as ThemeConfig['radius']) || config.theme.radius, + variant: (env.PCHAT_THEME_VARIANT as ThemeConfig['variant']) || config.theme.variant, + density: (env.PCHAT_THEME_DENSITY as ThemeConfig['density']) || config.theme.density, + colors: { + primary: env.PCHAT_COLOR || config.theme.colors.primary, + secondary: config.theme.colors.secondary, + accent: config.theme.colors.accent, + }, + }, + auth: { + providers: env.PCHAT_AUTH_PROVIDERS + ? envArray('PCHAT_AUTH_PROVIDERS', config.auth.providers || ['credentials']) + : config.auth.providers, + allowRegistration: env.PCHAT_ALLOW_REGISTRATION !== undefined + ? envBool('PCHAT_ALLOW_REGISTRATION', config.auth.allowRegistration) + : config.auth.allowRegistration, + }, + i18n: { + locales: env.PCHAT_LOCALES + ? envArray('PCHAT_LOCALES', config.i18n.locales) + : config.i18n.locales, + defaultLocale: env.PCHAT_DEFAULT_LOCALE || config.i18n.defaultLocale, + }, + features: { + privatePrompts: envBool('PCHAT_FEATURE_PRIVATE_PROMPTS', config.features.privatePrompts), + changeRequests: envBool('PCHAT_FEATURE_CHANGE_REQUESTS', config.features.changeRequests), + categories: envBool('PCHAT_FEATURE_CATEGORIES', config.features.categories), + tags: envBool('PCHAT_FEATURE_TAGS', config.features.tags), + aiSearch: envBool('PCHAT_FEATURE_AI_SEARCH', config.features.aiSearch ?? false), + aiGeneration: envBool('PCHAT_FEATURE_AI_GENERATION', config.features.aiGeneration ?? false), + mcp: envBool('PCHAT_FEATURE_MCP', config.features.mcp ?? false), + comments: envBool('PCHAT_FEATURE_COMMENTS', config.features.comments ?? true), + }, + homepage: env.PCHAT_NAME ? { + // If custom branding via env, use clone branding mode + useCloneBranding: true, + achievements: { enabled: false }, + sponsors: { enabled: false, items: [] }, + } : config.homepage, + }; +} + export async function getConfig(): Promise { if (cachedConfig) return cachedConfig; + let baseConfig: PromptsConfig; + try { // Dynamic import of user config const userConfig = await import("@/../prompts.config"); - cachedConfig = userConfig.default; - return cachedConfig; + baseConfig = userConfig.default; } catch { // Fallback to default config - cachedConfig = { + baseConfig = { branding: { name: "prompts.chat", logo: "/logo.svg", @@ -127,8 +210,11 @@ export async function getConfig(): Promise { comments: true, }, }; - return cachedConfig; } + + // Apply runtime environment variable overrides + cachedConfig = applyEnvOverrides(baseConfig); + return cachedConfig; } // Sync version for client components (must be initialized first)