diff --git a/.env.example b/.env.example
deleted file mode 100644
index 894a989cc..000000000
--- a/.env.example
+++ /dev/null
@@ -1,41 +0,0 @@
-# Docker Specific Env's Only - Can skip if needed
-
-# Celery Config
-REDIS_PORT=6379
-FLOWER_PORT=5555
-
-# Frontend Configuration
-FRONTEND_PORT=3000
-NEXT_PUBLIC_FASTAPI_BACKEND_URL=http://localhost:8000 (Default: http://localhost:8000)
-NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE=LOCAL or GOOGLE (Default: LOCAL)
-NEXT_PUBLIC_ETL_SERVICE=UNSTRUCTURED or LLAMACLOUD or DOCLING (Default: DOCLING)
-# Backend Configuration
-BACKEND_PORT=8000
-# Auth type for backend login flow (Default: LOCAL)
-# Set to GOOGLE if using Google OAuth
-AUTH_TYPE=LOCAL
-# Frontend URL used by backend for CORS allowed origins and OAuth redirects
-# Must match the URL your browser uses to access the frontend
-NEXT_FRONTEND_URL=http://localhost:3000
-
-# Database Configuration
-POSTGRES_USER=postgres
-POSTGRES_PASSWORD=postgres
-POSTGRES_DB=surfsense
-POSTGRES_PORT=5432
-
-# Electric-SQL Configuration
-ELECTRIC_PORT=5133
-# PostgreSQL host for Electric connection
-# - 'db' for Docker PostgreSQL (service name in docker-compose)
-# - 'host.docker.internal' for local PostgreSQL (recommended when Electric runs in Docker)
-# Note: host.docker.internal works on Docker Desktop (Mac/Windows) and can be enabled on Linux
-POSTGRES_HOST=db
-ELECTRIC_DB_USER=electric
-ELECTRIC_DB_PASSWORD=electric_password
-NEXT_PUBLIC_ELECTRIC_URL=http://localhost:5133
-
-# pgAdmin Configuration
-PGADMIN_PORT=5050
-PGADMIN_DEFAULT_EMAIL=admin@surfsense.com
-PGADMIN_DEFAULT_PASSWORD=surfsense
diff --git a/.github/workflows/docker_build.yaml b/.github/workflows/docker_build.yaml
index ff6c838d1..6fd336cf9 100644
--- a/.github/workflows/docker_build.yaml
+++ b/.github/workflows/docker_build.yaml
@@ -1,6 +1,12 @@
-name: Build and Push Docker Image
+name: Build and Push Docker Images
on:
+ push:
+ branches:
+ - main
+ paths:
+ - 'surfsense_backend/**'
+ - 'surfsense_web/**'
workflow_dispatch:
inputs:
branch:
@@ -8,6 +14,10 @@ on:
required: false
default: ''
+concurrency:
+ group: docker-build
+ cancel-in-progress: false
+
permissions:
contents: write
packages: write
@@ -28,33 +38,28 @@ jobs:
- name: Read app version and calculate next Docker build version
id: tag_version
run: |
- # Read version from pyproject.toml
APP_VERSION=$(grep -E '^version = ' surfsense_backend/pyproject.toml | sed 's/version = "\(.*\)"/\1/')
echo "App version from pyproject.toml: $APP_VERSION"
-
+
if [ -z "$APP_VERSION" ]; then
echo "Error: Could not read version from surfsense_backend/pyproject.toml"
exit 1
fi
-
- # Fetch all tags
+
git fetch --tags
-
- # Find the latest docker build tag for this app version (format: APP_VERSION.BUILD_NUMBER)
- # Tags follow pattern: 0.0.11.1, 0.0.11.2, etc.
+
LATEST_BUILD_TAG=$(git tag --list "${APP_VERSION}.*" --sort='-v:refname' | head -n 1)
-
+
if [ -z "$LATEST_BUILD_TAG" ]; then
echo "No previous Docker build tag found for version ${APP_VERSION}. Starting with ${APP_VERSION}.1"
NEXT_VERSION="${APP_VERSION}.1"
else
echo "Latest Docker build tag found: $LATEST_BUILD_TAG"
- # Extract the build number (4th component)
BUILD_NUMBER=$(echo "$LATEST_BUILD_TAG" | rev | cut -d. -f1 | rev)
NEXT_BUILD=$((BUILD_NUMBER + 1))
NEXT_VERSION="${APP_VERSION}.${NEXT_BUILD}"
fi
-
+
echo "Calculated next Docker version: $NEXT_VERSION"
echo "next_version=$NEXT_VERSION" >> $GITHUB_OUTPUT
@@ -78,67 +83,35 @@ jobs:
git ls-remote --tags origin | grep "refs/tags/${{ steps.tag_version.outputs.next_version }}" || (echo "Tag push verification failed!" && exit 1)
echo "Tag successfully pushed."
- # Build for AMD64 on native x64 runner
- build_amd64:
- runs-on: ubuntu-latest
+ build:
needs: tag_release
+ runs-on: ${{ matrix.os }}
permissions:
packages: write
contents: read
- outputs:
- digest: ${{ steps.build.outputs.digest }}
+ strategy:
+ fail-fast: false
+ matrix:
+ platform: [linux/amd64, linux/arm64]
+ image: [backend, web]
+ include:
+ - platform: linux/amd64
+ suffix: amd64
+ os: ubuntu-latest
+ - platform: linux/arm64
+ suffix: arm64
+ os: ubuntu-24.04-arm
+ - image: backend
+ name: surfsense-backend
+ context: ./surfsense_backend
+ file: ./surfsense_backend/Dockerfile
+ - image: web
+ name: surfsense-web
+ context: ./surfsense_web
+ file: ./surfsense_web/Dockerfile
env:
- REGISTRY_IMAGE: ghcr.io/${{ github.repository_owner }}/surfsense
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
+ REGISTRY_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ matrix.name }}
- - name: Set lowercase image name
- id: image
- run: echo "name=${REGISTRY_IMAGE,,}" >> $GITHUB_OUTPUT
-
- - name: Login to GitHub Container Registry
- uses: docker/login-action@v3
- with:
- registry: ghcr.io
- username: ${{ github.repository_owner }}
- password: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
-
- - name: Free up disk space
- run: |
- sudo rm -rf /usr/share/dotnet
- sudo rm -rf /opt/ghc
- sudo rm -rf /usr/local/share/boost
- sudo rm -rf "$AGENT_TOOLSDIRECTORY"
- docker system prune -af
-
- - name: Build and push AMD64 image
- id: build
- uses: docker/build-push-action@v5
- with:
- context: .
- file: ./Dockerfile.allinone
- push: true
- tags: ${{ steps.image.outputs.name }}:${{ needs.tag_release.outputs.new_tag }}-amd64
- platforms: linux/amd64
- cache-from: type=gha,scope=amd64
- cache-to: type=gha,mode=max,scope=amd64
- provenance: false
-
- # Build for ARM64 on native arm64 runner (no QEMU emulation!)
- build_arm64:
- runs-on: ubuntu-24.04-arm
- needs: tag_release
- permissions:
- packages: write
- contents: read
- outputs:
- digest: ${{ steps.build.outputs.digest }}
- env:
- REGISTRY_IMAGE: ghcr.io/${{ github.repository_owner }}/surfsense
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -165,28 +138,41 @@ jobs:
sudo rm -rf "$AGENT_TOOLSDIRECTORY" || true
docker system prune -af
- - name: Build and push ARM64 image
+ - name: Build and push ${{ matrix.name }} (${{ matrix.suffix }})
id: build
- uses: docker/build-push-action@v5
+ uses: docker/build-push-action@v6
with:
- context: .
- file: ./Dockerfile.allinone
+ context: ${{ matrix.context }}
+ file: ${{ matrix.file }}
push: true
- tags: ${{ steps.image.outputs.name }}:${{ needs.tag_release.outputs.new_tag }}-arm64
- platforms: linux/arm64
- cache-from: type=gha,scope=arm64
- cache-to: type=gha,mode=max,scope=arm64
+ tags: ${{ steps.image.outputs.name }}:${{ needs.tag_release.outputs.new_tag }}-${{ matrix.suffix }}
+ platforms: ${{ matrix.platform }}
+ cache-from: type=gha,scope=${{ matrix.image }}-${{ matrix.suffix }}
+ cache-to: type=gha,mode=max,scope=${{ matrix.image }}-${{ matrix.suffix }}
provenance: false
+ build-args: |
+ ${{ matrix.image == 'web' && 'NEXT_PUBLIC_FASTAPI_BACKEND_URL=__NEXT_PUBLIC_FASTAPI_BACKEND_URL__' || '' }}
+ ${{ matrix.image == 'web' && 'NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE=__NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE__' || '' }}
+ ${{ matrix.image == 'web' && 'NEXT_PUBLIC_ETL_SERVICE=__NEXT_PUBLIC_ETL_SERVICE__' || '' }}
+ ${{ matrix.image == 'web' && 'NEXT_PUBLIC_ELECTRIC_URL=__NEXT_PUBLIC_ELECTRIC_URL__' || '' }}
+ ${{ matrix.image == 'web' && 'NEXT_PUBLIC_ELECTRIC_AUTH_MODE=__NEXT_PUBLIC_ELECTRIC_AUTH_MODE__' || '' }}
+ ${{ matrix.image == 'web' && 'NEXT_PUBLIC_DEPLOYMENT_MODE=__NEXT_PUBLIC_DEPLOYMENT_MODE__' || '' }}
- # Create multi-arch manifest combining both platform images
create_manifest:
runs-on: ubuntu-latest
- needs: [tag_release, build_amd64, build_arm64]
+ needs: [tag_release, build]
permissions:
packages: write
contents: read
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - name: surfsense-backend
+ - name: surfsense-web
env:
- REGISTRY_IMAGE: ghcr.io/${{ github.repository_owner }}/surfsense
+ REGISTRY_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ matrix.name }}
+
steps:
- name: Set lowercase image name
id: image
@@ -203,28 +189,31 @@ jobs:
run: |
VERSION_TAG="${{ needs.tag_release.outputs.new_tag }}"
IMAGE="${{ steps.image.outputs.name }}"
-
- # Create manifest for version tag
+ APP_VERSION=$(echo "$VERSION_TAG" | rev | cut -d. -f2- | rev)
+
docker manifest create ${IMAGE}:${VERSION_TAG} \
${IMAGE}:${VERSION_TAG}-amd64 \
${IMAGE}:${VERSION_TAG}-arm64
-
+
docker manifest push ${IMAGE}:${VERSION_TAG}
-
- # Create/update latest tag if on default branch
+
if [[ "${{ github.ref }}" == "refs/heads/${{ github.event.repository.default_branch }}" ]] || [[ "${{ github.event.inputs.branch }}" == "${{ github.event.repository.default_branch }}" ]]; then
+ docker manifest create ${IMAGE}:${APP_VERSION} \
+ ${IMAGE}:${VERSION_TAG}-amd64 \
+ ${IMAGE}:${VERSION_TAG}-arm64
+
+ docker manifest push ${IMAGE}:${APP_VERSION}
+
docker manifest create ${IMAGE}:latest \
${IMAGE}:${VERSION_TAG}-amd64 \
${IMAGE}:${VERSION_TAG}-arm64
-
+
docker manifest push ${IMAGE}:latest
fi
- - name: Clean up architecture-specific tags (optional)
- continue-on-error: true
+ - name: Summary
run: |
- # Note: GHCR doesn't support tag deletion via API easily
- # The arch-specific tags will remain but users should use the main tags
- echo "Multi-arch manifest created successfully!"
- echo "Users should pull: ${{ steps.image.outputs.name }}:${{ needs.tag_release.outputs.new_tag }}"
- echo "Or for latest: ${{ steps.image.outputs.name }}:latest"
+ echo "Multi-arch manifest created for ${{ matrix.name }}!"
+ echo "Versioned: ${{ steps.image.outputs.name }}:${{ needs.tag_release.outputs.new_tag }}"
+ echo "App version: ${{ steps.image.outputs.name }}:$(echo '${{ needs.tag_release.outputs.new_tag }}' | rev | cut -d. -f2- | rev)"
+ echo "Latest: ${{ steps.image.outputs.name }}:latest"
diff --git a/Dockerfile.allinone b/Dockerfile.allinone
deleted file mode 100644
index 6bcf78459..000000000
--- a/Dockerfile.allinone
+++ /dev/null
@@ -1,289 +0,0 @@
-# SurfSense All-in-One Docker Image
-# This image bundles PostgreSQL+pgvector, Redis, Electric SQL, Backend, and Frontend
-# Usage: docker run -d -p 3000:3000 -p 8000:8000 -p 5133:5133 -v surfsense-data:/data --name surfsense ghcr.io/modsetter/surfsense:latest
-#
-# Included Services (all run locally by default):
-# - PostgreSQL 14 + pgvector (vector database)
-# - Redis (task queue)
-# - Electric SQL (real-time sync)
-# - Docling (document processing, CPU-only, OCR disabled)
-# - Kokoro TTS (local text-to-speech for podcasts)
-# - Faster-Whisper (local speech-to-text for audio files)
-# - Playwright Chromium (web scraping)
-#
-# Note: This is the CPU-only version. A :cuda tagged image with GPU support
-# will be available in the future for faster AI inference.
-
-# ====================
-# Stage 1: Get Electric SQL Binary
-# ====================
-FROM electricsql/electric:latest AS electric-builder
-
-# ====================
-# Stage 2: Build Frontend
-# ====================
-FROM node:20-alpine AS frontend-builder
-
-WORKDIR /app
-
-# Install pnpm
-RUN corepack enable pnpm
-
-# Copy package files
-COPY surfsense_web/package.json surfsense_web/pnpm-lock.yaml* ./
-COPY surfsense_web/source.config.ts ./
-COPY surfsense_web/content ./content
-
-# Install dependencies (skip postinstall which requires all source files)
-RUN pnpm install --frozen-lockfile --ignore-scripts
-
-# Copy source
-COPY surfsense_web/ ./
-
-# Run fumadocs-mdx postinstall now that source files are available
-RUN pnpm fumadocs-mdx
-
-# Build with placeholder values that will be replaced at runtime
-# These unique strings allow runtime substitution via entrypoint script
-ENV NEXT_PUBLIC_FASTAPI_BACKEND_URL=__NEXT_PUBLIC_FASTAPI_BACKEND_URL__
-ENV NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE=__NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE__
-ENV NEXT_PUBLIC_ETL_SERVICE=__NEXT_PUBLIC_ETL_SERVICE__
-ENV NEXT_PUBLIC_ELECTRIC_URL=__NEXT_PUBLIC_ELECTRIC_URL__
-ENV NEXT_PUBLIC_ELECTRIC_AUTH_MODE=__NEXT_PUBLIC_ELECTRIC_AUTH_MODE__
-ENV NEXT_PUBLIC_DEPLOYMENT_MODE=__NEXT_PUBLIC_DEPLOYMENT_MODE__
-
-# Build
-RUN pnpm run build
-
-# ====================
-# Stage 3: Runtime Image
-# ====================
-FROM ubuntu:22.04 AS runtime
-
-# Prevent interactive prompts
-ENV DEBIAN_FRONTEND=noninteractive
-
-# Install system dependencies
-RUN apt-get update && apt-get install -y --no-install-recommends \
- # PostgreSQL
- postgresql-14 \
- postgresql-contrib-14 \
- # Build tools for pgvector
- build-essential \
- postgresql-server-dev-14 \
- git \
- # Redis
- redis-server \
- # Node.js prerequisites
- curl \
- ca-certificates \
- gnupg \
- # Backend dependencies
- gcc \
- wget \
- unzip \
- dos2unix \
- # For PPAs
- software-properties-common \
- # ============================
- # Local TTS (Kokoro) dependencies
- # ============================
- espeak-ng \
- libespeak-ng1 \
- # ============================
- # Local STT (Faster-Whisper) dependencies
- # ============================
- ffmpeg \
- # ============================
- # Audio processing (soundfile)
- # ============================
- libsndfile1 \
- # ============================
- # Image/OpenCV dependencies (for Docling)
- # ============================
- libgl1 \
- libglib2.0-0 \
- libsm6 \
- libxext6 \
- libxrender1 \
- # ============================
- # Playwright browser dependencies
- # ============================
- libnspr4 \
- libnss3 \
- libatk1.0-0 \
- libatk-bridge2.0-0 \
- libcups2 \
- libxkbcommon0 \
- libatspi2.0-0 \
- libxcomposite1 \
- libxdamage1 \
- libxrandr2 \
- libgbm1 \
- libcairo2 \
- libpango-1.0-0 \
- && rm -rf /var/lib/apt/lists/*
-
-# Install Pandoc 3.x from GitHub (apt ships 2.9 which has broken table rendering).
-RUN ARCH=$(dpkg --print-architecture) && \
- wget -qO /tmp/pandoc.deb "https://github.com/jgm/pandoc/releases/download/3.9/pandoc-3.9-1-${ARCH}.deb" && \
- dpkg -i /tmp/pandoc.deb && \
- rm /tmp/pandoc.deb
-
-
-# Install Node.js 20.x (for running frontend)
-RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
- && apt-get install -y nodejs \
- && rm -rf /var/lib/apt/lists/*
-
-# Install Python 3.12 from deadsnakes PPA
-RUN add-apt-repository ppa:deadsnakes/ppa -y \
- && apt-get update \
- && apt-get install -y --no-install-recommends \
- python3.12 \
- python3.12-venv \
- python3.12-dev \
- && rm -rf /var/lib/apt/lists/*
-
-# Set Python 3.12 as default
-RUN update-alternatives --install /usr/bin/python python /usr/bin/python3.12 1 \
- && update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.12 1
-
-# Install pip for Python 3.12
-RUN python3.12 -m ensurepip --upgrade \
- && python3.12 -m pip install --upgrade pip
-
-# Install supervisor via pip (system package incompatible with Python 3.12)
-RUN pip install --no-cache-dir supervisor
-
-# Build and install pgvector
-RUN cd /tmp \
- && git clone --branch v0.7.4 https://github.com/pgvector/pgvector.git \
- && cd pgvector \
- && make \
- && make install \
- && rm -rf /tmp/pgvector
-
-# Update certificates
-RUN update-ca-certificates
-
-# Create data directories
-RUN mkdir -p /data/postgres /data/redis /data/surfsense \
- && chown -R postgres:postgres /data/postgres
-
-# ====================
-# Copy Frontend Build
-# ====================
-WORKDIR /app/frontend
-
-# Copy only the standalone build (not node_modules)
-COPY --from=frontend-builder /app/.next/standalone ./
-COPY --from=frontend-builder /app/.next/static ./.next/static
-COPY --from=frontend-builder /app/public ./public
-
-COPY surfsense_web/content/docs /app/surfsense_web/content/docs
-
-# ====================
-# Copy Electric SQL Release
-# ====================
-COPY --from=electric-builder /app /app/electric-release
-
-# ====================
-# Setup Backend
-# ====================
-WORKDIR /app/backend
-
-# Copy backend dependency files
-COPY surfsense_backend/pyproject.toml surfsense_backend/uv.lock ./
-
-# Install PyTorch CPU-only (Docling needs it but OCR is disabled, no GPU needed)
-RUN pip install --no-cache-dir torch torchvision --index-url https://download.pytorch.org/whl/cpu
-
-# Install python dependencies
-RUN pip install --no-cache-dir certifi pip-system-certs uv \
- && uv pip install --system --no-cache-dir -e .
-
-# Set SSL environment variables
-RUN CERTIFI_PATH=$(python -c "import certifi; print(certifi.where())") \
- && echo "export SSL_CERT_FILE=$CERTIFI_PATH" >> /etc/profile.d/ssl.sh \
- && echo "export REQUESTS_CA_BUNDLE=$CERTIFI_PATH" >> /etc/profile.d/ssl.sh
-
-# Note: EasyOCR models NOT downloaded - OCR is disabled in docling_service.py
-# GPU support will be added in a future :cuda tagged image
-
-# Install Playwright browsers
-RUN pip install --no-cache-dir playwright \
- && playwright install chromium \
- && rm -rf /root/.cache/ms-playwright/ffmpeg*
-
-# Copy backend source
-COPY surfsense_backend/ ./
-
-# ====================
-# Configuration
-# ====================
-WORKDIR /app
-
-# Copy supervisor configuration
-COPY scripts/docker/supervisor-allinone.conf /etc/supervisor/conf.d/surfsense.conf
-
-# Copy entrypoint script
-COPY scripts/docker/entrypoint-allinone.sh /app/entrypoint.sh
-RUN dos2unix /app/entrypoint.sh && chmod +x /app/entrypoint.sh
-
-# PostgreSQL initialization script
-COPY scripts/docker/init-postgres.sh /app/init-postgres.sh
-RUN dos2unix /app/init-postgres.sh && chmod +x /app/init-postgres.sh
-
-# Clean up build dependencies to reduce image size
-RUN apt-get purge -y build-essential postgresql-server-dev-14 \
- && apt-get autoremove -y \
- && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
-
-# Environment variables with defaults
-ENV POSTGRES_USER=surfsense
-ENV POSTGRES_PASSWORD=surfsense
-ENV POSTGRES_DB=surfsense
-ENV DATABASE_URL=postgresql+asyncpg://surfsense:surfsense@localhost:5432/surfsense
-ENV CELERY_BROKER_URL=redis://localhost:6379/0
-ENV CELERY_RESULT_BACKEND=redis://localhost:6379/0
-ENV CELERY_TASK_DEFAULT_QUEUE=surfsense
-ENV PYTHONPATH=/app/backend
-ENV NEXT_FRONTEND_URL=http://localhost:3000
-ENV AUTH_TYPE=LOCAL
-ENV ETL_SERVICE=DOCLING
-ENV EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
-
-# Frontend configuration (can be overridden at runtime)
-# These are injected into the Next.js build at container startup
-ENV NEXT_PUBLIC_FASTAPI_BACKEND_URL=http://localhost:8000
-ENV NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE=LOCAL
-ENV NEXT_PUBLIC_ETL_SERVICE=DOCLING
-
-# Daytona Sandbox (cloud code execution — no local server needed)
-ENV DAYTONA_SANDBOX_ENABLED=FALSE
-# DAYTONA_API_KEY, DAYTONA_API_URL, DAYTONA_TARGET: set at runtime for production.
-
-# Electric SQL configuration (ELECTRIC_DATABASE_URL is built dynamically by entrypoint from these values)
-ENV ELECTRIC_DB_USER=electric
-ENV ELECTRIC_DB_PASSWORD=electric_password
-# Note: ELECTRIC_DATABASE_URL is NOT set here - entrypoint builds it dynamically from ELECTRIC_DB_USER/PASSWORD
-ENV ELECTRIC_INSECURE=true
-ENV ELECTRIC_WRITE_TO_PG_MODE=direct
-ENV ELECTRIC_PORT=5133
-ENV PORT=5133
-ENV NEXT_PUBLIC_ELECTRIC_URL=http://localhost:5133
-ENV NEXT_PUBLIC_ELECTRIC_AUTH_MODE=insecure
-
-# Data volume
-VOLUME ["/data"]
-
-# Expose ports (Frontend: 3000, Backend: 8000, Electric: 5133)
-EXPOSE 3000 8000 5133
-
-# Health check
-HEALTHCHECK --interval=30s --timeout=10s --start-period=120s --retries=3 \
- CMD curl -f http://localhost:3000 || exit 1
-
-# Run entrypoint
-CMD ["/app/entrypoint.sh"]
diff --git a/README.es.md b/README.es.md
index 5472ad069..c2f55f366 100644
--- a/README.es.md
+++ b/README.es.md
@@ -81,13 +81,16 @@ https://github.com/user-attachments/assets/a0a16566-6967-4374-ac51-9b3e07fbecd7
Ejecuta SurfSense en tu propia infraestructura para control total de datos y privacidad.
-**Inicio Rápido (Docker en un solo comando):**
+**Requisitos previos:** [Docker](https://docs.docker.com/get-docker/) (con [Docker Compose](https://docs.docker.com/compose/install/)) debe estar instalado y en ejecución.
+
+> [!NOTE]
+> Usuarios de Windows: instalen [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) primero y ejecuten el siguiente comando en la terminal de Ubuntu.
```bash
-docker run -d -p 3000:3000 -p 8000:8000 -p 5133:5133 -v surfsense-data:/data --name surfsense --restart unless-stopped ghcr.io/modsetter/surfsense:latest
+curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.sh | bash
```
-Después de iniciar, abre [http://localhost:3000](http://localhost:3000) en tu navegador.
+El script de instalación configura [Watchtower](https://github.com/nicholas-fedor/watchtower) automáticamente para actualizaciones diarias. Para omitirlo, agrega la bandera `--no-watchtower`.
Para Docker Compose, instalación manual y otras opciones de despliegue, consulta la [documentación](https://www.surfsense.com/docs/).
diff --git a/README.hi.md b/README.hi.md
index d4388b616..066e01eb7 100644
--- a/README.hi.md
+++ b/README.hi.md
@@ -81,13 +81,16 @@ https://github.com/user-attachments/assets/a0a16566-6967-4374-ac51-9b3e07fbecd7
पूर्ण डेटा नियंत्रण और गोपनीयता के लिए SurfSense को अपने स्वयं के बुनियादी ढांचे पर चलाएं।
-**त्वरित शुरुआत (Docker एक कमांड में):**
+**आवश्यकताएँ:** [Docker](https://docs.docker.com/get-docker/) ([Docker Compose](https://docs.docker.com/compose/install/) सहित) इंस्टॉल और चालू होना चाहिए।
+
+> [!NOTE]
+> Windows उपयोगकर्ता: पहले [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) इंस्टॉल करें और नीचे दिया गया कमांड Ubuntu टर्मिनल में चलाएं।
```bash
-docker run -d -p 3000:3000 -p 8000:8000 -p 5133:5133 -v surfsense-data:/data --name surfsense --restart unless-stopped ghcr.io/modsetter/surfsense:latest
+curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.sh | bash
```
-शुरू करने के बाद, अपने ब्राउज़र में [http://localhost:3000](http://localhost:3000) खोलें।
+इंस्टॉल स्क्रिप्ट दैनिक ऑटो-अपडेट के लिए स्वचालित रूप से [Watchtower](https://github.com/nicholas-fedor/watchtower) सेटअप करती है। इसे छोड़ने के लिए, `--no-watchtower` फ्लैग जोड़ें।
Docker Compose, मैनुअल इंस्टॉलेशन और अन्य डिप्लॉयमेंट विकल्पों के लिए, [डॉक्स](https://www.surfsense.com/docs/) देखें।
diff --git a/README.md b/README.md
index 9056c27f2..7641aa202 100644
--- a/README.md
+++ b/README.md
@@ -81,21 +81,18 @@ https://github.com/user-attachments/assets/a0a16566-6967-4374-ac51-9b3e07fbecd7
Run SurfSense on your own infrastructure for full data control and privacy.
-**Quick Start (Docker one-liner):**
+**Prerequisites:** [Docker](https://docs.docker.com/get-docker/) (with [Docker Compose](https://docs.docker.com/compose/install/)) must be installed and running.
+
+> [!NOTE]
+> Windows users: install [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) first and run the command below in the Ubuntu terminal.
```bash
-docker run -d -p 3000:3000 -p 8000:8000 -p 5133:5133 -v surfsense-data:/data --name surfsense --restart unless-stopped ghcr.io/modsetter/surfsense:latest
+curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.sh | bash
```
-After starting, open [http://localhost:3000](http://localhost:3000) in your browser.
+The install script sets up [Watchtower](https://github.com/nicholas-fedor/watchtower) automatically for daily auto-updates. To skip it, add the `--no-watchtower` flag.
-**Update (Automatic updates with Watchtower):**
-
-```bash
-docker run --rm -v /var/run/docker.sock:/var/run/docker.sock nickfedor/watchtower --run-once surfsense
-```
-
-For Docker Compose, manual installation, and other deployment options, check the [docs](https://www.surfsense.com/docs/).
+For Docker Compose, manual installation, and other deployment options, see the [docs](https://www.surfsense.com/docs/).
### How to Realtime Collaborate (Beta)
diff --git a/README.pt-BR.md b/README.pt-BR.md
index 351cdb85b..d2e45fe5f 100644
--- a/README.pt-BR.md
+++ b/README.pt-BR.md
@@ -81,13 +81,16 @@ https://github.com/user-attachments/assets/a0a16566-6967-4374-ac51-9b3e07fbecd7
Execute o SurfSense na sua própria infraestrutura para controle total de dados e privacidade.
-**Início Rápido (Docker em um único comando):**
+**Pré-requisitos:** [Docker](https://docs.docker.com/get-docker/) (com [Docker Compose](https://docs.docker.com/compose/install/)) deve estar instalado e em execução.
+
+> [!NOTE]
+> Usuários do Windows: instalem o [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) primeiro e executem o comando abaixo no terminal do Ubuntu.
```bash
-docker run -d -p 3000:3000 -p 8000:8000 -p 5133:5133 -v surfsense-data:/data --name surfsense --restart unless-stopped ghcr.io/modsetter/surfsense:latest
+curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.sh | bash
```
-Após iniciar, abra [http://localhost:3000](http://localhost:3000) no seu navegador.
+O script de instalação configura o [Watchtower](https://github.com/nicholas-fedor/watchtower) automaticamente para atualizações diárias. Para pular, adicione a flag `--no-watchtower`.
Para Docker Compose, instalação manual e outras opções de implantação, consulte a [documentação](https://www.surfsense.com/docs/).
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 49ef64619..218252388 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -81,13 +81,16 @@ https://github.com/user-attachments/assets/a0a16566-6967-4374-ac51-9b3e07fbecd7
在您自己的基础设施上运行 SurfSense,实现完全的数据控制和隐私保护。
-**快速开始(Docker 一行命令):**
+**前置条件:** 需要安装并运行 [Docker](https://docs.docker.com/get-docker/)(含 [Docker Compose](https://docs.docker.com/compose/install/))。
+
+> [!NOTE]
+> Windows 用户:请先安装 [WSL](https://learn.microsoft.com/en-us/windows/wsl/install),然后在 Ubuntu 终端中运行以下命令。
```bash
-docker run -d -p 3000:3000 -p 8000:8000 -p 5133:5133 -v surfsense-data:/data --name surfsense --restart unless-stopped ghcr.io/modsetter/surfsense:latest
+curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.sh | bash
```
-启动后,在浏览器中打开 [http://localhost:3000](http://localhost:3000)。
+安装脚本会自动配置 [Watchtower](https://github.com/nicholas-fedor/watchtower) 以实现每日自动更新。如需跳过,请添加 `--no-watchtower` 参数。
如需 Docker Compose、手动安装及其他部署方式,请查看[文档](https://www.surfsense.com/docs/)。
diff --git a/docker-compose.quickstart.yml b/docker-compose.quickstart.yml
deleted file mode 100644
index ff72618b7..000000000
--- a/docker-compose.quickstart.yml
+++ /dev/null
@@ -1,80 +0,0 @@
-# SurfSense Quick Start Docker Compose
-#
-# This is a simplified docker-compose for quick local deployment using pre-built images.
-# For production or customized deployments, use the main docker-compose.yml
-#
-# Usage:
-# 1. (Optional) Create a .env file with your configuration
-# 2. Run: docker compose -f docker-compose.quickstart.yml up -d
-# 3. Access SurfSense at http://localhost:3000
-#
-# All Environment Variables are Optional:
-# - SECRET_KEY: JWT secret key (auto-generated and persisted if not set)
-# - EMBEDDING_MODEL: Embedding model to use (default: sentence-transformers/all-MiniLM-L6-v2)
-# - ETL_SERVICE: Document parsing service - DOCLING, UNSTRUCTURED, or LLAMACLOUD (default: DOCLING)
-# - TTS_SERVICE: Text-to-speech service for podcasts (default: local/kokoro)
-# - STT_SERVICE: Speech-to-text service with model size (default: local/base)
-# - FIRECRAWL_API_KEY: For web crawling features
-
-version: "3.8"
-
-services:
- # All-in-one SurfSense container
- surfsense:
- image: ghcr.io/modsetter/surfsense:latest
- container_name: surfsense
- ports:
- - "${FRONTEND_PORT:-3000}:3000"
- - "${BACKEND_PORT:-8000}:8000"
- volumes:
- - surfsense-data:/data
- environment:
- # Authentication (auto-generated if not set)
- - SECRET_KEY=${SECRET_KEY:-}
-
- # Auth Configuration
- - AUTH_TYPE=${AUTH_TYPE:-LOCAL}
- - GOOGLE_OAUTH_CLIENT_ID=${GOOGLE_OAUTH_CLIENT_ID:-}
- - GOOGLE_OAUTH_CLIENT_SECRET=${GOOGLE_OAUTH_CLIENT_SECRET:-}
-
- # AI/ML Configuration
- - EMBEDDING_MODEL=${EMBEDDING_MODEL:-sentence-transformers/all-MiniLM-L6-v2}
- - RERANKERS_ENABLED=${RERANKERS_ENABLED:-FALSE}
- - RERANKERS_MODEL_NAME=${RERANKERS_MODEL_NAME:-}
- - RERANKERS_MODEL_TYPE=${RERANKERS_MODEL_TYPE:-}
-
- # Document Processing
- - ETL_SERVICE=${ETL_SERVICE:-DOCLING}
- - UNSTRUCTURED_API_KEY=${UNSTRUCTURED_API_KEY:-}
- - LLAMA_CLOUD_API_KEY=${LLAMA_CLOUD_API_KEY:-}
-
- # Audio Services
- - TTS_SERVICE=${TTS_SERVICE:-local/kokoro}
- - TTS_SERVICE_API_KEY=${TTS_SERVICE_API_KEY:-}
- - STT_SERVICE=${STT_SERVICE:-local/base}
- - STT_SERVICE_API_KEY=${STT_SERVICE_API_KEY:-}
-
- # Web Crawling
- - FIRECRAWL_API_KEY=${FIRECRAWL_API_KEY:-}
-
- # Optional Features
- - REGISTRATION_ENABLED=${REGISTRATION_ENABLED:-TRUE}
- - SCHEDULE_CHECKER_INTERVAL=${SCHEDULE_CHECKER_INTERVAL:-1m}
-
- # LangSmith Observability (optional)
- - LANGSMITH_TRACING=${LANGSMITH_TRACING:-false}
- - LANGSMITH_ENDPOINT=${LANGSMITH_ENDPOINT:-}
- - LANGSMITH_API_KEY=${LANGSMITH_API_KEY:-}
- - LANGSMITH_PROJECT=${LANGSMITH_PROJECT:-}
- restart: unless-stopped
- healthcheck:
- test: ["CMD", "curl", "-f", "http://localhost:3000", "&&", "curl", "-f", "http://localhost:8000/docs"]
- interval: 30s
- timeout: 10s
- retries: 3
- start_period: 120s
-
-volumes:
- surfsense-data:
- name: surfsense-data
-
diff --git a/docker-compose.yml b/docker-compose.yml
deleted file mode 100644
index 50abb8548..000000000
--- a/docker-compose.yml
+++ /dev/null
@@ -1,172 +0,0 @@
-version: "3.8"
-
-services:
- db:
- image: ankane/pgvector:latest
- ports:
- - "${POSTGRES_PORT:-5432}:5432"
- volumes:
- - postgres_data:/var/lib/postgresql/data
- - ./scripts/docker/postgresql.conf:/etc/postgresql/postgresql.conf:ro
- - ./scripts/docker/init-electric-user.sh:/docker-entrypoint-initdb.d/init-electric-user.sh:ro
- environment:
- - POSTGRES_USER=${POSTGRES_USER:-postgres}
- - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgres}
- - POSTGRES_DB=${POSTGRES_DB:-surfsense}
- - ELECTRIC_DB_USER=${ELECTRIC_DB_USER:-electric}
- - ELECTRIC_DB_PASSWORD=${ELECTRIC_DB_PASSWORD:-electric_password}
- command: postgres -c config_file=/etc/postgresql/postgresql.conf
-
- pgadmin:
- image: dpage/pgadmin4
- ports:
- - "${PGADMIN_PORT:-5050}:80"
- environment:
- - PGADMIN_DEFAULT_EMAIL=${PGADMIN_DEFAULT_EMAIL:-admin@surfsense.com}
- - PGADMIN_DEFAULT_PASSWORD=${PGADMIN_DEFAULT_PASSWORD:-surfsense}
- volumes:
- - pgadmin_data:/var/lib/pgadmin
- depends_on:
- - db
-
- redis:
- image: redis:7-alpine
- ports:
- - "${REDIS_PORT:-6379}:6379"
- volumes:
- - redis_data:/data
- command: redis-server --appendonly yes
-
- backend:
- build: ./surfsense_backend
- # image: ghcr.io/modsetter/surfsense_backend:latest
- ports:
- - "${BACKEND_PORT:-8000}:8000"
- volumes:
- - ./surfsense_backend/app:/app/app
- - shared_temp:/tmp
- # Uncomment and edit the line below to enable Obsidian vault indexing
- # - /path/to/your/obsidian/vault:/obsidian-vault:ro
- env_file:
- - ./surfsense_backend/.env
- environment:
- - DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-surfsense}
- - CELERY_BROKER_URL=redis://redis:${REDIS_PORT:-6379}/0
- - CELERY_RESULT_BACKEND=redis://redis:${REDIS_PORT:-6379}/0
- - REDIS_APP_URL=redis://redis:${REDIS_PORT:-6379}/0
- # Queue name isolation - prevents task collision if Redis is shared with other apps
- - CELERY_TASK_DEFAULT_QUEUE=surfsense
- - PYTHONPATH=/app
- - UVICORN_LOOP=asyncio
- - UNSTRUCTURED_HAS_PATCHED_LOOP=1
- - LANGCHAIN_TRACING_V2=false
- - LANGSMITH_TRACING=false
- - ELECTRIC_DB_USER=${ELECTRIC_DB_USER:-electric}
- - ELECTRIC_DB_PASSWORD=${ELECTRIC_DB_PASSWORD:-electric_password}
- - AUTH_TYPE=${AUTH_TYPE:-LOCAL}
- - NEXT_FRONTEND_URL=${NEXT_FRONTEND_URL:-http://localhost:3000}
- # Daytona Sandbox – uncomment and set credentials to enable cloud code execution
- # - DAYTONA_SANDBOX_ENABLED=TRUE
- # - DAYTONA_API_KEY=${DAYTONA_API_KEY:-}
- # - DAYTONA_API_URL=${DAYTONA_API_URL:-https://app.daytona.io/api}
- # - DAYTONA_TARGET=${DAYTONA_TARGET:-us}
- depends_on:
- - db
- - redis
-
- # Run these services separately in production
- # celery_worker:
- # build: ./surfsense_backend
- # # image: ghcr.io/modsetter/surfsense_backend:latest
- # command: celery -A app.celery_app worker --loglevel=info --concurrency=1 --pool=solo
- # volumes:
- # - ./surfsense_backend:/app
- # - shared_temp:/tmp
- # env_file:
- # - ./surfsense_backend/.env
- # environment:
- # - DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-surfsense}
- # - CELERY_BROKER_URL=redis://redis:${REDIS_PORT:-6379}/0
- # - CELERY_RESULT_BACKEND=redis://redis:${REDIS_PORT:-6379}/0
- # - PYTHONPATH=/app
- # depends_on:
- # - db
- # - redis
- # - backend
-
- # celery_beat:
- # build: ./surfsense_backend
- # # image: ghcr.io/modsetter/surfsense_backend:latest
- # command: celery -A app.celery_app beat --loglevel=info
- # volumes:
- # - ./surfsense_backend:/app
- # - shared_temp:/tmp
- # env_file:
- # - ./surfsense_backend/.env
- # environment:
- # - DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-surfsense}
- # - CELERY_BROKER_URL=redis://redis:${REDIS_PORT:-6379}/0
- # - CELERY_RESULT_BACKEND=redis://redis:${REDIS_PORT:-6379}/0
- # - PYTHONPATH=/app
- # depends_on:
- # - db
- # - redis
- # - celery_worker
-
- # flower:
- # build: ./surfsense_backend
- # # image: ghcr.io/modsetter/surfsense_backend:latest
- # command: celery -A app.celery_app flower --port=5555
- # ports:
- # - "${FLOWER_PORT:-5555}:5555"
- # env_file:
- # - ./surfsense_backend/.env
- # environment:
- # - CELERY_BROKER_URL=redis://redis:${REDIS_PORT:-6379}/0
- # - CELERY_RESULT_BACKEND=redis://redis:${REDIS_PORT:-6379}/0
- # - PYTHONPATH=/app
- # depends_on:
- # - redis
- # - celery_worker
-
- electric:
- image: electricsql/electric:latest
- ports:
- - "${ELECTRIC_PORT:-5133}:3000"
- environment:
- - DATABASE_URL=${ELECTRIC_DATABASE_URL:-postgresql://${ELECTRIC_DB_USER:-electric}:${ELECTRIC_DB_PASSWORD:-electric_password}@${POSTGRES_HOST:-db}:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-surfsense}?sslmode=disable}
- - ELECTRIC_INSECURE=true
- - ELECTRIC_WRITE_TO_PG_MODE=direct
- restart: unless-stopped
- # depends_on:
- # - db
- healthcheck:
- test: ["CMD", "curl", "-f", "http://localhost:3000/v1/health"]
- interval: 10s
- timeout: 5s
- retries: 5
-
- frontend:
- build:
- context: ./surfsense_web
- # image: ghcr.io/modsetter/surfsense_ui:latest
- args:
- NEXT_PUBLIC_FASTAPI_BACKEND_URL: ${NEXT_PUBLIC_FASTAPI_BACKEND_URL:-http://localhost:8000}
- NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE: ${NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE:-LOCAL}
- NEXT_PUBLIC_ETL_SERVICE: ${NEXT_PUBLIC_ETL_SERVICE:-DOCLING}
- ports:
- - "${FRONTEND_PORT:-3000}:3000"
- env_file:
- - ./surfsense_web/.env
- environment:
- - NEXT_PUBLIC_ELECTRIC_URL=${NEXT_PUBLIC_ELECTRIC_URL:-http://localhost:5133}
- - NEXT_PUBLIC_ELECTRIC_AUTH_MODE=insecure
- depends_on:
- - backend
- - electric
-
-volumes:
- postgres_data:
- pgadmin_data:
- redis_data:
- shared_temp:
diff --git a/docker/.env.example b/docker/.env.example
new file mode 100644
index 000000000..7025cac52
--- /dev/null
+++ b/docker/.env.example
@@ -0,0 +1,256 @@
+# ==============================================================================
+# SurfSense Docker Configuration
+# ==============================================================================
+# Database, Redis, and internal service wiring are handled automatically.
+# ==============================================================================
+
+# SurfSense version (use "latest", a clean version like "0.0.14", or a specific build like "0.0.14.1")
+SURFSENSE_VERSION=latest
+
+# ------------------------------------------------------------------------------
+# Core Settings
+# ------------------------------------------------------------------------------
+
+# REQUIRED: Generate a secret key with: openssl rand -base64 32
+SECRET_KEY=replace_me_with_a_random_string
+
+# Auth type: LOCAL (email/password) or GOOGLE (OAuth)
+AUTH_TYPE=LOCAL
+
+# Allow new user registrations (TRUE or FALSE)
+# REGISTRATION_ENABLED=TRUE
+
+# Document parsing service: DOCLING, UNSTRUCTURED, or LLAMACLOUD
+ETL_SERVICE=DOCLING
+
+# Embedding model for vector search
+# Local: sentence-transformers/all-MiniLM-L6-v2
+# OpenAI: openai://text-embedding-ada-002 (set OPENAI_API_KEY below)
+# Cohere: cohere://embed-english-light-v3.0 (set COHERE_API_KEY below)
+EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
+
+# ------------------------------------------------------------------------------
+# Ports (change to avoid conflicts with other services on your machine)
+# ------------------------------------------------------------------------------
+
+# BACKEND_PORT=8000
+# FRONTEND_PORT=3000
+# ELECTRIC_PORT=5133
+# FLOWER_PORT=5555
+
+# ==============================================================================
+# DEV COMPOSE ONLY (docker-compose.dev.yml)
+# You only need them only if you are running `docker-compose.dev.yml`.
+# ==============================================================================
+
+# -- pgAdmin (database GUI) --
+# PGADMIN_PORT=5050
+# PGADMIN_DEFAULT_EMAIL=admin@surfsense.com
+# PGADMIN_DEFAULT_PASSWORD=surfsense
+
+# -- Redis exposed port (dev only; Redis is internal-only in prod) --
+# REDIS_PORT=6379
+
+# -- Frontend Build Args --
+# In dev, the frontend is built from source and these are passed as build args.
+# In prod, they are automatically derived from AUTH_TYPE, ETL_SERVICE, and the port settings above.
+# NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE=LOCAL
+# NEXT_PUBLIC_ETL_SERVICE=DOCLING
+# NEXT_PUBLIC_DEPLOYMENT_MODE=self-hosted
+# NEXT_PUBLIC_ELECTRIC_AUTH_MODE=insecure
+
+# ------------------------------------------------------------------------------
+# Custom Domain / Reverse Proxy
+# ------------------------------------------------------------------------------
+# ONLY set these if you are serving SurfSense on a real domain via a reverse
+# proxy (e.g. Caddy, Nginx, Cloudflare Tunnel).
+# For standard localhost deployments, leave all of these commented out —
+# they are automatically derived from the port settings above.
+#
+# NEXT_FRONTEND_URL=https://app.yourdomain.com
+# BACKEND_URL=https://api.yourdomain.com
+# NEXT_PUBLIC_FASTAPI_BACKEND_URL=https://api.yourdomain.com
+# NEXT_PUBLIC_ELECTRIC_URL=https://electric.yourdomain.com
+
+
+# ------------------------------------------------------------------------------
+# Database (defaults work out of the box, change for security)
+# ------------------------------------------------------------------------------
+
+# DB_USER=surfsense
+# DB_PASSWORD=surfsense
+# DB_NAME=surfsense
+# DB_HOST=db
+# DB_PORT=5432
+
+# SSL mode for database connections: disable, require, verify-ca, verify-full
+# DB_SSLMODE=disable
+
+# Full DATABASE_URL override — when set, takes precedence over the individual
+# DB_USER / DB_PASSWORD / DB_NAME / DB_HOST / DB_PORT settings above.
+# Use this for managed databases (AWS RDS, GCP Cloud SQL, Supabase, etc.)
+# DATABASE_URL=postgresql+asyncpg://user:password@your-rds-host:5432/surfsense?sslmode=require
+
+# ------------------------------------------------------------------------------
+# Redis (defaults work out of the box)
+# ------------------------------------------------------------------------------
+# Full Redis URL override for Celery broker, result backend, and app cache.
+# Use this for managed Redis (AWS ElastiCache, Redis Cloud, etc.)
+# Supports auth: redis://:password@host:port/0
+# Supports TLS: rediss://:password@host:6380/0
+# REDIS_URL=redis://redis:6379/0
+
+# ------------------------------------------------------------------------------
+# Electric SQL (real-time sync credentials)
+# ------------------------------------------------------------------------------
+# These must match on the db, backend, and electric services.
+# Change for security; defaults work out of the box.
+
+# ELECTRIC_DB_USER=electric
+# ELECTRIC_DB_PASSWORD=electric_password
+# Full override for the Electric → Postgres connection URL.
+# Leave commented out to use the Docker-managed `db` container (default).
+# Uncomment and set `db` to `host.docker.internal` when pointing Electric at a local Postgres instance (e.g. Postgres.app on macOS):
+# ELECTRIC_DATABASE_URL=postgresql://electric:electric_password@db:5432/surfsense?sslmode=disable
+
+# ------------------------------------------------------------------------------
+# TTS & STT (Text-to-Speech / Speech-to-Text)
+# ------------------------------------------------------------------------------
+
+# Local Kokoro TTS (default) or LiteLLM provider
+TTS_SERVICE=local/kokoro
+# TTS_SERVICE_API_KEY=
+# TTS_SERVICE_API_BASE=
+
+# Local Faster-Whisper STT: local/MODEL_SIZE (tiny, base, small, medium, large-v3)
+STT_SERVICE=local/base
+# Or use LiteLLM: openai/whisper-1
+# STT_SERVICE_API_KEY=
+# STT_SERVICE_API_BASE=
+
+# ------------------------------------------------------------------------------
+# Rerankers (optional, disabled by default)
+# ------------------------------------------------------------------------------
+
+# RERANKERS_ENABLED=TRUE
+# RERANKERS_MODEL_NAME=ms-marco-MiniLM-L-12-v2
+# RERANKERS_MODEL_TYPE=flashrank
+
+# ------------------------------------------------------------------------------
+# Google OAuth (only if AUTH_TYPE=GOOGLE)
+# ------------------------------------------------------------------------------
+
+# GOOGLE_OAUTH_CLIENT_ID=
+# GOOGLE_OAUTH_CLIENT_SECRET=
+
+# ------------------------------------------------------------------------------
+# Connector OAuth Keys (uncomment connectors you want to use)
+# ------------------------------------------------------------------------------
+
+# -- Google Connectors --
+# GOOGLE_CALENDAR_REDIRECT_URI=http://localhost:8000/api/v1/auth/google/calendar/connector/callback
+# GOOGLE_GMAIL_REDIRECT_URI=http://localhost:8000/api/v1/auth/google/gmail/connector/callback
+# GOOGLE_DRIVE_REDIRECT_URI=http://localhost:8000/api/v1/auth/google/drive/connector/callback
+
+# -- Notion --
+# NOTION_CLIENT_ID=
+# NOTION_CLIENT_SECRET=
+# NOTION_REDIRECT_URI=http://localhost:8000/api/v1/auth/notion/connector/callback
+
+# -- Slack --
+# SLACK_CLIENT_ID=
+# SLACK_CLIENT_SECRET=
+# SLACK_REDIRECT_URI=http://localhost:8000/api/v1/auth/slack/connector/callback
+
+# -- Discord --
+# DISCORD_CLIENT_ID=
+# DISCORD_CLIENT_SECRET=
+# DISCORD_REDIRECT_URI=http://localhost:8000/api/v1/auth/discord/connector/callback
+# DISCORD_BOT_TOKEN=
+
+# -- Atlassian (Jira & Confluence) --
+# ATLASSIAN_CLIENT_ID=
+# ATLASSIAN_CLIENT_SECRET=
+# JIRA_REDIRECT_URI=http://localhost:8000/api/v1/auth/jira/connector/callback
+# CONFLUENCE_REDIRECT_URI=http://localhost:8000/api/v1/auth/confluence/connector/callback
+
+# -- Linear --
+# LINEAR_CLIENT_ID=
+# LINEAR_CLIENT_SECRET=
+# LINEAR_REDIRECT_URI=http://localhost:8000/api/v1/auth/linear/connector/callback
+
+# -- ClickUp --
+# CLICKUP_CLIENT_ID=
+# CLICKUP_CLIENT_SECRET=
+# CLICKUP_REDIRECT_URI=http://localhost:8000/api/v1/auth/clickup/connector/callback
+
+# -- Airtable --
+# AIRTABLE_CLIENT_ID=
+# AIRTABLE_CLIENT_SECRET=
+# AIRTABLE_REDIRECT_URI=http://localhost:8000/api/v1/auth/airtable/connector/callback
+
+# -- Microsoft Teams --
+# TEAMS_CLIENT_ID=
+# TEAMS_CLIENT_SECRET=
+# TEAMS_REDIRECT_URI=http://localhost:8000/api/v1/auth/teams/connector/callback
+
+# -- Composio --
+# COMPOSIO_API_KEY=
+# COMPOSIO_ENABLED=TRUE
+# COMPOSIO_REDIRECT_URI=http://localhost:8000/api/v1/auth/composio/connector/callback
+
+# ------------------------------------------------------------------------------
+# Daytona Sandbox (optional — cloud code execution for the deep agent)
+# ------------------------------------------------------------------------------
+# Set DAYTONA_SANDBOX_ENABLED=TRUE and provide credentials to give the agent
+# an isolated code execution environment via the Daytona cloud API.
+# DAYTONA_SANDBOX_ENABLED=FALSE
+# DAYTONA_API_KEY=
+# DAYTONA_API_URL=https://app.daytona.io/api
+# DAYTONA_TARGET=us
+
+# ------------------------------------------------------------------------------
+# External API Keys (optional)
+# ------------------------------------------------------------------------------
+
+# Firecrawl (web scraping)
+# FIRECRAWL_API_KEY=
+
+# Unstructured (if ETL_SERVICE=UNSTRUCTURED)
+# UNSTRUCTURED_API_KEY=
+
+# LlamaCloud (if ETL_SERVICE=LLAMACLOUD)
+# LLAMA_CLOUD_API_KEY=
+
+# ------------------------------------------------------------------------------
+# Observability (optional)
+# ------------------------------------------------------------------------------
+
+# LANGSMITH_TRACING=true
+# LANGSMITH_ENDPOINT=https://api.smith.langchain.com
+# LANGSMITH_API_KEY=
+# LANGSMITH_PROJECT=surfsense
+
+# ------------------------------------------------------------------------------
+# Advanced (optional)
+# ------------------------------------------------------------------------------
+
+# Periodic connector sync interval (default: 5m)
+# SCHEDULE_CHECKER_INTERVAL=5m
+
+# JWT token lifetimes
+# ACCESS_TOKEN_LIFETIME_SECONDS=86400
+# REFRESH_TOKEN_LIFETIME_SECONDS=1209600
+
+# Pages limit per user for ETL (default: unlimited)
+# PAGES_LIMIT=500
+
+# Connector indexing lock TTL in seconds (default: 28800 = 8 hours)
+# CONNECTOR_INDEXING_LOCK_TTL_SECONDS=28800
+
+# Residential proxy for web crawling
+# RESIDENTIAL_PROXY_USERNAME=
+# RESIDENTIAL_PROXY_PASSWORD=
+# RESIDENTIAL_PROXY_HOSTNAME=
+# RESIDENTIAL_PROXY_LOCATION=
+# RESIDENTIAL_PROXY_TYPE=1
diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml
new file mode 100644
index 000000000..b76f26b2d
--- /dev/null
+++ b/docker/docker-compose.dev.yml
@@ -0,0 +1,206 @@
+# =============================================================================
+# SurfSense — Development Docker Compose
+# =============================================================================
+# Usage (from repo root):
+# docker compose -f docker/docker-compose.dev.yml up --build
+#
+# This file builds from source and includes dev tools like pgAdmin.
+# For production with prebuilt images, use docker/docker-compose.yml instead.
+# =============================================================================
+
+name: surfsense
+
+services:
+ db:
+ image: pgvector/pgvector:pg17
+ ports:
+ - "${POSTGRES_PORT:-5432}:5432"
+ volumes:
+ - postgres_data:/var/lib/postgresql/data
+ - ./postgresql.conf:/etc/postgresql/postgresql.conf:ro
+ - ./scripts/init-electric-user.sh:/docker-entrypoint-initdb.d/init-electric-user.sh:ro
+ environment:
+ - POSTGRES_USER=${DB_USER:-postgres}
+ - POSTGRES_PASSWORD=${DB_PASSWORD:-postgres}
+ - POSTGRES_DB=${DB_NAME:-surfsense}
+ - ELECTRIC_DB_USER=${ELECTRIC_DB_USER:-electric}
+ - ELECTRIC_DB_PASSWORD=${ELECTRIC_DB_PASSWORD:-electric_password}
+ command: postgres -c config_file=/etc/postgresql/postgresql.conf
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-surfsense}"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+
+ pgadmin:
+ image: dpage/pgadmin4
+ ports:
+ - "${PGADMIN_PORT:-5050}:80"
+ environment:
+ - PGADMIN_DEFAULT_EMAIL=${PGADMIN_DEFAULT_EMAIL:-admin@surfsense.com}
+ - PGADMIN_DEFAULT_PASSWORD=${PGADMIN_DEFAULT_PASSWORD:-surfsense}
+ volumes:
+ - pgadmin_data:/var/lib/pgadmin
+ depends_on:
+ - db
+
+ redis:
+ image: redis:8-alpine
+ ports:
+ - "${REDIS_PORT:-6379}:6379"
+ volumes:
+ - redis_data:/data
+ command: redis-server --appendonly yes
+ healthcheck:
+ test: ["CMD", "redis-cli", "ping"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+
+ backend:
+ build: ../surfsense_backend
+ ports:
+ - "${BACKEND_PORT:-8000}:8000"
+ volumes:
+ - ../surfsense_backend/app:/app/app
+ - shared_temp:/shared_tmp
+ env_file:
+ - ../surfsense_backend/.env
+ environment:
+ - DATABASE_URL=${DATABASE_URL:-postgresql+asyncpg://${DB_USER:-postgres}:${DB_PASSWORD:-postgres}@${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-surfsense}}
+ - CELERY_BROKER_URL=${REDIS_URL:-redis://redis:6379/0}
+ - CELERY_RESULT_BACKEND=${REDIS_URL:-redis://redis:6379/0}
+ - REDIS_APP_URL=${REDIS_URL:-redis://redis:6379/0}
+ - CELERY_TASK_DEFAULT_QUEUE=surfsense
+ - PYTHONPATH=/app
+ - UVICORN_LOOP=asyncio
+ - UNSTRUCTURED_HAS_PATCHED_LOOP=1
+ - LANGCHAIN_TRACING_V2=false
+ - LANGSMITH_TRACING=false
+ - ELECTRIC_DB_USER=${ELECTRIC_DB_USER:-electric}
+ - ELECTRIC_DB_PASSWORD=${ELECTRIC_DB_PASSWORD:-electric_password}
+ - AUTH_TYPE=${AUTH_TYPE:-LOCAL}
+ - NEXT_FRONTEND_URL=${NEXT_FRONTEND_URL:-http://localhost:3000}
+ # Daytona Sandbox – uncomment and set credentials to enable cloud code execution
+ # - DAYTONA_SANDBOX_ENABLED=TRUE
+ # - DAYTONA_API_KEY=${DAYTONA_API_KEY:-}
+ # - DAYTONA_API_URL=${DAYTONA_API_URL:-https://app.daytona.io/api}
+ # - DAYTONA_TARGET=${DAYTONA_TARGET:-us}
+ - SERVICE_ROLE=api
+ depends_on:
+ db:
+ condition: service_healthy
+ redis:
+ condition: service_healthy
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
+ interval: 15s
+ timeout: 5s
+ retries: 30
+ start_period: 200s
+
+ celery_worker:
+ build: ../surfsense_backend
+ volumes:
+ - ../surfsense_backend/app:/app/app
+ - shared_temp:/shared_tmp
+ env_file:
+ - ../surfsense_backend/.env
+ environment:
+ - DATABASE_URL=${DATABASE_URL:-postgresql+asyncpg://${DB_USER:-postgres}:${DB_PASSWORD:-postgres}@${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-surfsense}}
+ - CELERY_BROKER_URL=${REDIS_URL:-redis://redis:6379/0}
+ - CELERY_RESULT_BACKEND=${REDIS_URL:-redis://redis:6379/0}
+ - REDIS_APP_URL=${REDIS_URL:-redis://redis:6379/0}
+ - CELERY_TASK_DEFAULT_QUEUE=surfsense
+ - PYTHONPATH=/app
+ - ELECTRIC_DB_USER=${ELECTRIC_DB_USER:-electric}
+ - ELECTRIC_DB_PASSWORD=${ELECTRIC_DB_PASSWORD:-electric_password}
+ - SERVICE_ROLE=worker
+ depends_on:
+ db:
+ condition: service_healthy
+ redis:
+ condition: service_healthy
+ backend:
+ condition: service_healthy
+
+ celery_beat:
+ build: ../surfsense_backend
+ env_file:
+ - ../surfsense_backend/.env
+ environment:
+ - DATABASE_URL=${DATABASE_URL:-postgresql+asyncpg://${DB_USER:-postgres}:${DB_PASSWORD:-postgres}@${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-surfsense}}
+ - CELERY_BROKER_URL=${REDIS_URL:-redis://redis:6379/0}
+ - CELERY_RESULT_BACKEND=${REDIS_URL:-redis://redis:6379/0}
+ - CELERY_TASK_DEFAULT_QUEUE=surfsense
+ - PYTHONPATH=/app
+ - SERVICE_ROLE=beat
+ depends_on:
+ db:
+ condition: service_healthy
+ redis:
+ condition: service_healthy
+ celery_worker:
+ condition: service_started
+
+ # flower:
+ # build: ../surfsense_backend
+ # ports:
+ # - "${FLOWER_PORT:-5555}:5555"
+ # env_file:
+ # - ../surfsense_backend/.env
+ # environment:
+ # - CELERY_BROKER_URL=${REDIS_URL:-redis://redis:6379/0}
+ # - CELERY_RESULT_BACKEND=${REDIS_URL:-redis://redis:6379/0}
+ # - PYTHONPATH=/app
+ # command: celery -A app.celery_app flower --port=5555
+ # depends_on:
+ # - redis
+ # - celery_worker
+
+ electric:
+ image: electricsql/electric:1.4.10
+ ports:
+ - "${ELECTRIC_PORT:-5133}:3000"
+ # depends_on:
+ # - db
+ environment:
+ - DATABASE_URL=${ELECTRIC_DATABASE_URL:-postgresql://${ELECTRIC_DB_USER:-electric}:${ELECTRIC_DB_PASSWORD:-electric_password}@${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-surfsense}?sslmode=${DB_SSLMODE:-disable}}
+ - ELECTRIC_INSECURE=true
+ - ELECTRIC_WRITE_TO_PG_MODE=direct
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:3000/v1/health"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+
+ frontend:
+ build:
+ context: ../surfsense_web
+ args:
+ NEXT_PUBLIC_FASTAPI_BACKEND_URL: ${NEXT_PUBLIC_FASTAPI_BACKEND_URL:-http://localhost:8000}
+ NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE: ${NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE:-LOCAL}
+ NEXT_PUBLIC_ETL_SERVICE: ${NEXT_PUBLIC_ETL_SERVICE:-DOCLING}
+ NEXT_PUBLIC_ELECTRIC_URL: ${NEXT_PUBLIC_ELECTRIC_URL:-http://localhost:5133}
+ NEXT_PUBLIC_ELECTRIC_AUTH_MODE: ${NEXT_PUBLIC_ELECTRIC_AUTH_MODE:-insecure}
+ NEXT_PUBLIC_DEPLOYMENT_MODE: ${NEXT_PUBLIC_DEPLOYMENT_MODE:-self-hosted}
+ ports:
+ - "${FRONTEND_PORT:-3000}:3000"
+ env_file:
+ - ../surfsense_web/.env
+ depends_on:
+ backend:
+ condition: service_healthy
+ electric:
+ condition: service_healthy
+
+volumes:
+ postgres_data:
+ name: surfsense-postgres
+ pgadmin_data:
+ name: surfsense-pgadmin
+ redis_data:
+ name: surfsense-redis
+ shared_temp:
+ name: surfsense-shared-temp
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
new file mode 100644
index 000000000..9fca4dfb5
--- /dev/null
+++ b/docker/docker-compose.yml
@@ -0,0 +1,195 @@
+# =============================================================================
+# SurfSense — Production Docker Compose
+# Docs: https://docs.surfsense.com/docs/docker-installation
+# =============================================================================
+# Usage:
+# 1. Copy .env.example to .env and edit the required values
+# 2. docker compose up -d
+# =============================================================================
+
+name: surfsense
+
+services:
+ db:
+ image: pgvector/pgvector:pg17
+ volumes:
+ - postgres_data:/var/lib/postgresql/data
+ - ./postgresql.conf:/etc/postgresql/postgresql.conf:ro
+ - ./scripts/init-electric-user.sh:/docker-entrypoint-initdb.d/init-electric-user.sh:ro
+ environment:
+ POSTGRES_USER: ${DB_USER:-surfsense}
+ POSTGRES_PASSWORD: ${DB_PASSWORD:-surfsense}
+ POSTGRES_DB: ${DB_NAME:-surfsense}
+ ELECTRIC_DB_USER: ${ELECTRIC_DB_USER:-electric}
+ ELECTRIC_DB_PASSWORD: ${ELECTRIC_DB_PASSWORD:-electric_password}
+ command: postgres -c config_file=/etc/postgresql/postgresql.conf
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-surfsense} -d ${DB_NAME:-surfsense}"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+
+ redis:
+ image: redis:8-alpine
+ volumes:
+ - redis_data:/data
+ command: redis-server --appendonly yes
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD", "redis-cli", "ping"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+
+ backend:
+ image: ghcr.io/modsetter/surfsense-backend:${SURFSENSE_VERSION:-latest}
+ ports:
+ - "${BACKEND_PORT:-8000}:8000"
+ volumes:
+ - shared_temp:/shared_tmp
+ env_file:
+ - .env
+ environment:
+ DATABASE_URL: ${DATABASE_URL:-postgresql+asyncpg://${DB_USER:-surfsense}:${DB_PASSWORD:-surfsense}@${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-surfsense}}
+ CELERY_BROKER_URL: ${REDIS_URL:-redis://redis:6379/0}
+ CELERY_RESULT_BACKEND: ${REDIS_URL:-redis://redis:6379/0}
+ REDIS_APP_URL: ${REDIS_URL:-redis://redis:6379/0}
+ CELERY_TASK_DEFAULT_QUEUE: surfsense
+ PYTHONPATH: /app
+ UVICORN_LOOP: asyncio
+ UNSTRUCTURED_HAS_PATCHED_LOOP: "1"
+ ELECTRIC_DB_USER: ${ELECTRIC_DB_USER:-electric}
+ ELECTRIC_DB_PASSWORD: ${ELECTRIC_DB_PASSWORD:-electric_password}
+ NEXT_FRONTEND_URL: ${NEXT_FRONTEND_URL:-http://localhost:${FRONTEND_PORT:-3000}}
+ # Daytona Sandbox – uncomment and set credentials to enable cloud code execution
+ # DAYTONA_SANDBOX_ENABLED: "TRUE"
+ # DAYTONA_API_KEY: ${DAYTONA_API_KEY:-}
+ # DAYTONA_API_URL: ${DAYTONA_API_URL:-https://app.daytona.io/api}
+ # DAYTONA_TARGET: ${DAYTONA_TARGET:-us}
+ SERVICE_ROLE: api
+ labels:
+ - "com.centurylinklabs.watchtower.enable=true"
+ depends_on:
+ db:
+ condition: service_healthy
+ redis:
+ condition: service_healthy
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
+ interval: 15s
+ timeout: 5s
+ retries: 30
+ start_period: 200s
+
+ celery_worker:
+ image: ghcr.io/modsetter/surfsense-backend:${SURFSENSE_VERSION:-latest}
+ volumes:
+ - shared_temp:/shared_tmp
+ env_file:
+ - .env
+ environment:
+ DATABASE_URL: ${DATABASE_URL:-postgresql+asyncpg://${DB_USER:-surfsense}:${DB_PASSWORD:-surfsense}@${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-surfsense}}
+ CELERY_BROKER_URL: ${REDIS_URL:-redis://redis:6379/0}
+ CELERY_RESULT_BACKEND: ${REDIS_URL:-redis://redis:6379/0}
+ REDIS_APP_URL: ${REDIS_URL:-redis://redis:6379/0}
+ CELERY_TASK_DEFAULT_QUEUE: surfsense
+ PYTHONPATH: /app
+ ELECTRIC_DB_USER: ${ELECTRIC_DB_USER:-electric}
+ ELECTRIC_DB_PASSWORD: ${ELECTRIC_DB_PASSWORD:-electric_password}
+ SERVICE_ROLE: worker
+ depends_on:
+ db:
+ condition: service_healthy
+ redis:
+ condition: service_healthy
+ backend:
+ condition: service_healthy
+ labels:
+ - "com.centurylinklabs.watchtower.enable=true"
+ restart: unless-stopped
+
+ celery_beat:
+ image: ghcr.io/modsetter/surfsense-backend:${SURFSENSE_VERSION:-latest}
+ env_file:
+ - .env
+ environment:
+ DATABASE_URL: ${DATABASE_URL:-postgresql+asyncpg://${DB_USER:-surfsense}:${DB_PASSWORD:-surfsense}@${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-surfsense}}
+ CELERY_BROKER_URL: ${REDIS_URL:-redis://redis:6379/0}
+ CELERY_RESULT_BACKEND: ${REDIS_URL:-redis://redis:6379/0}
+ CELERY_TASK_DEFAULT_QUEUE: surfsense
+ PYTHONPATH: /app
+ SERVICE_ROLE: beat
+ depends_on:
+ db:
+ condition: service_healthy
+ redis:
+ condition: service_healthy
+ celery_worker:
+ condition: service_started
+ labels:
+ - "com.centurylinklabs.watchtower.enable=true"
+ restart: unless-stopped
+
+ # flower:
+ # image: ghcr.io/modsetter/surfsense-backend:${SURFSENSE_VERSION:-latest}
+ # ports:
+ # - "${FLOWER_PORT:-5555}:5555"
+ # env_file:
+ # - .env
+ # environment:
+ # CELERY_BROKER_URL: ${REDIS_URL:-redis://redis:6379/0}
+ # CELERY_RESULT_BACKEND: ${REDIS_URL:-redis://redis:6379/0}
+ # PYTHONPATH: /app
+ # command: celery -A app.celery_app flower --port=5555
+ # depends_on:
+ # - redis
+ # - celery_worker
+ # restart: unless-stopped
+
+ electric:
+ image: electricsql/electric:1.4.10
+ ports:
+ - "${ELECTRIC_PORT:-5133}:3000"
+ environment:
+ DATABASE_URL: ${ELECTRIC_DATABASE_URL:-postgresql://${ELECTRIC_DB_USER:-electric}:${ELECTRIC_DB_PASSWORD:-electric_password}@${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-surfsense}?sslmode=${DB_SSLMODE:-disable}}
+ ELECTRIC_INSECURE: "true"
+ ELECTRIC_WRITE_TO_PG_MODE: direct
+ restart: unless-stopped
+ depends_on:
+ db:
+ condition: service_healthy
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:3000/v1/health"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+
+ frontend:
+ image: ghcr.io/modsetter/surfsense-web:${SURFSENSE_VERSION:-latest}
+ ports:
+ - "${FRONTEND_PORT:-3000}:3000"
+ environment:
+ NEXT_PUBLIC_FASTAPI_BACKEND_URL: ${NEXT_PUBLIC_FASTAPI_BACKEND_URL:-http://localhost:${BACKEND_PORT:-8000}}
+ NEXT_PUBLIC_ELECTRIC_URL: ${NEXT_PUBLIC_ELECTRIC_URL:-http://localhost:${ELECTRIC_PORT:-5133}}
+ NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE: ${AUTH_TYPE:-LOCAL}
+ NEXT_PUBLIC_ETL_SERVICE: ${ETL_SERVICE:-DOCLING}
+ NEXT_PUBLIC_DEPLOYMENT_MODE: ${DEPLOYMENT_MODE:-self-hosted}
+ NEXT_PUBLIC_ELECTRIC_AUTH_MODE: ${NEXT_PUBLIC_ELECTRIC_AUTH_MODE:-insecure}
+ labels:
+ - "com.centurylinklabs.watchtower.enable=true"
+ depends_on:
+ backend:
+ condition: service_healthy
+ electric:
+ condition: service_healthy
+ restart: unless-stopped
+
+volumes:
+ postgres_data:
+ name: surfsense-postgres
+ redis_data:
+ name: surfsense-redis
+ shared_temp:
+ name: surfsense-shared-temp
diff --git a/scripts/docker/postgresql.conf b/docker/postgresql.conf
similarity index 100%
rename from scripts/docker/postgresql.conf
rename to docker/postgresql.conf
diff --git a/scripts/docker/init-electric-user.sh b/docker/scripts/init-electric-user.sh
similarity index 57%
rename from scripts/docker/init-electric-user.sh
rename to docker/scripts/init-electric-user.sh
index b3856c573..fbd1c361a 100755
--- a/scripts/docker/init-electric-user.sh
+++ b/docker/scripts/init-electric-user.sh
@@ -1,26 +1,9 @@
#!/bin/sh
-# ============================================================================
-# Electric SQL User Initialization Script (docker-compose only)
-# ============================================================================
-# This script is ONLY used when running via docker-compose.
-#
-# How it works:
-# - docker-compose.yml mounts this script into the PostgreSQL container's
-# /docker-entrypoint-initdb.d/ directory
-# - PostgreSQL automatically executes scripts in that directory on first
-# container initialization
-#
-# For local PostgreSQL users (non-Docker), this script is NOT used.
-# Instead, the Electric user is created by Alembic migration 66
-# (66_add_notifications_table_and_electric_replication.py).
-#
-# Both approaches are idempotent (use IF NOT EXISTS), so running both
-# will not cause conflicts.
-# ============================================================================
+# Creates the Electric SQL replication user on first DB initialization.
+# Idempotent — safe to run alongside Alembic migration 66.
set -e
-# Use environment variables with defaults
ELECTRIC_DB_USER="${ELECTRIC_DB_USER:-electric}"
ELECTRIC_DB_PASSWORD="${ELECTRIC_DB_PASSWORD:-electric_password}"
@@ -43,7 +26,6 @@ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-E
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO $ELECTRIC_DB_USER;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON SEQUENCES TO $ELECTRIC_DB_USER;
- -- Create the publication for Electric SQL (if not exists)
DO \$\$
BEGIN
IF NOT EXISTS (SELECT FROM pg_publication WHERE pubname = 'electric_publication_default') THEN
diff --git a/docker/scripts/install.sh b/docker/scripts/install.sh
new file mode 100644
index 000000000..4bd41a85a
--- /dev/null
+++ b/docker/scripts/install.sh
@@ -0,0 +1,340 @@
+#!/usr/bin/env bash
+# =============================================================================
+# SurfSense — One-line Install Script
+#
+#
+# Usage: curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.sh | bash
+#
+# Flags:
+# --no-watchtower Skip automatic Watchtower setup
+# --watchtower-interval=SECS Check interval in seconds (default: 86400 = 24h)
+#
+# Handles two cases automatically:
+# 1. Fresh install — no prior SurfSense data detected
+# 2. Migration from the legacy all-in-one container (surfsense-data volume)
+# Downloads and runs migrate-database.sh --yes, then restores the dump
+# into the new PostgreSQL 17 stack. The user runs one command for both.
+#
+# If you used custom database credentials in the old all-in-one container, run
+# migrate-database.sh manually first (with --db-user / --db-password flags),
+# then re-run this script:
+# curl -fsSL .../docker/scripts/migrate-database.sh | bash -s -- --db-user X --db-password Y
+# =============================================================================
+
+set -euo pipefail
+
+main() {
+
+REPO_RAW="https://raw.githubusercontent.com/MODSetter/SurfSense/main"
+INSTALL_DIR="./surfsense"
+OLD_VOLUME="surfsense-data"
+DUMP_FILE="./surfsense_migration_backup.sql"
+KEY_FILE="./surfsense_migration_secret.key"
+MIGRATION_DONE_FILE="${INSTALL_DIR}/.migration_done"
+MIGRATION_MODE=false
+SETUP_WATCHTOWER=true
+WATCHTOWER_INTERVAL=86400
+WATCHTOWER_CONTAINER="watchtower"
+
+# ── Parse flags ─────────────────────────────────────────────────────────────
+for arg in "$@"; do
+ case "$arg" in
+ --no-watchtower) SETUP_WATCHTOWER=false ;;
+ --watchtower-interval=*) WATCHTOWER_INTERVAL="${arg#*=}" ;;
+ esac
+done
+
+CYAN='\033[1;36m'
+YELLOW='\033[1;33m'
+GREEN='\033[0;32m'
+RED='\033[0;31m'
+BOLD='\033[1m'
+NC='\033[0m'
+
+info() { printf "${CYAN}[SurfSense]${NC} %s\n" "$1"; }
+success() { printf "${GREEN}[SurfSense]${NC} %s\n" "$1"; }
+warn() { printf "${YELLOW}[SurfSense]${NC} %s\n" "$1"; }
+error() { printf "${RED}[SurfSense]${NC} ERROR: %s\n" "$1" >&2; exit 1; }
+step() { printf "\n${BOLD}${CYAN}── %s${NC}\n" "$1"; }
+
+# ── Pre-flight checks ────────────────────────────────────────────────────────
+
+step "Checking prerequisites"
+
+command -v docker >/dev/null 2>&1 \
+ || error "Docker is not installed. Install it at: https://docs.docker.com/get-docker/"
+success "Docker found."
+
+docker info >/dev/null 2>&1 < /dev/null \
+ || error "Docker daemon is not running. Please start Docker and try again."
+success "Docker daemon is running."
+
+if docker compose version >/dev/null 2>&1 < /dev/null; then
+ DC="docker compose"
+elif command -v docker-compose >/dev/null 2>&1; then
+ DC="docker-compose"
+else
+ error "Docker Compose is not installed. Install it at: https://docs.docker.com/compose/install/"
+fi
+success "Docker Compose found ($DC)."
+
+# ── Wait-for-postgres helper ─────────────────────────────────────────────────
+wait_for_pg() {
+ local db_user="$1"
+ local max_attempts=45
+ local attempt=0
+
+ info "Waiting for PostgreSQL to accept connections..."
+ until (cd "${INSTALL_DIR}" && ${DC} exec -T db pg_isready -U "${db_user}" -q 2>/dev/null) < /dev/null; do
+ attempt=$((attempt + 1))
+ if [[ $attempt -ge $max_attempts ]]; then
+ error "PostgreSQL did not become ready after $((max_attempts * 2)) seconds.\nCheck logs: cd ${INSTALL_DIR} && ${DC} logs db"
+ fi
+ printf "."
+ sleep 2
+ done
+ printf "\n"
+ success "PostgreSQL is ready."
+}
+
+# ── Download files ───────────────────────────────────────────────────────────
+
+step "Downloading SurfSense files"
+info "Installation directory: ${INSTALL_DIR}"
+mkdir -p "${INSTALL_DIR}/scripts"
+
+FILES=(
+ "docker/docker-compose.yml:docker-compose.yml"
+ "docker/.env.example:.env.example"
+ "docker/postgresql.conf:postgresql.conf"
+ "docker/scripts/init-electric-user.sh:scripts/init-electric-user.sh"
+ "docker/scripts/migrate-database.sh:scripts/migrate-database.sh"
+)
+
+for entry in "${FILES[@]}"; do
+ src="${entry%%:*}"
+ dest="${entry##*:}"
+ info "Downloading ${dest}..."
+ curl -fsSL "${REPO_RAW}/${src}" -o "${INSTALL_DIR}/${dest}" \
+ || error "Failed to download ${dest}. Check your internet connection and try again."
+done
+
+chmod +x "${INSTALL_DIR}/scripts/init-electric-user.sh"
+chmod +x "${INSTALL_DIR}/scripts/migrate-database.sh"
+success "All files downloaded to ${INSTALL_DIR}/"
+
+# ── Legacy all-in-one detection ──────────────────────────────────────────────
+# Detect surfsense-data volume → migration mode.
+# If a dump already exists (from a previous partial run) skip extraction and
+# go straight to restore — this makes re-runs safe and idempotent.
+
+if docker volume ls --format '{{.Name}}' 2>/dev/null < /dev/null | grep -q "^${OLD_VOLUME}$" \
+ && [[ ! -f "${MIGRATION_DONE_FILE}" ]]; then
+ MIGRATION_MODE=true
+
+ if [[ -f "${DUMP_FILE}" ]]; then
+ step "Migration mode — using existing dump (skipping extraction)"
+ info "Found existing dump: ${DUMP_FILE}"
+ info "Skipping data extraction — proceeding directly to restore."
+ info "To force a fresh extraction, remove the dump first: rm ${DUMP_FILE}"
+ else
+ step "Migration mode — legacy all-in-one container detected"
+ warn "Volume '${OLD_VOLUME}' found. Your data will be migrated automatically."
+ warn "PostgreSQL is being upgraded from version 14 to 17."
+ warn "Your original data will NOT be deleted."
+ printf "\n"
+ info "Running data extraction (migrate-database.sh --yes)..."
+ info "Full extraction log: ./surfsense-migration.log"
+ printf "\n"
+
+ # Run extraction non-interactively. On failure the error from
+ # migrate-database.sh is printed and install.sh exits here.
+ bash "${INSTALL_DIR}/scripts/migrate-database.sh" --yes < /dev/null \
+ || error "Data extraction failed. See ./surfsense-migration.log for details.\nYou can also run migrate-database.sh manually with custom flags:\n bash ${INSTALL_DIR}/scripts/migrate-database.sh --db-user X --db-password Y"
+
+ printf "\n"
+ success "Data extraction complete. Proceeding with installation and restore."
+ fi
+fi
+
+# ── Set up .env ──────────────────────────────────────────────────────────────
+
+step "Configuring environment"
+
+if [ ! -f "${INSTALL_DIR}/.env" ]; then
+ cp "${INSTALL_DIR}/.env.example" "${INSTALL_DIR}/.env"
+
+ if $MIGRATION_MODE && [[ -f "${KEY_FILE}" ]]; then
+ SECRET_KEY=$(cat "${KEY_FILE}" | tr -d '[:space:]')
+ success "Using SECRET_KEY recovered from legacy container."
+ else
+ SECRET_KEY=$(openssl rand -base64 32 2>/dev/null \
+ || head -c 32 /dev/urandom | base64 | tr -d '\n')
+ success "Generated new random SECRET_KEY."
+ fi
+
+ if [[ "$OSTYPE" == "darwin"* ]]; then
+ sed -i '' "s|SECRET_KEY=replace_me_with_a_random_string|SECRET_KEY=${SECRET_KEY}|" "${INSTALL_DIR}/.env"
+ else
+ sed -i "s|SECRET_KEY=replace_me_with_a_random_string|SECRET_KEY=${SECRET_KEY}|" "${INSTALL_DIR}/.env"
+ fi
+ info "Created ${INSTALL_DIR}/.env"
+else
+ warn ".env already exists — keeping your existing configuration."
+fi
+
+# ── Start containers ─────────────────────────────────────────────────────────
+
+if $MIGRATION_MODE; then
+ # Read DB credentials from .env (fall back to defaults from docker-compose.yml)
+ DB_USER=$(grep '^DB_USER=' "${INSTALL_DIR}/.env" 2>/dev/null | cut -d= -f2 | tr -d '"' | head -1 || true)
+ DB_PASS=$(grep '^DB_PASSWORD=' "${INSTALL_DIR}/.env" 2>/dev/null | cut -d= -f2 | tr -d '"' | head -1 || true)
+ DB_NAME=$(grep '^DB_NAME=' "${INSTALL_DIR}/.env" 2>/dev/null | cut -d= -f2 | tr -d '"' | head -1 || true)
+ DB_USER="${DB_USER:-surfsense}"
+ DB_PASS="${DB_PASS:-surfsense}"
+ DB_NAME="${DB_NAME:-surfsense}"
+
+ step "Starting PostgreSQL 17"
+ (cd "${INSTALL_DIR}" && ${DC} up -d db) < /dev/null
+ wait_for_pg "${DB_USER}"
+
+ step "Restoring database"
+ [[ -f "${DUMP_FILE}" ]] \
+ || error "Dump file '${DUMP_FILE}' not found. The migration script may have failed.\n Check: ./surfsense-migration.log\n Or run manually: bash ${INSTALL_DIR}/scripts/migrate-database.sh --yes"
+ info "Restoring dump into PostgreSQL 17 — this may take a while for large databases..."
+
+ RESTORE_ERR="/tmp/surfsense_restore_err.log"
+ (cd "${INSTALL_DIR}" && ${DC} exec -T \
+ -e PGPASSWORD="${DB_PASS}" \
+ db psql -U "${DB_USER}" -d "${DB_NAME}" \
+ >/dev/null 2>"${RESTORE_ERR}") < "${DUMP_FILE}" || true
+
+ # Surface real errors; ignore benign "already exists" noise from pg_dump headers
+ FATAL_ERRORS=$(grep -i "^ERROR:" "${RESTORE_ERR}" \
+ | grep -iv "already exists" \
+ | grep -iv "multiple primary keys" \
+ || true)
+
+ if [[ -n "${FATAL_ERRORS}" ]]; then
+ warn "Restore completed with errors (may be harmless pg_dump header noise):"
+ printf "%s\n" "${FATAL_ERRORS}"
+ warn "If SurfSense behaves incorrectly, inspect manually:"
+ warn " cd ${INSTALL_DIR} && ${DC} exec db psql -U ${DB_USER} -d ${DB_NAME} < ${DUMP_FILE}"
+ else
+ success "Database restored with no fatal errors."
+ fi
+
+ # Smoke test — verify tables are present
+ TABLE_COUNT=$(
+ cd "${INSTALL_DIR}" && ${DC} exec -T \
+ -e PGPASSWORD="${DB_PASS}" \
+ db psql -U "${DB_USER}" -d "${DB_NAME}" -t \
+ -c "SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public';" \
+ 2>/dev/null < /dev/null | tr -d ' \n' || echo "0"
+ )
+ if [[ "${TABLE_COUNT}" == "0" || -z "${TABLE_COUNT}" ]]; then
+ warn "Smoke test: no tables found after restore."
+ warn "The restore may have failed silently. Check: cd ${INSTALL_DIR} && ${DC} logs db"
+ else
+ success "Smoke test passed: ${TABLE_COUNT} table(s) restored successfully."
+ touch "${MIGRATION_DONE_FILE}"
+ fi
+
+ step "Starting all SurfSense services"
+ (cd "${INSTALL_DIR}" && ${DC} up -d) < /dev/null
+ success "All services started."
+
+ # Key file is no longer needed — SECRET_KEY is now in .env
+ rm -f "${KEY_FILE}"
+
+else
+ step "Starting SurfSense"
+ (cd "${INSTALL_DIR}" && ${DC} up -d) < /dev/null
+ success "All services started."
+fi
+
+# ── Watchtower (auto-update) ─────────────────────────────────────────────────
+
+if $SETUP_WATCHTOWER; then
+ step "Setting up Watchtower (auto-updates every $((WATCHTOWER_INTERVAL / 3600))h)"
+
+ WT_STATE=$(docker inspect -f '{{.State.Running}}' "${WATCHTOWER_CONTAINER}" 2>/dev/null < /dev/null || echo "missing")
+
+ if [[ "${WT_STATE}" == "true" ]]; then
+ success "Watchtower is already running — skipping."
+ else
+ if [[ "${WT_STATE}" != "missing" ]]; then
+ info "Removing stopped Watchtower container..."
+ docker rm -f "${WATCHTOWER_CONTAINER}" >/dev/null 2>&1 < /dev/null || true
+ fi
+ docker run -d \
+ --name "${WATCHTOWER_CONTAINER}" \
+ --restart unless-stopped \
+ -v /var/run/docker.sock:/var/run/docker.sock \
+ nickfedor/watchtower \
+ --label-enable \
+ --interval "${WATCHTOWER_INTERVAL}" >/dev/null 2>&1 < /dev/null \
+ && success "Watchtower started — labeled SurfSense containers will auto-update." \
+ || warn "Could not start Watchtower. You can set it up manually or use: docker compose pull && docker compose up -d"
+ fi
+else
+ info "Skipping Watchtower setup (--no-watchtower flag)."
+fi
+
+# ── Done ─────────────────────────────────────────────────────────────────────
+
+echo ""
+printf '\033[1;37m'
+cat << 'EOF'
+
+
+ .d8888b. .d888 .d8888b.
+d88P Y88b d88P" d88P Y88b
+Y88b. 888 Y88b.
+ "Y888b. 888 888 888d888 888888 "Y888b. .d88b. 88888b. .d8888b .d88b.
+ "Y88b. 888 888 888P" 888 "Y88b. d8P Y8b 888 "88b 88K d8P Y8b
+ "888 888 888 888 888 "888 88888888 888 888 "Y8888b. 88888888
+Y88b d88P Y88b 888 888 888 Y88b d88P Y8b. 888 888 X88 Y8b.
+ "Y8888P" "Y88888 888 888 "Y8888P" "Y8888 888 888 88888P' "Y8888
+
+
+EOF
+if [[ "${SURFSENSE_VERSION:-latest}" == "latest" ]]; then
+ _version_display="latest"
+else
+ _version_display="v${SURFSENSE_VERSION}"
+fi
+printf " Your personal AI-powered search engine ${YELLOW}[%s]${NC}\n" "${_version_display}"
+printf "${CYAN}══════════════════════════════════════════════════════════════${NC}\n\n"
+
+info " Frontend: http://localhost:3000"
+info " Backend: http://localhost:8000"
+info " API Docs: http://localhost:8000/docs"
+info ""
+info " Config: ${INSTALL_DIR}/.env"
+info " Logs: cd ${INSTALL_DIR} && ${DC} logs -f"
+info " Stop: cd ${INSTALL_DIR} && ${DC} down"
+info " Update: cd ${INSTALL_DIR} && ${DC} pull && ${DC} up -d"
+info ""
+
+if $SETUP_WATCHTOWER; then
+ info " Watchtower: auto-updates every $((WATCHTOWER_INTERVAL / 3600))h (stop: docker rm -f ${WATCHTOWER_CONTAINER})"
+else
+ warn " Watchtower skipped. For auto-updates, re-run without --no-watchtower."
+fi
+info ""
+
+if $MIGRATION_MODE; then
+ warn " Migration complete! Open frontend and verify your data."
+ warn " Once verified, clean up the legacy volume and migration files:"
+ warn " docker volume rm ${OLD_VOLUME}"
+ warn " rm ${DUMP_FILE}"
+ warn " rm ${MIGRATION_DONE_FILE}"
+else
+ warn " First startup may take a few minutes while images are pulled."
+ warn " Edit ${INSTALL_DIR}/.env to configure API keys, OAuth, etc."
+fi
+
+} # end main()
+
+main "$@"
diff --git a/docker/scripts/migrate-database.sh b/docker/scripts/migrate-database.sh
new file mode 100755
index 000000000..9bc1d90b7
--- /dev/null
+++ b/docker/scripts/migrate-database.sh
@@ -0,0 +1,335 @@
+#!/usr/bin/env bash
+# =============================================================================
+# SurfSense — Database Migration Script
+#
+# Extracts data from the legacy all-in-one surfsense-data volume (PostgreSQL 14)
+# and saves it as a SQL dump + SECRET_KEY file ready for install.sh to restore.
+#
+# Usage:
+# bash migrate-database.sh [options]
+#
+# Options:
+# --db-user USER Old PostgreSQL username (default: surfsense)
+# --db-password PASS Old PostgreSQL password (default: surfsense)
+# --db-name NAME Old PostgreSQL database (default: surfsense)
+# --yes / -y Skip all confirmation prompts
+# --help / -h Show this help
+#
+# Prerequisites:
+# - Docker installed and running
+# - The legacy surfsense-data volume must exist
+# - ~500 MB free disk space for the dump file
+#
+# What this script does:
+# 1. Stops any container using surfsense-data (to prevent corruption)
+# 2. Starts a temporary PG14 container against the old volume
+# 3. Dumps the database to ./surfsense_migration_backup.sql
+# 4. Recovers the SECRET_KEY to ./surfsense_migration_secret.key
+# 5. Exits — leaving installation to install.sh
+#
+# What this script does NOT do:
+# - Delete the original surfsense-data volume (do this manually after verifying)
+# - Install the new SurfSense stack (install.sh handles that automatically)
+#
+# Note:
+# install.sh downloads and runs this script automatically when it detects the
+# legacy surfsense-data volume. You only need to run this script manually if
+# you have custom database credentials (--db-user / --db-password / --db-name)
+# or if the automatic migration inside install.sh fails at the extraction step.
+# =============================================================================
+
+set -euo pipefail
+
+# ── Colours ──────────────────────────────────────────────────────────────────
+CYAN='\033[1;36m'
+YELLOW='\033[1;33m'
+GREEN='\033[0;32m'
+RED='\033[0;31m'
+BOLD='\033[1m'
+NC='\033[0m'
+
+# ── Logging — tee everything to a log file ───────────────────────────────────
+LOG_FILE="./surfsense-migration.log"
+exec > >(tee -a "${LOG_FILE}") 2>&1
+
+# ── Output helpers ────────────────────────────────────────────────────────────
+info() { printf "${CYAN}[SurfSense]${NC} %s\n" "$1"; }
+success() { printf "${GREEN}[SurfSense]${NC} %s\n" "$1"; }
+warn() { printf "${YELLOW}[SurfSense]${NC} %s\n" "$1"; }
+error() { printf "${RED}[SurfSense]${NC} ERROR: %s\n" "$1" >&2; exit 1; }
+step() { printf "\n${BOLD}${CYAN}── Step %s: %s${NC}\n" "$1" "$2"; }
+
+# ── Constants ─────────────────────────────────────────────────────────────────
+OLD_VOLUME="surfsense-data"
+TEMP_CONTAINER="surfsense-pg14-migration"
+DUMP_FILE="./surfsense_migration_backup.sql"
+KEY_FILE="./surfsense_migration_secret.key"
+PG14_IMAGE="pgvector/pgvector:pg14"
+
+# ── Defaults ──────────────────────────────────────────────────────────────────
+OLD_DB_USER="surfsense"
+OLD_DB_PASSWORD="surfsense"
+OLD_DB_NAME="surfsense"
+AUTO_YES=false
+
+# ── Argument parsing ──────────────────────────────────────────────────────────
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --db-user) OLD_DB_USER="$2"; shift 2 ;;
+ --db-password) OLD_DB_PASSWORD="$2"; shift 2 ;;
+ --db-name) OLD_DB_NAME="$2"; shift 2 ;;
+ --yes|-y) AUTO_YES=true; shift ;;
+ --help|-h)
+ grep '^#' "$0" | grep -v '^#!/' | sed 's/^# \{0,1\}//'
+ exit 0
+ ;;
+ *) error "Unknown option: $1 — run with --help for usage." ;;
+ esac
+done
+
+# ── Confirmation helper ───────────────────────────────────────────────────────
+confirm() {
+ if $AUTO_YES; then return 0; fi
+ printf "${YELLOW}[SurfSense]${NC} %s [y/N] " "$1"
+ read -r reply
+ [[ "$reply" =~ ^[Yy]$ ]] || { warn "Aborted."; exit 0; }
+}
+
+# ── Cleanup trap — always remove the temp container ──────────────────────────
+cleanup() {
+ local exit_code=$?
+ if docker ps -a --format '{{.Names}}' 2>/dev/null < /dev/null | grep -q "^${TEMP_CONTAINER}$"; then
+ info "Cleaning up temporary container '${TEMP_CONTAINER}'..."
+ docker stop "${TEMP_CONTAINER}" >/dev/null 2>&1 < /dev/null || true
+ docker rm "${TEMP_CONTAINER}" >/dev/null 2>&1 < /dev/null || true
+ fi
+ if [[ $exit_code -ne 0 ]]; then
+ printf "\n${RED}[SurfSense]${NC} Migration data extraction failed (exit code %s).\n" "${exit_code}" >&2
+ printf "${RED}[SurfSense]${NC} Full log: %s\n" "${LOG_FILE}" >&2
+ printf "${YELLOW}[SurfSense]${NC} Your original data in '${OLD_VOLUME}' is untouched.\n" >&2
+ fi
+}
+trap cleanup EXIT
+
+# ── Wait-for-postgres helper ──────────────────────────────────────────────────
+wait_for_pg() {
+ local container="$1"
+ local user="$2"
+ local label="${3:-PostgreSQL}"
+ local max_attempts=45
+ local attempt=0
+
+ info "Waiting for ${label} to accept connections..."
+ until docker exec "${container}" pg_isready -U "${user}" -q 2>/dev/null < /dev/null; do
+ attempt=$((attempt + 1))
+ if [[ $attempt -ge $max_attempts ]]; then
+ error "${label} did not become ready after $((max_attempts * 2)) seconds. Check: docker logs ${container}"
+ fi
+ printf "."
+ sleep 2
+ done
+ printf "\n"
+ success "${label} is ready."
+}
+
+info "Migrating data from legacy database (PostgreSQL 14 → 17)"
+
+# ── Step 0: Pre-flight checks ─────────────────────────────────────────────────
+step "0" "Pre-flight checks"
+
+# Docker CLI
+command -v docker >/dev/null 2>&1 \
+ || error "Docker is not installed. Install it at: https://docs.docker.com/get-docker/"
+
+# Docker daemon
+docker info >/dev/null 2>&1 < /dev/null \
+ || error "Docker daemon is not running. Please start Docker and try again."
+
+# Old volume must exist
+docker volume ls --format '{{.Name}}' < /dev/null | grep -q "^${OLD_VOLUME}$" \
+ || error "Legacy volume '${OLD_VOLUME}' not found.\n Are you sure you ran the old all-in-one SurfSense container?"
+success "Found legacy volume: ${OLD_VOLUME}"
+
+# Detect and stop any container currently using the old volume
+# (mounting a live PG volume into a second container causes the new container's
+# entrypoint to chown the data files, breaking the running container's access)
+OLD_CONTAINER=$(docker ps --filter "volume=${OLD_VOLUME}" --format '{{.Names}}' < /dev/null | head -n1 || true)
+if [[ -n "${OLD_CONTAINER}" ]]; then
+ warn "Container '${OLD_CONTAINER}' is running and using the '${OLD_VOLUME}' volume."
+ warn "It must be stopped before migration to prevent data file corruption."
+ confirm "Stop '${OLD_CONTAINER}' now and proceed with data extraction?"
+ docker stop "${OLD_CONTAINER}" >/dev/null 2>&1 < /dev/null \
+ || error "Failed to stop '${OLD_CONTAINER}'. Try: docker stop ${OLD_CONTAINER}"
+ success "Container '${OLD_CONTAINER}' stopped."
+fi
+
+# Bail out if a dump already exists — don't overwrite a previous successful run
+if [[ -f "${DUMP_FILE}" ]]; then
+ warn "Dump file '${DUMP_FILE}' already exists."
+ warn "If a previous extraction succeeded, just run install.sh now."
+ warn "To re-extract, remove the file first: rm ${DUMP_FILE}"
+ error "Aborting to avoid overwriting an existing dump."
+fi
+
+# Clean up any stale temp container from a previous failed run
+if docker ps -a --format '{{.Names}}' < /dev/null | grep -q "^${TEMP_CONTAINER}$"; then
+ warn "Stale migration container '${TEMP_CONTAINER}' found — removing it."
+ docker stop "${TEMP_CONTAINER}" >/dev/null 2>&1 < /dev/null || true
+ docker rm "${TEMP_CONTAINER}" >/dev/null 2>&1 < /dev/null || true
+fi
+
+# Disk space (warn if < 500 MB free)
+if command -v df >/dev/null 2>&1; then
+ FREE_KB=$(df -k . | awk 'NR==2 {print $4}')
+ FREE_MB=$(( FREE_KB / 1024 ))
+ if [[ $FREE_MB -lt 500 ]]; then
+ warn "Low disk space: ${FREE_MB} MB free. At least 500 MB recommended for the dump."
+ confirm "Continue anyway?"
+ else
+ success "Disk space: ${FREE_MB} MB free."
+ fi
+fi
+
+success "All pre-flight checks passed."
+
+# ── Confirmation prompt ───────────────────────────────────────────────────────
+printf "\n${BOLD}Extraction plan:${NC}\n"
+printf " Source volume : ${YELLOW}%s${NC} (PG14 data at /data/postgres)\n" "${OLD_VOLUME}"
+printf " Old credentials : user=${YELLOW}%s${NC} db=${YELLOW}%s${NC}\n" "${OLD_DB_USER}" "${OLD_DB_NAME}"
+printf " Dump saved to : ${YELLOW}%s${NC}\n" "${DUMP_FILE}"
+printf " SECRET_KEY to : ${YELLOW}%s${NC}\n" "${KEY_FILE}"
+printf " Log file : ${YELLOW}%s${NC}\n\n" "${LOG_FILE}"
+confirm "Start data extraction? (Your original data will not be deleted or modified.)"
+
+# ── Step 1: Start temporary PostgreSQL 14 container ──────────────────────────
+step "1" "Starting temporary PostgreSQL 14 container"
+
+info "Pulling ${PG14_IMAGE}..."
+docker pull "${PG14_IMAGE}" >/dev/null 2>&1 < /dev/null \
+ || warn "Could not pull ${PG14_IMAGE} — using cached image if available."
+
+# Detect the UID that owns the existing data files and run the temp container
+# as that user. This prevents the official postgres image entrypoint from
+# running as root and doing `chown -R postgres /data/postgres`, which would
+# re-own the files to UID 999 and break any subsequent access by the original
+# container's postgres process (which may run as a different UID).
+DATA_UID=$(docker run --rm -v "${OLD_VOLUME}:/data" alpine \
+ stat -c '%u' /data/postgres 2>/dev/null < /dev/null || echo "")
+if [[ -z "${DATA_UID}" || "${DATA_UID}" == "0" ]]; then
+ warn "Could not detect data directory UID — falling back to default (may chown files)."
+ USER_FLAG=""
+else
+ info "Data directory owned by UID ${DATA_UID} — starting temp container as that user."
+ USER_FLAG="--user ${DATA_UID}"
+fi
+
+docker run -d \
+ --name "${TEMP_CONTAINER}" \
+ -v "${OLD_VOLUME}:/data" \
+ -e PGDATA=/data/postgres \
+ -e POSTGRES_USER="${OLD_DB_USER}" \
+ -e POSTGRES_PASSWORD="${OLD_DB_PASSWORD}" \
+ -e POSTGRES_DB="${OLD_DB_NAME}" \
+ ${USER_FLAG} \
+ "${PG14_IMAGE}" >/dev/null < /dev/null
+
+success "Temporary container '${TEMP_CONTAINER}' started."
+wait_for_pg "${TEMP_CONTAINER}" "${OLD_DB_USER}" "PostgreSQL 14"
+
+# ── Step 2: Dump the database ─────────────────────────────────────────────────
+step "2" "Dumping PostgreSQL 14 database"
+
+info "Running pg_dump — this may take a while for large databases..."
+
+if ! docker exec \
+ -e PGPASSWORD="${OLD_DB_PASSWORD}" \
+ "${TEMP_CONTAINER}" \
+ pg_dump -U "${OLD_DB_USER}" --no-password "${OLD_DB_NAME}" \
+ > "${DUMP_FILE}" 2>/tmp/surfsense_pgdump_err < /dev/null; then
+ cat /tmp/surfsense_pgdump_err >&2
+ error "pg_dump failed. See above for details."
+fi
+
+# Validate: non-empty file
+[[ -s "${DUMP_FILE}" ]] \
+ || error "Dump file '${DUMP_FILE}' is empty. Something went wrong with pg_dump."
+
+# Validate: looks like a real PG dump
+grep -q "PostgreSQL database dump" "${DUMP_FILE}" \
+ || error "Dump file does not contain a valid PostgreSQL dump header — the file may be corrupt."
+
+# Validate: sanity-check line count
+DUMP_LINES=$(wc -l < "${DUMP_FILE}" | tr -d ' ')
+[[ $DUMP_LINES -ge 10 ]] \
+ || error "Dump has only ${DUMP_LINES} lines — suspiciously small. Aborting."
+
+DUMP_SIZE=$(du -sh "${DUMP_FILE}" 2>/dev/null | cut -f1)
+success "Dump complete: ${DUMP_SIZE} (${DUMP_LINES} lines) → ${DUMP_FILE}"
+
+# Stop the temp container (trap will also handle it on unexpected exit)
+info "Stopping temporary PostgreSQL 14 container..."
+docker stop "${TEMP_CONTAINER}" >/dev/null 2>&1 < /dev/null || true
+docker rm "${TEMP_CONTAINER}" >/dev/null 2>&1 < /dev/null || true
+success "Temporary container removed."
+
+# ── Step 3: Recover SECRET_KEY ────────────────────────────────────────────────
+step "3" "Recovering SECRET_KEY"
+
+RECOVERED_KEY=""
+
+if docker run --rm -v "${OLD_VOLUME}:/data" alpine \
+ sh -c 'test -f /data/.secret_key && cat /data/.secret_key' \
+ 2>/dev/null < /dev/null | grep -q .; then
+ RECOVERED_KEY=$(
+ docker run --rm -v "${OLD_VOLUME}:/data" alpine \
+ cat /data/.secret_key 2>/dev/null < /dev/null | tr -d '[:space:]'
+ )
+ success "Recovered SECRET_KEY from '${OLD_VOLUME}'."
+else
+ warn "No SECRET_KEY file found at /data/.secret_key in '${OLD_VOLUME}'."
+ warn "This means the all-in-one container was launched with SECRET_KEY set as an explicit env var."
+ if $AUTO_YES; then
+ # Non-interactive (called from install.sh) — auto-generate rather than hanging on read
+ RECOVERED_KEY=$(openssl rand -base64 32 2>/dev/null \
+ || head -c 32 /dev/urandom | base64 | tr -d '\n')
+ warn "Non-interactive mode: generated a new SECRET_KEY automatically."
+ warn "All active browser sessions will be logged out after migration."
+ warn "To restore your original key, update SECRET_KEY in ./surfsense/.env afterwards."
+ else
+ printf "${YELLOW}[SurfSense]${NC} Enter the SECRET_KEY from your old container's environment\n"
+ printf "${YELLOW}[SurfSense]${NC} (press Enter to generate a new one — existing sessions will be invalidated): "
+ read -r RECOVERED_KEY
+ if [[ -z "${RECOVERED_KEY}" ]]; then
+ RECOVERED_KEY=$(openssl rand -base64 32 2>/dev/null \
+ || head -c 32 /dev/urandom | base64 | tr -d '\n')
+ warn "Generated a new SECRET_KEY. All active browser sessions will be logged out after migration."
+ fi
+ fi
+fi
+
+# Save SECRET_KEY to a file for install.sh to pick up
+printf '%s' "${RECOVERED_KEY}" > "${KEY_FILE}"
+success "SECRET_KEY saved to ${KEY_FILE}"
+
+# ── Done ──────────────────────────────────────────────────────────────────────
+printf "\n${GREEN}${BOLD}"
+printf "══════════════════════════════════════════════════════════════\n"
+printf " Data extraction complete!\n"
+printf "══════════════════════════════════════════════════════════════\n"
+printf "${NC}\n"
+
+success "Dump file : ${DUMP_FILE} (${DUMP_SIZE})"
+success "Secret key: ${KEY_FILE}"
+printf "\n"
+info "Next step — run install.sh from this same directory:"
+printf "\n"
+printf "${CYAN} curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.sh | bash${NC}\n"
+printf "\n"
+info "install.sh will detect the dump, restore your data into PostgreSQL 17,"
+info "and start the full SurfSense stack automatically."
+printf "\n"
+warn "Keep both files until you have verified the migration:"
+warn " ${DUMP_FILE}"
+warn " ${KEY_FILE}"
+warn "Full log saved to: ${LOG_FILE}"
+printf "\n"
diff --git a/scripts/docker/entrypoint-allinone.sh b/scripts/docker/entrypoint-allinone.sh
deleted file mode 100644
index 7c232a079..000000000
--- a/scripts/docker/entrypoint-allinone.sh
+++ /dev/null
@@ -1,244 +0,0 @@
-#!/bin/bash
-set -e
-
-echo "==========================================="
-echo " 🏄 SurfSense All-in-One Container"
-echo "==========================================="
-
-# Create log directory
-mkdir -p /var/log/supervisor
-
-# ================================================
-# Ensure data directory exists
-# ================================================
-mkdir -p /data
-
-# ================================================
-# Generate SECRET_KEY if not provided
-# ================================================
-if [ -z "$SECRET_KEY" ]; then
- # Generate a random secret key and persist it
- if [ -f /data/.secret_key ]; then
- export SECRET_KEY=$(cat /data/.secret_key)
- echo "✅ Using existing SECRET_KEY from persistent storage"
- else
- export SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_urlsafe(32))")
- echo "$SECRET_KEY" > /data/.secret_key
- chmod 600 /data/.secret_key
- echo "✅ Generated new SECRET_KEY (saved for persistence)"
- fi
-fi
-
-# ================================================
-# Set default TTS/STT services if not provided
-# ================================================
-if [ -z "$TTS_SERVICE" ]; then
- export TTS_SERVICE="local/kokoro"
- echo "✅ Using default TTS_SERVICE: local/kokoro"
-fi
-
-if [ -z "$STT_SERVICE" ]; then
- export STT_SERVICE="local/base"
- echo "✅ Using default STT_SERVICE: local/base"
-fi
-
-# ================================================
-# Set Electric SQL configuration
-# ================================================
-export ELECTRIC_DB_USER="${ELECTRIC_DB_USER:-electric}"
-export ELECTRIC_DB_PASSWORD="${ELECTRIC_DB_PASSWORD:-electric_password}"
-if [ -z "$ELECTRIC_DATABASE_URL" ]; then
- export ELECTRIC_DATABASE_URL="postgresql://${ELECTRIC_DB_USER}:${ELECTRIC_DB_PASSWORD}@localhost:5432/${POSTGRES_DB:-surfsense}?sslmode=disable"
- echo "✅ Electric SQL URL configured dynamically"
-else
- # Ensure sslmode=disable is in the URL if not already present
- if [[ "$ELECTRIC_DATABASE_URL" != *"sslmode="* ]]; then
- # Add sslmode=disable (handle both cases: with or without existing query params)
- if [[ "$ELECTRIC_DATABASE_URL" == *"?"* ]]; then
- export ELECTRIC_DATABASE_URL="${ELECTRIC_DATABASE_URL}&sslmode=disable"
- else
- export ELECTRIC_DATABASE_URL="${ELECTRIC_DATABASE_URL}?sslmode=disable"
- fi
- fi
- echo "✅ Electric SQL URL configured from environment"
-fi
-
-# Set Electric SQL port
-export ELECTRIC_PORT="${ELECTRIC_PORT:-5133}"
-export PORT="${ELECTRIC_PORT}"
-
-# ================================================
-# Initialize PostgreSQL if needed
-# ================================================
-if [ ! -f /data/postgres/PG_VERSION ]; then
- echo "📦 Initializing PostgreSQL database..."
-
- # Initialize PostgreSQL data directory
- chown -R postgres:postgres /data/postgres
- chmod 700 /data/postgres
-
- # Initialize with UTF8 encoding (required for proper text handling)
- su - postgres -c "/usr/lib/postgresql/14/bin/initdb -D /data/postgres --encoding=UTF8 --locale=C.UTF-8"
-
- # Configure PostgreSQL for connections
- echo "host all all 0.0.0.0/0 md5" >> /data/postgres/pg_hba.conf
- echo "local all all trust" >> /data/postgres/pg_hba.conf
- echo "listen_addresses='*'" >> /data/postgres/postgresql.conf
-
- # Enable logical replication for Electric SQL
- echo "wal_level = logical" >> /data/postgres/postgresql.conf
- echo "max_replication_slots = 10" >> /data/postgres/postgresql.conf
- echo "max_wal_senders = 10" >> /data/postgres/postgresql.conf
-
- # Start PostgreSQL temporarily to create database and user
- su - postgres -c "/usr/lib/postgresql/14/bin/pg_ctl -D /data/postgres -l /tmp/postgres_init.log start"
-
- # Wait for PostgreSQL to be ready
- sleep 5
-
- # Create user and database
- su - postgres -c "psql -c \"CREATE USER ${POSTGRES_USER:-surfsense} WITH PASSWORD '${POSTGRES_PASSWORD:-surfsense}' SUPERUSER;\""
- su - postgres -c "psql -c \"CREATE DATABASE ${POSTGRES_DB:-surfsense} OWNER ${POSTGRES_USER:-surfsense};\""
-
- # Enable pgvector extension
- su - postgres -c "psql -d ${POSTGRES_DB:-surfsense} -c 'CREATE EXTENSION IF NOT EXISTS vector;'"
-
- # Create Electric SQL replication user (idempotent - uses IF NOT EXISTS)
- echo "📡 Creating Electric SQL replication user..."
- su - postgres -c "psql -d ${POSTGRES_DB:-surfsense} <<-EOSQL
- DO \\\$\\\$
- BEGIN
- IF NOT EXISTS (SELECT FROM pg_user WHERE usename = '${ELECTRIC_DB_USER}') THEN
- CREATE USER ${ELECTRIC_DB_USER} WITH REPLICATION PASSWORD '${ELECTRIC_DB_PASSWORD}';
- END IF;
- END
- \\\$\\\$;
-
- GRANT CONNECT ON DATABASE ${POSTGRES_DB:-surfsense} TO ${ELECTRIC_DB_USER};
- GRANT USAGE ON SCHEMA public TO ${ELECTRIC_DB_USER};
- GRANT SELECT ON ALL TABLES IN SCHEMA public TO ${ELECTRIC_DB_USER};
- GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO ${ELECTRIC_DB_USER};
- ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO ${ELECTRIC_DB_USER};
- ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON SEQUENCES TO ${ELECTRIC_DB_USER};
-
- -- Create the publication for Electric SQL (if not exists)
- DO \\\$\\\$
- BEGIN
- IF NOT EXISTS (SELECT FROM pg_publication WHERE pubname = 'electric_publication_default') THEN
- CREATE PUBLICATION electric_publication_default;
- END IF;
- END
- \\\$\\\$;
-EOSQL"
- echo "✅ Electric SQL user '${ELECTRIC_DB_USER}' created"
-
- # Stop temporary PostgreSQL
- su - postgres -c "/usr/lib/postgresql/14/bin/pg_ctl -D /data/postgres stop"
-
- echo "✅ PostgreSQL initialized successfully"
-else
- echo "✅ PostgreSQL data directory already exists"
-fi
-
-# ================================================
-# Initialize Redis data directory
-# ================================================
-mkdir -p /data/redis
-chmod 755 /data/redis
-echo "✅ Redis data directory ready"
-
-# ================================================
-# Copy frontend build to runtime location
-# ================================================
-if [ -d /app/frontend/.next/standalone ]; then
- cp -r /app/frontend/.next/standalone/* /app/frontend/ 2>/dev/null || true
- cp -r /app/frontend/.next/static /app/frontend/.next/static 2>/dev/null || true
-fi
-
-# ================================================
-# Runtime Environment Variable Replacement
-# ================================================
-# Next.js NEXT_PUBLIC_* vars are baked in at build time.
-# This replaces placeholder values with actual runtime env vars.
-echo "🔧 Applying runtime environment configuration..."
-
-# Set defaults if not provided
-NEXT_PUBLIC_FASTAPI_BACKEND_URL="${NEXT_PUBLIC_FASTAPI_BACKEND_URL:-http://localhost:8000}"
-NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE="${NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE:-LOCAL}"
-NEXT_PUBLIC_ETL_SERVICE="${NEXT_PUBLIC_ETL_SERVICE:-DOCLING}"
-NEXT_PUBLIC_ELECTRIC_URL="${NEXT_PUBLIC_ELECTRIC_URL:-http://localhost:5133}"
-NEXT_PUBLIC_ELECTRIC_AUTH_MODE="${NEXT_PUBLIC_ELECTRIC_AUTH_MODE:-insecure}"
-NEXT_PUBLIC_DEPLOYMENT_MODE="${NEXT_PUBLIC_DEPLOYMENT_MODE:-self-hosted}"
-
-# Replace placeholders in all JS files
-find /app/frontend -type f \( -name "*.js" -o -name "*.json" \) -exec sed -i \
- -e "s|__NEXT_PUBLIC_FASTAPI_BACKEND_URL__|${NEXT_PUBLIC_FASTAPI_BACKEND_URL}|g" \
- -e "s|__NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE__|${NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE}|g" \
- -e "s|__NEXT_PUBLIC_ETL_SERVICE__|${NEXT_PUBLIC_ETL_SERVICE}|g" \
- -e "s|__NEXT_PUBLIC_ELECTRIC_URL__|${NEXT_PUBLIC_ELECTRIC_URL}|g" \
- -e "s|__NEXT_PUBLIC_ELECTRIC_AUTH_MODE__|${NEXT_PUBLIC_ELECTRIC_AUTH_MODE}|g" \
- -e "s|__NEXT_PUBLIC_DEPLOYMENT_MODE__|${NEXT_PUBLIC_DEPLOYMENT_MODE}|g" \
- {} +
-
-echo "✅ Environment configuration applied"
-echo " Backend URL: ${NEXT_PUBLIC_FASTAPI_BACKEND_URL}"
-echo " Auth Type: ${NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE}"
-echo " ETL Service: ${NEXT_PUBLIC_ETL_SERVICE}"
-echo " Electric URL: ${NEXT_PUBLIC_ELECTRIC_URL}"
-echo " Deployment Mode: ${NEXT_PUBLIC_DEPLOYMENT_MODE}"
-
-# ================================================
-# Run database migrations
-# ================================================
-run_migrations() {
- echo "🔄 Running database migrations..."
-
- # Start PostgreSQL temporarily for migrations
- su - postgres -c "/usr/lib/postgresql/14/bin/pg_ctl -D /data/postgres -l /tmp/postgres_migrate.log start"
- sleep 5
-
- # Start Redis temporarily for migrations (some might need it)
- redis-server --dir /data/redis --daemonize yes
- sleep 2
-
- # Run alembic migrations
- cd /app/backend
- alembic upgrade head || echo "⚠️ Migrations may have already been applied"
-
- # Stop temporary services
- redis-cli shutdown || true
- su - postgres -c "/usr/lib/postgresql/14/bin/pg_ctl -D /data/postgres stop"
-
- echo "✅ Database migrations complete"
-}
-
-# Always run migrations on startup - alembic upgrade head is safe to run
-# every time. It only applies pending migrations (never re-runs applied ones,
-# never calls downgrade). This ensures updates are applied automatically.
-run_migrations
-
-# ================================================
-# Environment Variables Info
-# ================================================
-echo ""
-echo "==========================================="
-echo " 📋 Configuration"
-echo "==========================================="
-echo " Frontend URL: http://localhost:3000"
-echo " Backend API: ${NEXT_PUBLIC_FASTAPI_BACKEND_URL}"
-echo " API Docs: ${NEXT_PUBLIC_FASTAPI_BACKEND_URL}/docs"
-echo " Electric URL: ${NEXT_PUBLIC_ELECTRIC_URL:-http://localhost:5133}"
-echo " Auth Type: ${NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE}"
-echo " ETL Service: ${NEXT_PUBLIC_ETL_SERVICE}"
-echo " TTS Service: ${TTS_SERVICE}"
-echo " STT Service: ${STT_SERVICE}"
-echo " Daytona Sandbox: ${DAYTONA_SANDBOX_ENABLED:-FALSE}"
-echo "==========================================="
-echo ""
-
-# ================================================
-# Start Supervisor (manages all services)
-# ================================================
-echo "🚀 Starting all services..."
-exec /usr/local/bin/supervisord -c /etc/supervisor/conf.d/surfsense.conf
-
diff --git a/scripts/docker/init-postgres.sh b/scripts/docker/init-postgres.sh
deleted file mode 100644
index b6ddb6a50..000000000
--- a/scripts/docker/init-postgres.sh
+++ /dev/null
@@ -1,77 +0,0 @@
-#!/bin/bash
-# PostgreSQL initialization script for SurfSense
-# This script is called during container startup if the database needs initialization
-
-set -e
-
-PGDATA=${PGDATA:-/data/postgres}
-POSTGRES_USER=${POSTGRES_USER:-surfsense}
-POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-surfsense}
-POSTGRES_DB=${POSTGRES_DB:-surfsense}
-
-# Electric SQL user credentials (configurable)
-ELECTRIC_DB_USER=${ELECTRIC_DB_USER:-electric}
-ELECTRIC_DB_PASSWORD=${ELECTRIC_DB_PASSWORD:-electric_password}
-
-echo "Initializing PostgreSQL..."
-
-# Check if PostgreSQL is already initialized
-if [ -f "$PGDATA/PG_VERSION" ]; then
- echo "PostgreSQL data directory already exists. Skipping initialization."
- exit 0
-fi
-
-# Initialize the database cluster
-/usr/lib/postgresql/14/bin/initdb -D "$PGDATA" --username=postgres
-
-# Configure PostgreSQL
-cat >> "$PGDATA/postgresql.conf" << EOF
-listen_addresses = '*'
-max_connections = 200
-shared_buffers = 256MB
-
-# Enable logical replication (required for Electric SQL)
-wal_level = logical
-max_replication_slots = 10
-max_wal_senders = 10
-
-# Performance settings
-checkpoint_timeout = 10min
-max_wal_size = 1GB
-min_wal_size = 80MB
-EOF
-
-cat >> "$PGDATA/pg_hba.conf" << EOF
-# Allow connections from anywhere with password
-host all all 0.0.0.0/0 md5
-host all all ::0/0 md5
-EOF
-
-# Start PostgreSQL temporarily
-/usr/lib/postgresql/14/bin/pg_ctl -D "$PGDATA" -l /tmp/postgres_init.log start
-
-# Wait for PostgreSQL to start
-sleep 3
-
-# Create user and database
-psql -U postgres << EOF
-CREATE USER $POSTGRES_USER WITH PASSWORD '$POSTGRES_PASSWORD' SUPERUSER;
-CREATE DATABASE $POSTGRES_DB OWNER $POSTGRES_USER;
-\c $POSTGRES_DB
-CREATE EXTENSION IF NOT EXISTS vector;
-
--- Create Electric SQL replication user
-CREATE USER $ELECTRIC_DB_USER WITH REPLICATION PASSWORD '$ELECTRIC_DB_PASSWORD';
-GRANT CONNECT ON DATABASE $POSTGRES_DB TO $ELECTRIC_DB_USER;
-GRANT USAGE ON SCHEMA public TO $ELECTRIC_DB_USER;
-GRANT SELECT ON ALL TABLES IN SCHEMA public TO $ELECTRIC_DB_USER;
-GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO $ELECTRIC_DB_USER;
-ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO $ELECTRIC_DB_USER;
-ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON SEQUENCES TO $ELECTRIC_DB_USER;
-EOF
-
-echo "PostgreSQL initialized successfully."
-
-# Stop PostgreSQL (supervisor will start it)
-/usr/lib/postgresql/14/bin/pg_ctl -D "$PGDATA" stop
-
diff --git a/scripts/docker/supervisor-allinone.conf b/scripts/docker/supervisor-allinone.conf
deleted file mode 100644
index 1a21fcc04..000000000
--- a/scripts/docker/supervisor-allinone.conf
+++ /dev/null
@@ -1,121 +0,0 @@
-[supervisord]
-nodaemon=true
-logfile=/dev/stdout
-logfile_maxbytes=0
-pidfile=/var/run/supervisord.pid
-loglevel=info
-user=root
-
-[unix_http_server]
-file=/var/run/supervisor.sock
-chmod=0700
-
-[rpcinterface:supervisor]
-supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
-
-[supervisorctl]
-serverurl=unix:///var/run/supervisor.sock
-
-# PostgreSQL
-[program:postgresql]
-command=/usr/lib/postgresql/14/bin/postgres -D /data/postgres
-user=postgres
-autostart=true
-autorestart=true
-priority=10
-stdout_logfile=/dev/stdout
-stdout_logfile_maxbytes=0
-stderr_logfile=/dev/stderr
-stderr_logfile_maxbytes=0
-environment=PGDATA="/data/postgres"
-
-# Redis
-[program:redis]
-command=/usr/bin/redis-server --dir /data/redis --appendonly yes
-autostart=true
-autorestart=true
-priority=20
-stdout_logfile=/dev/stdout
-stdout_logfile_maxbytes=0
-stderr_logfile=/dev/stderr
-stderr_logfile_maxbytes=0
-
-# Backend API
-[program:backend]
-command=python main.py
-directory=/app/backend
-autostart=true
-autorestart=true
-priority=30
-startsecs=10
-startretries=3
-stdout_logfile=/dev/stdout
-stdout_logfile_maxbytes=0
-stderr_logfile=/dev/stderr
-stderr_logfile_maxbytes=0
-environment=PYTHONPATH="/app/backend",UVICORN_LOOP="asyncio",UNSTRUCTURED_HAS_PATCHED_LOOP="1"
-
-# Celery Worker
-[program:celery-worker]
-command=celery -A app.celery_app worker --loglevel=info --concurrency=2 --pool=solo --queues=surfsense,surfsense.connectors
-directory=/app/backend
-autostart=true
-autorestart=true
-priority=40
-startsecs=15
-startretries=3
-stdout_logfile=/dev/stdout
-stdout_logfile_maxbytes=0
-stderr_logfile=/dev/stderr
-stderr_logfile_maxbytes=0
-environment=PYTHONPATH="/app/backend"
-
-# Celery Beat (scheduler)
-[program:celery-beat]
-command=celery -A app.celery_app beat --loglevel=info
-directory=/app/backend
-autostart=true
-autorestart=true
-priority=50
-startsecs=20
-startretries=3
-stdout_logfile=/dev/stdout
-stdout_logfile_maxbytes=0
-stderr_logfile=/dev/stderr
-stderr_logfile_maxbytes=0
-environment=PYTHONPATH="/app/backend"
-
-# Electric SQL (real-time sync)
-[program:electric]
-command=/app/electric-release/bin/entrypoint start
-autostart=true
-autorestart=true
-priority=25
-startsecs=10
-startretries=3
-stdout_logfile=/dev/stdout
-stdout_logfile_maxbytes=0
-stderr_logfile=/dev/stderr
-stderr_logfile_maxbytes=0
-environment=DATABASE_URL="%(ENV_ELECTRIC_DATABASE_URL)s",ELECTRIC_INSECURE="%(ENV_ELECTRIC_INSECURE)s",ELECTRIC_WRITE_TO_PG_MODE="%(ENV_ELECTRIC_WRITE_TO_PG_MODE)s",RELEASE_COOKIE="surfsense_electric_cookie",PORT="%(ENV_ELECTRIC_PORT)s"
-
-# Frontend
-[program:frontend]
-command=node server.js
-directory=/app/frontend
-autostart=true
-autorestart=true
-priority=60
-startsecs=5
-startretries=3
-stdout_logfile=/dev/stdout
-stdout_logfile_maxbytes=0
-stderr_logfile=/dev/stderr
-stderr_logfile_maxbytes=0
-environment=NODE_ENV="production",PORT="3000",HOSTNAME="0.0.0.0"
-
-# Process Groups
-[group:surfsense]
-programs=postgresql,redis,electric,backend,celery-worker,celery-beat,frontend
-priority=999
-
diff --git a/surfsense_backend/alembic/versions/66_add_notifications_table_and_electric_replication.py b/surfsense_backend/alembic/versions/66_add_notifications_table_and_electric_replication.py
index 182bf981c..35418c6ae 100644
--- a/surfsense_backend/alembic/versions/66_add_notifications_table_and_electric_replication.py
+++ b/surfsense_backend/alembic/versions/66_add_notifications_table_and_electric_replication.py
@@ -8,7 +8,7 @@ Creates notifications table and sets up Electric SQL replication
search_source_connectors, and documents tables.
NOTE: Electric SQL user creation is idempotent (uses IF NOT EXISTS).
-- Docker deployments: user is pre-created by scripts/docker/init-electric-user.sh
+- Docker deployments: user is pre-created by docker/scripts/init-electric-user.sh
- Local PostgreSQL: user is created here during migration
Both approaches are safe to run together without conflicts as this migraiton is idempotent
"""
diff --git a/surfsense_backend/scripts/docker/entrypoint.sh b/surfsense_backend/scripts/docker/entrypoint.sh
index e1a2778fc..7bfcfce86 100644
--- a/surfsense_backend/scripts/docker/entrypoint.sh
+++ b/surfsense_backend/scripts/docker/entrypoint.sh
@@ -53,7 +53,7 @@ run_migrations() {
sleep 1
done
- if timeout 60 alembic upgrade head 2>&1; then
+ if timeout 300 alembic upgrade head 2>&1; then
echo "Migrations completed successfully."
else
echo "WARNING: Migration failed or timed out. Continuing anyway..."
diff --git a/surfsense_web/Dockerfile b/surfsense_web/Dockerfile
index e0ecb1225..51f65da5e 100644
--- a/surfsense_web/Dockerfile
+++ b/surfsense_web/Dockerfile
@@ -29,15 +29,22 @@ WORKDIR /app
# Enable pnpm
RUN corepack enable pnpm
-# Accept build arguments for Next.js public env vars
-ARG NEXT_PUBLIC_FASTAPI_BACKEND_URL
-ARG NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE
-ARG NEXT_PUBLIC_ETL_SERVICE
+# Build with placeholder values for NEXT_PUBLIC_* variables.
+# These are replaced at container startup by docker-entrypoint.js
+# with real values from the container's environment variables.
+ARG NEXT_PUBLIC_FASTAPI_BACKEND_URL=__NEXT_PUBLIC_FASTAPI_BACKEND_URL__
+ARG NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE=__NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE__
+ARG NEXT_PUBLIC_ETL_SERVICE=__NEXT_PUBLIC_ETL_SERVICE__
+ARG NEXT_PUBLIC_ELECTRIC_URL=__NEXT_PUBLIC_ELECTRIC_URL__
+ARG NEXT_PUBLIC_ELECTRIC_AUTH_MODE=__NEXT_PUBLIC_ELECTRIC_AUTH_MODE__
+ARG NEXT_PUBLIC_DEPLOYMENT_MODE=__NEXT_PUBLIC_DEPLOYMENT_MODE__
-# Set them as environment variables for the build
ENV NEXT_PUBLIC_FASTAPI_BACKEND_URL=$NEXT_PUBLIC_FASTAPI_BACKEND_URL
ENV NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE=$NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE
ENV NEXT_PUBLIC_ETL_SERVICE=$NEXT_PUBLIC_ETL_SERVICE
+ENV NEXT_PUBLIC_ELECTRIC_URL=$NEXT_PUBLIC_ELECTRIC_URL
+ENV NEXT_PUBLIC_ELECTRIC_AUTH_MODE=$NEXT_PUBLIC_ELECTRIC_AUTH_MODE
+ENV NEXT_PUBLIC_DEPLOYMENT_MODE=$NEXT_PUBLIC_DEPLOYMENT_MODE
COPY --from=deps /app/node_modules ./node_modules
COPY . .
@@ -67,6 +74,10 @@ COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
+# Entrypoint scripts for runtime env var substitution
+COPY --chown=nextjs:nodejs docker-entrypoint.js ./docker-entrypoint.js
+COPY --chown=nextjs:nodejs --chmod=755 docker-entrypoint.sh ./docker-entrypoint.sh
+
USER nextjs
EXPOSE 3000
@@ -76,4 +87,4 @@ ENV PORT=3000
# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/config/next-config-js/output
ENV HOSTNAME="0.0.0.0"
-CMD ["node", "server.js"]
\ No newline at end of file
+ENTRYPOINT ["/bin/sh", "./docker-entrypoint.sh"]
\ No newline at end of file
diff --git a/surfsense_web/content/docs/connectors/airtable.mdx b/surfsense_web/content/docs/connectors/airtable.mdx
index db7fe3ac0..71148335c 100644
--- a/surfsense_web/content/docs/connectors/airtable.mdx
+++ b/surfsense_web/content/docs/connectors/airtable.mdx
@@ -88,16 +88,16 @@ After saving, you'll find your OAuth credentials on the integration page:
## Running SurfSense with Airtable Connector
-Add the Airtable environment variables to your Docker run command:
+Add the Airtable credentials to your `.env` file (created during [Docker installation](/docs/docker-installation)):
```bash
-docker run -d -p 3000:3000 -p 8000:8000 \
- -v surfsense-data:/data \
- # Airtable Connector
- -e AIRTABLE_CLIENT_ID=your_airtable_client_id \
- -e AIRTABLE_CLIENT_SECRET=your_airtable_client_secret \
- -e AIRTABLE_REDIRECT_URI=http://localhost:8000/api/v1/auth/airtable/connector/callback \
- --name surfsense \
- --restart unless-stopped \
- ghcr.io/modsetter/surfsense:latest
+AIRTABLE_CLIENT_ID=your_airtable_client_id
+AIRTABLE_CLIENT_SECRET=your_airtable_client_secret
+AIRTABLE_REDIRECT_URI=http://localhost:8000/api/v1/auth/airtable/connector/callback
+```
+
+Then restart the services:
+
+```bash
+docker compose up -d
```
\ No newline at end of file
diff --git a/surfsense_web/content/docs/connectors/clickup.mdx b/surfsense_web/content/docs/connectors/clickup.mdx
index 960b88370..768bca859 100644
--- a/surfsense_web/content/docs/connectors/clickup.mdx
+++ b/surfsense_web/content/docs/connectors/clickup.mdx
@@ -44,16 +44,16 @@ After creating the app, you'll see your credentials:
## Running SurfSense with ClickUp Connector
-Add the ClickUp environment variables to your Docker run command:
+Add the ClickUp credentials to your `.env` file (created during [Docker installation](/docs/docker-installation)):
```bash
-docker run -d -p 3000:3000 -p 8000:8000 \
- -v surfsense-data:/data \
- # ClickUp Connector
- -e CLICKUP_CLIENT_ID=your_clickup_client_id \
- -e CLICKUP_CLIENT_SECRET=your_clickup_client_secret \
- -e CLICKUP_REDIRECT_URI=http://localhost:8000/api/v1/auth/clickup/connector/callback \
- --name surfsense \
- --restart unless-stopped \
- ghcr.io/modsetter/surfsense:latest
+CLICKUP_CLIENT_ID=your_clickup_client_id
+CLICKUP_CLIENT_SECRET=your_clickup_client_secret
+CLICKUP_REDIRECT_URI=http://localhost:8000/api/v1/auth/clickup/connector/callback
+```
+
+Then restart the services:
+
+```bash
+docker compose up -d
```
\ No newline at end of file
diff --git a/surfsense_web/content/docs/connectors/confluence.mdx b/surfsense_web/content/docs/connectors/confluence.mdx
index 57116cf29..3ee3394a4 100644
--- a/surfsense_web/content/docs/connectors/confluence.mdx
+++ b/surfsense_web/content/docs/connectors/confluence.mdx
@@ -97,16 +97,16 @@ Select the **"Granular scopes"** tab and enable:
## Running SurfSense with Confluence Connector
-Add the Atlassian environment variables to your Docker run command:
+Add the Atlassian credentials to your `.env` file (created during [Docker installation](/docs/docker-installation)):
```bash
-docker run -d -p 3000:3000 -p 8000:8000 \
- -v surfsense-data:/data \
- # Confluence Connector
- -e ATLASSIAN_CLIENT_ID=your_atlassian_client_id \
- -e ATLASSIAN_CLIENT_SECRET=your_atlassian_client_secret \
- -e CONFLUENCE_REDIRECT_URI=http://localhost:8000/api/v1/auth/confluence/connector/callback \
- --name surfsense \
- --restart unless-stopped \
- ghcr.io/modsetter/surfsense:latest
+ATLASSIAN_CLIENT_ID=your_atlassian_client_id
+ATLASSIAN_CLIENT_SECRET=your_atlassian_client_secret
+CONFLUENCE_REDIRECT_URI=http://localhost:8000/api/v1/auth/confluence/connector/callback
+```
+
+Then restart the services:
+
+```bash
+docker compose up -d
```
diff --git a/surfsense_web/content/docs/connectors/discord.mdx b/surfsense_web/content/docs/connectors/discord.mdx
index a90bcfe87..05825e0ea 100644
--- a/surfsense_web/content/docs/connectors/discord.mdx
+++ b/surfsense_web/content/docs/connectors/discord.mdx
@@ -64,17 +64,17 @@ You'll also see your **Application ID** and **Public Key** on this page.
## Running SurfSense with Discord Connector
-Add the Discord environment variables to your Docker run command:
+Add the Discord credentials to your `.env` file (created during [Docker installation](/docs/docker-installation)):
```bash
-docker run -d -p 3000:3000 -p 8000:8000 \
- -v surfsense-data:/data \
- # Discord Connector
- -e DISCORD_CLIENT_ID=your_discord_client_id \
- -e DISCORD_CLIENT_SECRET=your_discord_client_secret \
- -e DISCORD_REDIRECT_URI=http://localhost:8000/api/v1/auth/discord/connector/callback \
- -e DISCORD_BOT_TOKEN=http://localhost:8000/api/v1/auth/discord/connector/callback \
- --name surfsense \
- --restart unless-stopped \
- ghcr.io/modsetter/surfsense:latest
+DISCORD_CLIENT_ID=your_discord_client_id
+DISCORD_CLIENT_SECRET=your_discord_client_secret
+DISCORD_REDIRECT_URI=http://localhost:8000/api/v1/auth/discord/connector/callback
+DISCORD_BOT_TOKEN=your_discord_bot_token
+```
+
+Then restart the services:
+
+```bash
+docker compose up -d
```
diff --git a/surfsense_web/content/docs/connectors/gmail.mdx b/surfsense_web/content/docs/connectors/gmail.mdx
index 2b514f89e..1b3f81efe 100644
--- a/surfsense_web/content/docs/connectors/gmail.mdx
+++ b/surfsense_web/content/docs/connectors/gmail.mdx
@@ -70,16 +70,16 @@ This guide walks you through setting up a Google OAuth 2.0 integration for SurfS
## Running SurfSense with Gmail Connector
-Add the Google OAuth environment variables to your Docker run command:
+Add the Google OAuth credentials to your `.env` file (created during [Docker installation](/docs/docker-installation)):
```bash
-docker run -d -p 3000:3000 -p 8000:8000 \
- -v surfsense-data:/data \
- # Gmail Connector
- -e GOOGLE_OAUTH_CLIENT_ID=your_google_client_id \
- -e GOOGLE_OAUTH_CLIENT_SECRET=your_google_client_secret \
- -e GOOGLE_GMAIL_REDIRECT_URI=http://localhost:8000/api/v1/auth/google/gmail/connector/callback \
- --name surfsense \
- --restart unless-stopped \
- ghcr.io/modsetter/surfsense:latest
+GOOGLE_OAUTH_CLIENT_ID=your_google_client_id
+GOOGLE_OAUTH_CLIENT_SECRET=your_google_client_secret
+GOOGLE_GMAIL_REDIRECT_URI=http://localhost:8000/api/v1/auth/google/gmail/connector/callback
+```
+
+Then restart the services:
+
+```bash
+docker compose up -d
```
diff --git a/surfsense_web/content/docs/connectors/google-calendar.mdx b/surfsense_web/content/docs/connectors/google-calendar.mdx
index 7919d0361..481b05444 100644
--- a/surfsense_web/content/docs/connectors/google-calendar.mdx
+++ b/surfsense_web/content/docs/connectors/google-calendar.mdx
@@ -69,16 +69,16 @@ This guide walks you through setting up a Google OAuth 2.0 integration for SurfS
## Running SurfSense with Google Calendar Connector
-Add the Google OAuth environment variables to your Docker run command:
+Add the Google OAuth credentials to your `.env` file (created during [Docker installation](/docs/docker-installation)):
```bash
-docker run -d -p 3000:3000 -p 8000:8000 \
- -v surfsense-data:/data \
- # Google Calendar Connector
- -e GOOGLE_OAUTH_CLIENT_ID=your_google_client_id \
- -e GOOGLE_OAUTH_CLIENT_SECRET=your_google_client_secret \
- -e GOOGLE_CALENDAR_REDIRECT_URI=http://localhost:8000/api/v1/auth/google/calendar/connector/callback \
- --name surfsense \
- --restart unless-stopped \
- ghcr.io/modsetter/surfsense:latest
+GOOGLE_OAUTH_CLIENT_ID=your_google_client_id
+GOOGLE_OAUTH_CLIENT_SECRET=your_google_client_secret
+GOOGLE_CALENDAR_REDIRECT_URI=http://localhost:8000/api/v1/auth/google/calendar/connector/callback
+```
+
+Then restart the services:
+
+```bash
+docker compose up -d
```
diff --git a/surfsense_web/content/docs/connectors/google-drive.mdx b/surfsense_web/content/docs/connectors/google-drive.mdx
index 402b25566..238100860 100644
--- a/surfsense_web/content/docs/connectors/google-drive.mdx
+++ b/surfsense_web/content/docs/connectors/google-drive.mdx
@@ -70,16 +70,16 @@ This guide walks you through setting up a Google OAuth 2.0 integration for SurfS
## Running SurfSense with Google Drive Connector
-Add the Google OAuth environment variables to your Docker run command:
+Add the Google OAuth credentials to your `.env` file (created during [Docker installation](/docs/docker-installation)):
```bash
-docker run -d -p 3000:3000 -p 8000:8000 \
- -v surfsense-data:/data \
- # Google Drive Connector
- -e GOOGLE_OAUTH_CLIENT_ID=your_google_client_id \
- -e GOOGLE_OAUTH_CLIENT_SECRET=your_google_client_secret \
- -e GOOGLE_DRIVE_REDIRECT_URI=http://localhost:8000/api/v1/auth/google/drive/connector/callback \
- --name surfsense \
- --restart unless-stopped \
- ghcr.io/modsetter/surfsense:latest
+GOOGLE_OAUTH_CLIENT_ID=your_google_client_id
+GOOGLE_OAUTH_CLIENT_SECRET=your_google_client_secret
+GOOGLE_DRIVE_REDIRECT_URI=http://localhost:8000/api/v1/auth/google/drive/connector/callback
+```
+
+Then restart the services:
+
+```bash
+docker compose up -d
```
diff --git a/surfsense_web/content/docs/connectors/jira.mdx b/surfsense_web/content/docs/connectors/jira.mdx
index c6b5a26e1..5bddbab8d 100644
--- a/surfsense_web/content/docs/connectors/jira.mdx
+++ b/surfsense_web/content/docs/connectors/jira.mdx
@@ -84,16 +84,16 @@ This guide walks you through setting up an Atlassian OAuth 2.0 (3LO) integration
## Running SurfSense with Jira Connector
-Add the Atlassian environment variables to your Docker run command:
+Add the Atlassian credentials to your `.env` file (created during [Docker installation](/docs/docker-installation)):
```bash
-docker run -d -p 3000:3000 -p 8000:8000 \
- -v surfsense-data:/data \
- # Jira Connector
- -e ATLASSIAN_CLIENT_ID=your_atlassian_client_id \
- -e ATLASSIAN_CLIENT_SECRET=your_atlassian_client_secret \
- -e JIRA_REDIRECT_URI=http://localhost:8000/api/v1/auth/jira/connector/callback \
- --name surfsense \
- --restart unless-stopped \
- ghcr.io/modsetter/surfsense:latest
+ATLASSIAN_CLIENT_ID=your_atlassian_client_id
+ATLASSIAN_CLIENT_SECRET=your_atlassian_client_secret
+JIRA_REDIRECT_URI=http://localhost:8000/api/v1/auth/jira/connector/callback
+```
+
+Then restart the services:
+
+```bash
+docker compose up -d
```
diff --git a/surfsense_web/content/docs/connectors/linear.mdx b/surfsense_web/content/docs/connectors/linear.mdx
index 5fb7bc8c5..3fd82aba1 100644
--- a/surfsense_web/content/docs/connectors/linear.mdx
+++ b/surfsense_web/content/docs/connectors/linear.mdx
@@ -53,17 +53,17 @@ After creating the application, you'll see your OAuth credentials:
## Running SurfSense with Linear Connector
-Add the Linear environment variables to your Docker run command:
+Add the Linear credentials to your `.env` file (created during [Docker installation](/docs/docker-installation)):
```bash
-docker run -d -p 3000:3000 -p 8000:8000 \
- -v surfsense-data:/data \
- # Linear Connector
- -e LINEAR_CLIENT_ID=your_linear_client_id \
- -e LINEAR_CLIENT_SECRET=your_linear_client_secret \
- -e LINEAR_REDIRECT_URI=http://localhost:8000/api/v1/auth/linear/connector/callback \
- --name surfsense \
- --restart unless-stopped \
- ghcr.io/modsetter/surfsense:latest
+LINEAR_CLIENT_ID=your_linear_client_id
+LINEAR_CLIENT_SECRET=your_linear_client_secret
+LINEAR_REDIRECT_URI=http://localhost:8000/api/v1/auth/linear/connector/callback
+```
+
+Then restart the services:
+
+```bash
+docker compose up -d
```
diff --git a/surfsense_web/content/docs/connectors/microsoft-teams.mdx b/surfsense_web/content/docs/connectors/microsoft-teams.mdx
index 53f36c249..5a05be709 100644
--- a/surfsense_web/content/docs/connectors/microsoft-teams.mdx
+++ b/surfsense_web/content/docs/connectors/microsoft-teams.mdx
@@ -90,16 +90,16 @@ After registration, you'll be taken to the app's **Overview** page. Here you'll
## Running SurfSense with Microsoft Teams Connector
-Add the Microsoft Teams environment variables to your Docker run command:
+Add the Microsoft Teams credentials to your `.env` file (created during [Docker installation](/docs/docker-installation)):
```bash
-docker run -d -p 3000:3000 -p 8000:8000 \
- -v surfsense-data:/data \
- # Microsoft Teams Connector
- -e TEAMS_CLIENT_ID=your_microsoft_client_id \
- -e TEAMS_CLIENT_SECRET=your_microsoft_client_secret \
- -e TEAMS_REDIRECT_URI=http://localhost:8000/api/v1/auth/teams/connector/callback \
- --name surfsense \
- --restart unless-stopped \
- ghcr.io/modsetter/surfsense:latest
+TEAMS_CLIENT_ID=your_microsoft_client_id
+TEAMS_CLIENT_SECRET=your_microsoft_client_secret
+TEAMS_REDIRECT_URI=http://localhost:8000/api/v1/auth/teams/connector/callback
+```
+
+Then restart the services:
+
+```bash
+docker compose up -d
```
diff --git a/surfsense_web/content/docs/connectors/notion.mdx b/surfsense_web/content/docs/connectors/notion.mdx
index 6fcda8dae..ca5856340 100644
--- a/surfsense_web/content/docs/connectors/notion.mdx
+++ b/surfsense_web/content/docs/connectors/notion.mdx
@@ -91,16 +91,16 @@ For additional information:
## Running SurfSense with Notion Connector
-Add the Notion environment variables to your Docker run command:
+Add the Notion credentials to your `.env` file (created during [Docker installation](/docs/docker-installation)):
```bash
-docker run -d -p 3000:3000 -p 8000:8000 \
- -v surfsense-data:/data \
- # Notion Connector
- -e NOTION_OAUTH_CLIENT_ID=your_notion_client_id \
- -e NOTION_OAUTH_CLIENT_SECRET=your_notion_client_secret \
- -e NOTION_REDIRECT_URI=http://localhost:8000/api/v1/auth/notion/connector/callback \
- --name surfsense \
- --restart unless-stopped \
- ghcr.io/modsetter/surfsense:latest
+NOTION_OAUTH_CLIENT_ID=your_notion_client_id
+NOTION_OAUTH_CLIENT_SECRET=your_notion_client_secret
+NOTION_REDIRECT_URI=http://localhost:8000/api/v1/auth/notion/connector/callback
+```
+
+Then restart the services:
+
+```bash
+docker compose up -d
```
diff --git a/surfsense_web/content/docs/connectors/slack.mdx b/surfsense_web/content/docs/connectors/slack.mdx
index 072b83343..af38487cc 100644
--- a/surfsense_web/content/docs/connectors/slack.mdx
+++ b/surfsense_web/content/docs/connectors/slack.mdx
@@ -80,16 +80,16 @@ Click **"Add an OAuth Scope"** to add each scope.
## Running SurfSense with Slack Connector
-Add the Slack environment variables to your Docker run command:
+Add the Slack credentials to your `.env` file (created during [Docker installation](/docs/docker-installation)):
```bash
-docker run -d -p 3000:3000 -p 8000:8000 \
- -v surfsense-data:/data \
- # Slack Connector
- -e SLACK_CLIENT_ID=your_slack_client_id \
- -e SLACK_CLIENT_SECRET=your_slack_client_secret \
- -e SLACK_REDIRECT_URI=https://localhost:8000/api/v1/auth/slack/connector/callback \
- --name surfsense \
- --restart unless-stopped \
- ghcr.io/modsetter/surfsense:latest
+SLACK_CLIENT_ID=your_slack_client_id
+SLACK_CLIENT_SECRET=your_slack_client_secret
+SLACK_REDIRECT_URI=http://localhost:8000/api/v1/auth/slack/connector/callback
+```
+
+Then restart the services:
+
+```bash
+docker compose up -d
```
diff --git a/surfsense_web/content/docs/docker-installation.mdx b/surfsense_web/content/docs/docker-installation.mdx
index 3a42746dc..d6a6bca85 100644
--- a/surfsense_web/content/docs/docker-installation.mdx
+++ b/surfsense_web/content/docs/docker-installation.mdx
@@ -4,511 +4,292 @@ description: Setting up SurfSense using Docker
icon: Container
---
+This guide explains how to run SurfSense using Docker, with options ranging from a single-command install to a fully manual setup.
-This guide explains how to run SurfSense using Docker, with options ranging from quick single-command deployment to full production setups.
+## Quick Start
-## Quick Start with Docker 🐳
+### Option 1 — Install Script (recommended)
-Get SurfSense running in seconds with a single command:
+Downloads the compose files, generates a `SECRET_KEY`, starts all services, and sets up [Watchtower](https://github.com/nicholas-fedor/watchtower) for automatic daily updates.
-The all-in-one Docker image bundles PostgreSQL (with pgvector), Redis, and all SurfSense services. Perfect for quick evaluation and development.
+Windows users: install [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) first and run the command below in the Ubuntu terminal.
-
-Make sure to include the `-v surfsense-data:/data` in your Docker command. This ensures your database and files are properly persisted.
-
-
-### One-Line Installation
-
-**Linux/macOS:**
-
```bash
-docker run -d -p 3000:3000 -p 8000:8000 -p 5133:5133 \
- -v surfsense-data:/data \
- --name surfsense \
- --restart unless-stopped \
- ghcr.io/modsetter/surfsense:latest
+curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.sh | bash
```
-**Windows (PowerShell):**
+This creates a `./surfsense/` directory with `docker-compose.yml` and `.env`, then runs `docker compose up -d`.
-```powershell
-docker run -d -p 3000:3000 -p 8000:8000 -p 5133:5133 `
- -v surfsense-data:/data `
- --name surfsense `
- --restart unless-stopped `
- ghcr.io/modsetter/surfsense:latest
-```
-
-> **Note:** A secure `SECRET_KEY` is automatically generated and persisted in the data volume on first run.
-
-### With Custom Configuration
-
-You can pass any [environment variable](/docs/manual-installation#backend-environment-variables) using `-e` flags:
+To skip Watchtower (e.g. in production where you manage updates yourself):
```bash
-docker run -d -p 3000:3000 -p 8000:8000 -p 5133:5133 \
- -v surfsense-data:/data \
- -e EMBEDDING_MODEL=openai://text-embedding-ada-002 \
- -e OPENAI_API_KEY=your_openai_api_key \
- -e AUTH_TYPE=GOOGLE \
- -e GOOGLE_OAUTH_CLIENT_ID=your_google_client_id \
- -e GOOGLE_OAUTH_CLIENT_SECRET=your_google_client_secret \
- -e ETL_SERVICE=LLAMACLOUD \
- -e LLAMA_CLOUD_API_KEY=your_llama_cloud_key \
- --name surfsense \
- --restart unless-stopped \
- ghcr.io/modsetter/surfsense:latest
+curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.sh | bash -s -- --no-watchtower
```
-
-- For Google OAuth, create credentials in the [Google Cloud Console](https://console.cloud.google.com/apis/credentials)
-- For Airtable connector, create an OAuth integration in the [Airtable Developer Hub](https://airtable.com/create/oauth)
-- If deploying behind a reverse proxy with HTTPS, add `-e BACKEND_URL=https://api.yourdomain.com`
-
+To customise the check interval (default 24h), use `--watchtower-interval=SECONDS`.
-### Quick Start with Docker Compose
-
-For easier management with environment files:
+### Option 2 — Manual Docker Compose
```bash
-# Download the quick start compose file
-curl -o docker-compose.yml https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker-compose.quickstart.yml
-
-# Create .env file (optional - for custom configuration)
-cat > .env << EOF
-# EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
-# ETL_SERVICE=DOCLING
-# SECRET_KEY=your_custom_secret_key # Auto-generated if not set
-EOF
-
-# Start SurfSense
+git clone https://github.com/MODSetter/SurfSense.git
+cd SurfSense/docker
+cp .env.example .env
+# Edit .env — at minimum set SECRET_KEY
docker compose up -d
```
After starting, access SurfSense at:
+
- **Frontend**: [http://localhost:3000](http://localhost:3000)
- **Backend API**: [http://localhost:8000](http://localhost:8000)
- **API Docs**: [http://localhost:8000/docs](http://localhost:8000/docs)
-- **Electric-SQL**: [http://localhost:5133](http://localhost:5133)
+- **Electric SQL**: [http://localhost:5133](http://localhost:5133)
-### Quick Start Environment Variables
+---
-| Variable | Description | Default |
-|----------|-------------|---------|
-| SECRET_KEY | JWT secret key (auto-generated if not set) | Auto-generated |
-| AUTH_TYPE | Authentication: `LOCAL` or `GOOGLE` | LOCAL |
-| EMBEDDING_MODEL | Model for embeddings | sentence-transformers/all-MiniLM-L6-v2 |
-| ETL_SERVICE | Document parser: `DOCLING`, `UNSTRUCTURED`, `LLAMACLOUD` | DOCLING |
-| TTS_SERVICE | Text-to-speech for podcasts | local/kokoro |
-| STT_SERVICE | Speech-to-text for audio (model size: tiny, base, small, medium, large) | local/base |
-| REGISTRATION_ENABLED | Allow new user registration | TRUE |
+## Updating
-### Useful Commands
+**Option 1 — Watchtower daemon (recommended, auto-updates every 24 h):**
+
+If you used the install script (Option 1 above), Watchtower is already running. No extra setup needed.
+
+For manual Docker Compose installs (Option 2), start Watchtower separately:
```bash
-# View logs
-docker logs -f surfsense
-
-# Stop SurfSense
-docker stop surfsense
-
-# Start SurfSense
-docker start surfsense
-
-# Remove container (data preserved in volume)
-docker rm surfsense
-
-# Remove container AND data
-docker rm surfsense && docker volume rm surfsense-data
-```
-
-### Updating
-
-To update SurfSense to the latest version, you can use either of the following methods:
-
-
-Your data is safe! The `surfsense-data` volume persists across updates, and database migrations are applied automatically on every startup.
-
-
-**Option 1: Using Watchtower (one-time auto-update)**
-
-[Watchtower](https://github.com/nicholas-fedor/watchtower) can automatically pull the latest image, stop the old container, and restart it with the same options:
-
-```bash
-docker run --rm \
+docker run -d --name watchtower \
+ --restart unless-stopped \
-v /var/run/docker.sock:/var/run/docker.sock \
nickfedor/watchtower \
- --run-once surfsense
+ --label-enable \
+ --interval 86400
+```
+
+**Option 2 — Watchtower one-time update:**
+
+```bash
+docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
+ nickfedor/watchtower --run-once \
+ --label-filter "com.docker.compose.project=surfsense"
```
-Use the `nickfedor/watchtower` fork. The original `containrrr/watchtower` is no longer maintained and may fail with newer Docker versions.
+Use `nickfedor/watchtower`. The original `containrrr/watchtower` is no longer maintained and may fail with newer Docker versions.
-**Option 2: Manual Update**
+**Option 3 — Manual:**
```bash
-# Stop and remove the current container
-docker rm -f surfsense
-
-# Pull the latest image
-docker pull ghcr.io/modsetter/surfsense:latest
-
-# Start with the new image
-docker run -d -p 3000:3000 -p 8000:8000 -p 5133:5133 \
- -v surfsense-data:/data \
- --name surfsense \
- --restart unless-stopped \
- ghcr.io/modsetter/surfsense:latest
+cd surfsense # or SurfSense/docker if you cloned manually
+docker compose pull && docker compose up -d
```
-If you used Docker Compose for the quick start, updating is simpler:
+Database migrations are applied automatically on every startup.
+
+---
+
+## Configuration
+
+All configuration lives in a single `docker/.env` file (or `surfsense/.env` if you used the install script). Copy `.env.example` to `.env` and edit the values you need.
+
+### Required
+
+| Variable | Description |
+|----------|-------------|
+| `SECRET_KEY` | JWT secret key. Generate with: `openssl rand -base64 32`. Auto-generated by the install script. |
+
+### Core Settings
+
+| Variable | Description | Default |
+|----------|-------------|---------|
+| `SURFSENSE_VERSION` | Image tag to deploy. Use `latest`, a clean version (e.g. `0.0.14`), or a specific build (e.g. `0.0.14.1`) | `latest` |
+| `AUTH_TYPE` | Authentication method: `LOCAL` (email/password) or `GOOGLE` (OAuth) | `LOCAL` |
+| `ETL_SERVICE` | Document parsing: `DOCLING` (local), `UNSTRUCTURED`, or `LLAMACLOUD` | `DOCLING` |
+| `EMBEDDING_MODEL` | Embedding model for vector search | `sentence-transformers/all-MiniLM-L6-v2` |
+| `TTS_SERVICE` | Text-to-speech provider for podcasts | `local/kokoro` |
+| `STT_SERVICE` | Speech-to-text provider for audio files | `local/base` |
+| `REGISTRATION_ENABLED` | Allow new user registrations | `TRUE` |
+
+### Ports
+
+| Variable | Description | Default |
+|----------|-------------|---------|
+| `FRONTEND_PORT` | Frontend service port | `3000` |
+| `BACKEND_PORT` | Backend API service port | `8000` |
+| `ELECTRIC_PORT` | Electric SQL service port | `5133` |
+
+### Custom Domain / Reverse Proxy
+
+Only set these if serving SurfSense on a real domain via a reverse proxy (Caddy, Nginx, Cloudflare Tunnel, etc.). Leave commented out for standard localhost deployments.
+
+| Variable | Description |
+|----------|-------------|
+| `NEXT_FRONTEND_URL` | Public frontend URL (e.g. `https://app.yourdomain.com`) |
+| `BACKEND_URL` | Public backend URL for OAuth callbacks (e.g. `https://api.yourdomain.com`) |
+| `NEXT_PUBLIC_FASTAPI_BACKEND_URL` | Backend URL used by the frontend (e.g. `https://api.yourdomain.com`) |
+| `NEXT_PUBLIC_ELECTRIC_URL` | Electric SQL URL used by the frontend (e.g. `https://electric.yourdomain.com`) |
+
+### Database
+
+Defaults work out of the box. Change for security in production.
+
+| Variable | Description | Default |
+|----------|-------------|---------|
+| `DB_USER` | PostgreSQL username | `surfsense` |
+| `DB_PASSWORD` | PostgreSQL password | `surfsense` |
+| `DB_NAME` | PostgreSQL database name | `surfsense` |
+| `DB_HOST` | PostgreSQL host | `db` |
+| `DB_PORT` | PostgreSQL port | `5432` |
+| `DB_SSLMODE` | SSL mode: `disable`, `require`, `verify-ca`, `verify-full` | `disable` |
+| `DATABASE_URL` | Full connection URL override. Use for managed databases (RDS, Supabase, etc.) | *(built from above)* |
+
+### Electric SQL
+
+| Variable | Description | Default |
+|----------|-------------|---------|
+| `ELECTRIC_DB_USER` | Replication user for Electric SQL | `electric` |
+| `ELECTRIC_DB_PASSWORD` | Replication password for Electric SQL | `electric_password` |
+| `ELECTRIC_DATABASE_URL` | Full connection URL override for Electric. Set to `host.docker.internal` when pointing at a local Postgres instance | *(built from above)* |
+
+### Authentication
+
+| Variable | Description |
+|----------|-------------|
+| `GOOGLE_OAUTH_CLIENT_ID` | Google OAuth client ID (required if `AUTH_TYPE=GOOGLE`) |
+| `GOOGLE_OAUTH_CLIENT_SECRET` | Google OAuth client secret (required if `AUTH_TYPE=GOOGLE`) |
+
+Create credentials at the [Google Cloud Console](https://console.cloud.google.com/apis/credentials).
+
+### External API Keys
+
+| Variable | Description |
+|----------|-------------|
+| `FIRECRAWL_API_KEY` | Firecrawl API key for web crawling |
+| `UNSTRUCTURED_API_KEY` | Unstructured.io API key (required if `ETL_SERVICE=UNSTRUCTURED`) |
+| `LLAMA_CLOUD_API_KEY` | LlamaCloud API key (required if `ETL_SERVICE=LLAMACLOUD`) |
+
+### Connector OAuth Keys
+
+Uncomment the connectors you want to use. Redirect URIs follow the pattern `http://localhost:8000/api/v1/auth//connector/callback`.
+
+| Connector | Variables |
+|-----------|-----------|
+| Google Drive / Gmail / Calendar | `GOOGLE_DRIVE_REDIRECT_URI`, `GOOGLE_GMAIL_REDIRECT_URI`, `GOOGLE_CALENDAR_REDIRECT_URI` |
+| Notion | `NOTION_CLIENT_ID`, `NOTION_CLIENT_SECRET`, `NOTION_REDIRECT_URI` |
+| Slack | `SLACK_CLIENT_ID`, `SLACK_CLIENT_SECRET`, `SLACK_REDIRECT_URI` |
+| Discord | `DISCORD_CLIENT_ID`, `DISCORD_CLIENT_SECRET`, `DISCORD_BOT_TOKEN`, `DISCORD_REDIRECT_URI` |
+| Jira & Confluence | `ATLASSIAN_CLIENT_ID`, `ATLASSIAN_CLIENT_SECRET`, `JIRA_REDIRECT_URI`, `CONFLUENCE_REDIRECT_URI` |
+| Linear | `LINEAR_CLIENT_ID`, `LINEAR_CLIENT_SECRET`, `LINEAR_REDIRECT_URI` |
+| ClickUp | `CLICKUP_CLIENT_ID`, `CLICKUP_CLIENT_SECRET`, `CLICKUP_REDIRECT_URI` |
+| Airtable | `AIRTABLE_CLIENT_ID`, `AIRTABLE_CLIENT_SECRET`, `AIRTABLE_REDIRECT_URI` |
+| Microsoft Teams | `TEAMS_CLIENT_ID`, `TEAMS_CLIENT_SECRET`, `TEAMS_REDIRECT_URI` |
+
+For Airtable, create an OAuth integration at the [Airtable Developer Hub](https://airtable.com/create/oauth).
+
+### Observability (optional)
+
+| Variable | Description |
+|----------|-------------|
+| `LANGSMITH_TRACING` | Enable LangSmith tracing (`true` / `false`) |
+| `LANGSMITH_ENDPOINT` | LangSmith API endpoint |
+| `LANGSMITH_API_KEY` | LangSmith API key |
+| `LANGSMITH_PROJECT` | LangSmith project name |
+
+### Advanced (optional)
+
+| Variable | Description | Default |
+|----------|-------------|---------|
+| `SCHEDULE_CHECKER_INTERVAL` | How often to check for scheduled connector tasks (e.g. `5m`, `1h`) | `5m` |
+| `RERANKERS_ENABLED` | Enable document reranking for improved search | `FALSE` |
+| `RERANKERS_MODEL_NAME` | Reranker model name (e.g. `ms-marco-MiniLM-L-12-v2`) | |
+| `RERANKERS_MODEL_TYPE` | Reranker model type (e.g. `flashrank`) | |
+| `PAGES_LIMIT` | Max pages per user for ETL services | unlimited |
+
+---
+
+## Docker Services
+
+| Service | Description |
+|---------|-------------|
+| `db` | PostgreSQL with pgvector extension |
+| `redis` | Message broker for Celery |
+| `backend` | FastAPI application server |
+| `celery_worker` | Background task processing (document indexing, etc.) |
+| `celery_beat` | Periodic task scheduler (connector sync) |
+| `electric` | Electric SQL — real-time sync for the frontend |
+| `frontend` | Next.js web application |
+
+All services start automatically with `docker compose up -d`.
+
+The backend includes a health check — dependent services (workers, frontend) wait until the API is fully ready before starting. You can monitor startup progress with `docker compose ps` (look for `(health: starting)` → `(healthy)`).
+
+---
+
+## Development Compose File
+
+If you're contributing to SurfSense and want to build from source, use `docker-compose.dev.yml` instead:
```bash
-docker compose -f docker-compose.quickstart.yml pull
-docker compose -f docker-compose.quickstart.yml up -d
+cd SurfSense/docker
+docker compose -f docker-compose.dev.yml up --build
+```
+
+This file builds the backend and frontend from your local source code (instead of pulling prebuilt images) and includes pgAdmin for database inspection at [http://localhost:5050](http://localhost:5050). Use the production `docker-compose.yml` for all other cases.
+
+The following `.env` variables are **only used by the dev compose file** (they have no effect on the production `docker-compose.yml`):
+
+| Variable | Description | Default |
+|----------|-------------|---------|
+| `PGADMIN_PORT` | pgAdmin web UI port | `5050` |
+| `PGADMIN_DEFAULT_EMAIL` | pgAdmin login email | `admin@surfsense.com` |
+| `PGADMIN_DEFAULT_PASSWORD` | pgAdmin login password | `surfsense` |
+| `REDIS_PORT` | Exposed Redis port (internal-only in prod) | `6379` |
+| `NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE` | Frontend build arg for auth type | `LOCAL` |
+| `NEXT_PUBLIC_ETL_SERVICE` | Frontend build arg for ETL service | `DOCLING` |
+| `NEXT_PUBLIC_DEPLOYMENT_MODE` | Frontend build arg for deployment mode | `self-hosted` |
+| `NEXT_PUBLIC_ELECTRIC_AUTH_MODE` | Frontend build arg for Electric auth | `insecure` |
+
+In the production compose file, the `NEXT_PUBLIC_*` frontend variables are automatically derived from `AUTH_TYPE`, `ETL_SERVICE`, and the port settings. In the dev compose file, they are passed as build args since the frontend is built from source.
+
+---
+
+## Migrating from the All-in-One Container
+
+
+If you were previously using `docker-compose.quickstart.yml` (the legacy all-in-one `surfsense` container), your data lives in a `surfsense-data` volume and requires a **one-time migration** before switching to the current setup. PostgreSQL has been upgraded from version 14 to 17, so a simple volume swap will not work.
+
+See the full step-by-step guide: [Migrate from the All-in-One Container](/docs/how-to/migrate-from-allinone).
+
+
+---
+
+## Useful Commands
+
+```bash
+# View logs (all services)
+docker compose logs -f
+
+# View logs for a specific service
+docker compose logs -f backend
+docker compose logs -f electric
+
+# Stop all services
+docker compose down
+
+# Restart a specific service
+docker compose restart backend
+
+# Stop and remove all containers + volumes (destructive!)
+docker compose down -v
```
---
-## Full Docker Compose Setup (Production)
-
-For production deployments with separate services and more control, use the full Docker Compose setup below.
-
-## Prerequisites
-
-Before you begin, ensure you have:
-
-- [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/) installed on your machine
-- [Git](https://git-scm.com/downloads) (to clone the repository)
-- Completed all the [prerequisite setup steps](/docs) including:
- - Auth setup
- - **File Processing ETL Service** (choose one):
- - Unstructured.io API key (Supports 34+ formats)
- - LlamaIndex API key (enhanced parsing, supports 50+ formats)
- - Docling (local processing, no API key required, supports PDF, Office docs, images, HTML, CSV)
- - Other required API keys
-
-## Installation Steps
-
-1. **Configure Environment Variables**
- Set up the necessary environment variables:
-
- **Linux/macOS:**
-
- ```bash
- # Copy example environment files
- cp surfsense_backend/.env.example surfsense_backend/.env
- cp surfsense_web/.env.example surfsense_web/.env
- cp .env.example .env # For Docker-specific settings
- ```
-
- **Windows (Command Prompt):**
-
- ```cmd
- copy surfsense_backend\.env.example surfsense_backend\.env
- copy surfsense_web\.env.example surfsense_web\.env
- copy .env.example .env
- ```
-
- **Windows (PowerShell):**
-
- ```powershell
- Copy-Item -Path surfsense_backend\.env.example -Destination surfsense_backend\.env
- Copy-Item -Path surfsense_web\.env.example -Destination surfsense_web\.env
- Copy-Item -Path .env.example -Destination .env
- ```
-
- Edit all `.env` files and fill in the required values:
-
-### Docker-Specific Environment Variables (Optional)
-
-| ENV VARIABLE | DESCRIPTION | DEFAULT VALUE |
-|----------------------------|-----------------------------------------------------------------------------|---------------------|
-| FRONTEND_PORT | Port for the frontend service | 3000 |
-| BACKEND_PORT | Port for the backend API service | 8000 |
-| POSTGRES_PORT | Port for the PostgreSQL database | 5432 |
-| PGADMIN_PORT | Port for pgAdmin web interface | 5050 |
-| REDIS_PORT | Port for Redis (used by Celery) | 6379 |
-| FLOWER_PORT | Port for Flower (Celery monitoring tool) | 5555 |
-| POSTGRES_USER | PostgreSQL username | postgres |
-| POSTGRES_PASSWORD | PostgreSQL password | postgres |
-| POSTGRES_DB | PostgreSQL database name | surfsense |
-| PGADMIN_DEFAULT_EMAIL | Email for pgAdmin login | admin@surfsense.com |
-| PGADMIN_DEFAULT_PASSWORD | Password for pgAdmin login | surfsense |
-| NEXT_PUBLIC_FASTAPI_BACKEND_URL | URL of the backend API (used by frontend during build and runtime) | http://localhost:8000 |
-| NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE | Authentication method for frontend: `LOCAL` or `GOOGLE` | LOCAL |
-| NEXT_PUBLIC_ETL_SERVICE | Document parsing service for frontend UI: `UNSTRUCTURED`, `LLAMACLOUD`, or `DOCLING` | DOCLING |
-| ELECTRIC_PORT | Port for Electric-SQL service | 5133 |
-| POSTGRES_HOST | PostgreSQL host for Electric connection (`db` for Docker PostgreSQL, `host.docker.internal` for local PostgreSQL) | db |
-| ELECTRIC_DB_USER | PostgreSQL username for Electric connection | electric |
-| ELECTRIC_DB_PASSWORD | PostgreSQL password for Electric connection | electric_password |
-| NEXT_PUBLIC_ELECTRIC_URL | URL for Electric-SQL service (used by frontend) | http://localhost:5133 |
-
-**Note:** Frontend environment variables with the `NEXT_PUBLIC_` prefix are embedded into the Next.js production build at build time. Since the frontend now runs as a production build in Docker, these variables must be set in the root `.env` file (Docker-specific configuration) and will be passed as build arguments during the Docker build process.
-
-**Backend Environment Variables:**
-
-| ENV VARIABLE | DESCRIPTION |
-| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| DATABASE_URL | PostgreSQL connection string (e.g., `postgresql+asyncpg://postgres:postgres@localhost:5432/surfsense`) |
-| SECRET_KEY | JWT Secret key for authentication (should be a secure random string) |
-| NEXT_FRONTEND_URL | URL where your frontend application is hosted (e.g., `http://localhost:3000`) |
-| BACKEND_URL | (Optional) Public URL of the backend for OAuth callbacks (e.g., `https://api.yourdomain.com`). Required when running behind a reverse proxy with HTTPS. Used to set correct OAuth redirect URLs and secure cookies. |
-| AUTH_TYPE | Authentication method: `GOOGLE` for OAuth with Google, `LOCAL` for email/password authentication |
-| GOOGLE_OAUTH_CLIENT_ID | (Optional) Client ID from Google Cloud Console (required if AUTH_TYPE=GOOGLE) |
-| GOOGLE_OAUTH_CLIENT_SECRET | (Optional) Client secret from Google Cloud Console (required if AUTH_TYPE=GOOGLE) |
-| ELECTRIC_DB_USER | (Optional) PostgreSQL username for Electric-SQL connection (default: `electric`) |
-| ELECTRIC_DB_PASSWORD | (Optional) PostgreSQL password for Electric-SQL connection (default: `electric_password`) |
-| EMBEDDING_MODEL | Name of the embedding model (e.g., `sentence-transformers/all-MiniLM-L6-v2`, `openai://text-embedding-ada-002`) |
-| RERANKERS_ENABLED | (Optional) Enable or disable document reranking for improved search results (e.g., `TRUE` or `FALSE`, default: `FALSE`) |
-| RERANKERS_MODEL_NAME | Name of the reranker model (e.g., `ms-marco-MiniLM-L-12-v2`) (required if RERANKERS_ENABLED=TRUE) |
-| RERANKERS_MODEL_TYPE | Type of reranker model (e.g., `flashrank`) (required if RERANKERS_ENABLED=TRUE) |
-| TTS_SERVICE | Text-to-Speech API provider for Podcasts (e.g., `local/kokoro`, `openai/tts-1`). See [supported providers](https://docs.litellm.ai/docs/text_to_speech#supported-providers) |
-| TTS_SERVICE_API_KEY | (Optional if local) API key for the Text-to-Speech service |
-| TTS_SERVICE_API_BASE | (Optional) Custom API base URL for the Text-to-Speech service |
-| STT_SERVICE | Speech-to-Text API provider for Audio Files (e.g., `local/base`, `openai/whisper-1`). See [supported providers](https://docs.litellm.ai/docs/audio_transcription#supported-providers) |
-| STT_SERVICE_API_KEY | (Optional if local) API key for the Speech-to-Text service |
-| STT_SERVICE_API_BASE | (Optional) Custom API base URL for the Speech-to-Text service |
-| FIRECRAWL_API_KEY | API key for Firecrawl service for web crawling |
-| ETL_SERVICE | Document parsing service: `UNSTRUCTURED` (supports 34+ formats), `LLAMACLOUD` (supports 50+ formats including legacy document types), or `DOCLING` (local processing, supports PDF, Office docs, images, HTML, CSV) |
-| UNSTRUCTURED_API_KEY | API key for Unstructured.io service for document parsing (required if ETL_SERVICE=UNSTRUCTURED) |
-| LLAMA_CLOUD_API_KEY | API key for LlamaCloud service for document parsing (required if ETL_SERVICE=LLAMACLOUD) |
-| CELERY_BROKER_URL | Redis connection URL for Celery broker (e.g., `redis://localhost:6379/0`) |
-| CELERY_RESULT_BACKEND | Redis connection URL for Celery result backend (e.g., `redis://localhost:6379/0`) |
-| SCHEDULE_CHECKER_INTERVAL | (Optional) How often to check for scheduled connector tasks. Format: `` where unit is `m` (minutes) or `h` (hours). Examples: `1m`, `5m`, `1h`, `2h` (default: `1m`) |
-| REGISTRATION_ENABLED | (Optional) Enable or disable new user registration (e.g., `TRUE` or `FALSE`, default: `TRUE`) |
-| PAGES_LIMIT | (Optional) Maximum pages limit per user for ETL services (default: `999999999` for unlimited in OSS version) |
-
-**Google Connector OAuth Configuration:**
-| ENV VARIABLE | DESCRIPTION |
-| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| GOOGLE_CALENDAR_REDIRECT_URI | (Optional) Redirect URI for Google Calendar connector OAuth callback (e.g., `http://localhost:8000/api/v1/auth/google/calendar/connector/callback`) |
-| GOOGLE_GMAIL_REDIRECT_URI | (Optional) Redirect URI for Gmail connector OAuth callback (e.g., `http://localhost:8000/api/v1/auth/google/gmail/connector/callback`) |
-| GOOGLE_DRIVE_REDIRECT_URI | (Optional) Redirect URI for Google Drive connector OAuth callback (e.g., `http://localhost:8000/api/v1/auth/google/drive/connector/callback`) |
-
-**Connector OAuth Configurations (Optional):**
-
-| ENV VARIABLE | DESCRIPTION |
-| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| AIRTABLE_CLIENT_ID | (Optional) Airtable OAuth client ID from [Airtable Developer Hub](https://airtable.com/create/oauth) |
-| AIRTABLE_CLIENT_SECRET | (Optional) Airtable OAuth client secret |
-| AIRTABLE_REDIRECT_URI | (Optional) Redirect URI for Airtable connector OAuth callback (e.g., `http://localhost:8000/api/v1/auth/airtable/connector/callback`) |
-| CLICKUP_CLIENT_ID | (Optional) ClickUp OAuth client ID |
-| CLICKUP_CLIENT_SECRET | (Optional) ClickUp OAuth client secret |
-| CLICKUP_REDIRECT_URI | (Optional) Redirect URI for ClickUp connector OAuth callback (e.g., `http://localhost:8000/api/v1/auth/clickup/connector/callback`) |
-| DISCORD_CLIENT_ID | (Optional) Discord OAuth client ID |
-| DISCORD_CLIENT_SECRET | (Optional) Discord OAuth client secret |
-| DISCORD_REDIRECT_URI | (Optional) Redirect URI for Discord connector OAuth callback (e.g., `http://localhost:8000/api/v1/auth/discord/connector/callback`) |
-| DISCORD_BOT_TOKEN | (Optional) Discord bot token from Developer Portal |
-| ATLASSIAN_CLIENT_ID | (Optional) Atlassian OAuth client ID (for Jira and Confluence) |
-| ATLASSIAN_CLIENT_SECRET | (Optional) Atlassian OAuth client secret |
-| JIRA_REDIRECT_URI | (Optional) Redirect URI for Jira connector OAuth callback (e.g., `http://localhost:8000/api/v1/auth/jira/connector/callback`) |
-| CONFLUENCE_REDIRECT_URI | (Optional) Redirect URI for Confluence connector OAuth callback (e.g., `http://localhost:8000/api/v1/auth/confluence/connector/callback`) |
-| LINEAR_CLIENT_ID | (Optional) Linear OAuth client ID |
-| LINEAR_CLIENT_SECRET | (Optional) Linear OAuth client secret |
-| LINEAR_REDIRECT_URI | (Optional) Redirect URI for Linear connector OAuth callback (e.g., `http://localhost:8000/api/v1/auth/linear/connector/callback`) |
-| NOTION_CLIENT_ID | (Optional) Notion OAuth client ID |
-| NOTION_CLIENT_SECRET | (Optional) Notion OAuth client secret |
-| NOTION_REDIRECT_URI | (Optional) Redirect URI for Notion connector OAuth callback (e.g., `http://localhost:8000/api/v1/auth/notion/connector/callback`) |
-| SLACK_CLIENT_ID | (Optional) Slack OAuth client ID |
-| SLACK_CLIENT_SECRET | (Optional) Slack OAuth client secret |
-| SLACK_REDIRECT_URI | (Optional) Redirect URI for Slack connector OAuth callback (e.g., `http://localhost:8000/api/v1/auth/slack/connector/callback`) |
-| TEAMS_CLIENT_ID | (Optional) Microsoft Teams OAuth client ID |
-| TEAMS_CLIENT_SECRET | (Optional) Microsoft Teams OAuth client secret |
-| TEAMS_REDIRECT_URI | (Optional) Redirect URI for Teams connector OAuth callback (e.g., `http://localhost:8000/api/v1/auth/teams/connector/callback`) |
-
-
-**Optional Backend LangSmith Observability:**
-| ENV VARIABLE | DESCRIPTION |
-|--------------|-------------|
-| LANGSMITH_TRACING | Enable LangSmith tracing (e.g., `true`) |
-| LANGSMITH_ENDPOINT | LangSmith API endpoint (e.g., `https://api.smith.langchain.com`) |
-| LANGSMITH_API_KEY | Your LangSmith API key |
-| LANGSMITH_PROJECT | LangSmith project name (e.g., `surfsense`) |
-
-**Backend Uvicorn Server Configuration:**
-| ENV VARIABLE | DESCRIPTION | DEFAULT VALUE |
-|------------------------------|---------------------------------------------|---------------|
-| UVICORN_HOST | Host address to bind the server | 0.0.0.0 |
-| UVICORN_PORT | Port to run the backend API | 8000 |
-| UVICORN_LOG_LEVEL | Logging level (e.g., info, debug, warning) | info |
-| UVICORN_PROXY_HEADERS | Enable/disable proxy headers | false |
-| UVICORN_FORWARDED_ALLOW_IPS | Comma-separated list of allowed IPs | 127.0.0.1 |
-| UVICORN_WORKERS | Number of worker processes | 1 |
-| UVICORN_ACCESS_LOG | Enable/disable access log (true/false) | true |
-| UVICORN_LOOP | Event loop implementation | auto |
-| UVICORN_HTTP | HTTP protocol implementation | auto |
-| UVICORN_WS | WebSocket protocol implementation | auto |
-| UVICORN_LIFESPAN | Lifespan implementation | auto |
-| UVICORN_LOG_CONFIG | Path to logging config file or empty string | |
-| UVICORN_SERVER_HEADER | Enable/disable Server header | true |
-| UVICORN_DATE_HEADER | Enable/disable Date header | true |
-| UVICORN_LIMIT_CONCURRENCY | Max concurrent connections | |
-| UVICORN_LIMIT_MAX_REQUESTS | Max requests before worker restart | |
-| UVICORN_TIMEOUT_KEEP_ALIVE | Keep-alive timeout (seconds) | 5 |
-| UVICORN_TIMEOUT_NOTIFY | Worker shutdown notification timeout (sec) | 30 |
-| UVICORN_SSL_KEYFILE | Path to SSL key file | |
-| UVICORN_SSL_CERTFILE | Path to SSL certificate file | |
-| UVICORN_SSL_KEYFILE_PASSWORD | Password for SSL key file | |
-| UVICORN_SSL_VERSION | SSL version | |
-| UVICORN_SSL_CERT_REQS | SSL certificate requirements | |
-| UVICORN_SSL_CA_CERTS | Path to CA certificates file | |
-| UVICORN_SSL_CIPHERS | SSL ciphers | |
-| UVICORN_HEADERS | Comma-separated list of headers | |
-| UVICORN_USE_COLORS | Enable/disable colored logs | true |
-| UVICORN_UDS | Unix domain socket path | |
-| UVICORN_FD | File descriptor to bind to | |
-| UVICORN_ROOT_PATH | Root path for the application | |
-
-For more details, see the [Uvicorn documentation](https://www.uvicorn.org/#command-line-options).
-
-### Frontend Environment Variables
-
-**Important:** Frontend environment variables are now configured in the **Docker-Specific Environment Variables** section above since the Next.js application runs as a production build in Docker. The following `NEXT_PUBLIC_*` variables should be set in your root `.env` file:
-
-- `NEXT_PUBLIC_FASTAPI_BACKEND_URL` - URL of the backend service
-- `NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE` - Authentication method (`LOCAL` or `GOOGLE`)
-- `NEXT_PUBLIC_ETL_SERVICE` - Document parsing service (should match backend `ETL_SERVICE`)
-- `NEXT_PUBLIC_ELECTRIC_URL` - URL for Electric-SQL service (default: `http://localhost:5133`)
-- `NEXT_PUBLIC_ELECTRIC_AUTH_MODE` - Electric-SQL authentication mode (default: `insecure`)
-
-These variables are embedded into the application during the Docker build process and affect the frontend's behavior and available features.
-
-2. **Build and Start Containers**
-
- Start the Docker containers:
-
- **Linux/macOS/Windows:**
-
- ```bash
- docker compose up --build
- ```
-
- To run in detached mode (in the background):
-
- **Linux/macOS/Windows:**
-
- ```bash
- docker compose up -d
- ```
-
- **Note for Windows users:** If you're using older Docker Desktop versions, you might need to use `docker compose` (with a space) instead of `docker compose`.
-
-3. **Access the Applications**
-
- Once the containers are running, you can access:
-
- - Frontend: [http://localhost:3000](http://localhost:3000)
- - Backend API: [http://localhost:8000](http://localhost:8000)
- - API Documentation: [http://localhost:8000/docs](http://localhost:8000/docs)
- - Electric-SQL: [http://localhost:5133](http://localhost:5133)
- - pgAdmin: [http://localhost:5050](http://localhost:5050)
-
-## Docker Services Overview
-
-The Docker setup includes several services that work together:
-
-- **Backend**: FastAPI application server
-- **Frontend**: Next.js web application
-- **PostgreSQL (db)**: Database with pgvector extension
-- **Redis**: Message broker for Celery
-- **Electric-SQL**: Real-time sync service for database operations
-- **Celery Worker**: Handles background tasks (document processing, indexing, etc.)
-- **Celery Beat**: Scheduler for periodic tasks (enables scheduled connector indexing)
- - The schedule interval can be configured using the `SCHEDULE_CHECKER_INTERVAL` environment variable in your backend `.env` file
- - Default: checks every minute for connectors that need indexing
-- **pgAdmin**: Database management interface
-
-All services start automatically with `docker compose up`. The Celery Beat service ensures that periodic indexing functionality works out of the box.
-
-## Using pgAdmin
-
-pgAdmin is included in the Docker setup to help manage your PostgreSQL database. To connect:
-
-1. Open pgAdmin at [http://localhost:5050](http://localhost:5050)
-2. Login with the credentials from your `.env` file (default: admin@surfsense.com / surfsense)
-3. Right-click "Servers" > "Create" > "Server"
-4. In the "General" tab, name your connection (e.g., "SurfSense DB")
-5. In the "Connection" tab:
- - Host: `db`
- - Port: `5432`
- - Maintenance database: `surfsense`
- - Username: `postgres` (or your custom POSTGRES_USER)
- - Password: `postgres` (or your custom POSTGRES_PASSWORD)
-6. Click "Save" to connect
-
-## Updating (Full Docker Compose)
-
-To update the full Docker Compose production setup to the latest version:
-
-```bash
-# Pull latest changes
-git pull
-
-# Rebuild and restart containers
-docker compose up --build -d
-```
-
-Database migrations are applied automatically on startup.
-
-## Useful Docker Commands
-
-### Container Management
-
-- **Stop containers:**
-
- **Linux/macOS/Windows:**
-
- ```bash
- docker compose down
- ```
-
-- **View logs:**
-
- **Linux/macOS/Windows:**
-
- ```bash
- # All services
- docker compose logs -f
-
- # Specific service
- docker compose logs -f backend
- docker compose logs -f frontend
- docker compose logs -f db
- ```
-
-- **Restart a specific service:**
-
- **Linux/macOS/Windows:**
-
- ```bash
- docker compose restart backend
- ```
-
-- **Execute commands in a running container:**
-
- **Linux/macOS/Windows:**
-
- ```bash
- # Backend
- docker compose exec backend python -m pytest
-
- # Frontend
- docker compose exec frontend pnpm lint
- ```
-
## Troubleshooting
-- **Linux/macOS:** If you encounter permission errors, you may need to run the docker commands with `sudo`.
-- **Windows:** If you see access denied errors, make sure you're running Command Prompt or PowerShell as Administrator.
-- If ports are already in use, modify the port mappings in the `docker-compose.yml` file.
-- For backend dependency issues, check the `Dockerfile` in the backend directory.
-- For frontend dependency issues, check the `Dockerfile` in the frontend directory.
-- **Windows-specific:** If you encounter line ending issues (CRLF vs LF), configure Git to handle line endings properly with `git config --global core.autocrlf true` before cloning the repository.
-
-## Next Steps
-
-Once your installation is complete, you can start using SurfSense! Navigate to the frontend URL and log in using your Google account.
+- **Ports already in use** — Change the relevant `*_PORT` variable in `.env` and restart.
+- **Permission errors on Linux** — You may need to prefix `docker` commands with `sudo`.
+- **Electric SQL not connecting** — Check `docker compose logs electric`. If it shows `domain does not exist: db`, ensure `ELECTRIC_DATABASE_URL` is not set to a stale value in `.env`.
+- **Real-time updates not working in browser** — Open DevTools → Console and look for `[Electric]` errors. Check that `NEXT_PUBLIC_ELECTRIC_URL` matches the running Electric SQL address.
+- **Line ending issues on Windows** — Run `git config --global core.autocrlf true` before cloning.
diff --git a/surfsense_web/content/docs/how-to/electric-sql.mdx b/surfsense_web/content/docs/how-to/electric-sql.mdx
index 288745850..fb2cf941a 100644
--- a/surfsense_web/content/docs/how-to/electric-sql.mdx
+++ b/surfsense_web/content/docs/how-to/electric-sql.mdx
@@ -3,8 +3,6 @@ title: Electric SQL
description: Setting up Electric SQL for real-time data synchronization in SurfSense
---
-# Electric SQL
-
[Electric SQL](https://electric-sql.com/) enables real-time data synchronization in SurfSense, providing instant updates for inbox items, document indexing status, and connector sync progress without manual refresh. The frontend uses [PGlite](https://pglite.dev/) (a lightweight PostgreSQL in the browser) to maintain a local database that syncs with the backend via Electric SQL.
## What Does Electric SQL Do?
@@ -25,74 +23,29 @@ This means:
## Docker Setup
-### All-in-One Quickstart
-
-The simplest way to run SurfSense with Electric SQL is using the all-in-one Docker image. This bundles everything into a single container:
-
-- PostgreSQL + pgvector (vector database)
-- Redis (task queue)
-- Electric SQL (real-time sync)
-- Backend API
-- Frontend
+The `docker-compose.yml` includes the Electric SQL service. It is pre-configured to connect to the Docker-managed `db` container out of the box.
```bash
-docker run -d \
- -p 3000:3000 \
- -p 8000:8000 \
- -p 5133:5133 \
- -v surfsense-data:/data \
- --name surfsense \
- ghcr.io/modsetter/surfsense:latest
+docker compose up -d
```
-**With custom Electric SQL credentials:**
-
-```bash
-docker run -d \
- -p 3000:3000 \
- -p 8000:8000 \
- -p 5133:5133 \
- -v surfsense-data:/data \
- -e ELECTRIC_DB_USER=your_electric_user \
- -e ELECTRIC_DB_PASSWORD=your_electric_password \
- --name surfsense \
- ghcr.io/modsetter/surfsense:latest
-```
-
-Access SurfSense at `http://localhost:3000`. Electric SQL is automatically configured and running on port 5133.
-
-### Docker Compose
-
-For more control over individual services, use Docker Compose.
-
-**Quickstart (all-in-one image):**
-
-```bash
-docker compose -f docker-compose.quickstart.yml up -d
-```
-
-**Standard setup (separate services):**
-
-The `docker-compose.yml` includes the Electric SQL service configuration:
+The Electric SQL service configuration in `docker-compose.yml`:
```yaml
electric:
- image: electricsql/electric:latest
+ image: electricsql/electric:1.4.6
ports:
- "${ELECTRIC_PORT:-5133}:3000"
environment:
- - DATABASE_URL=${ELECTRIC_DATABASE_URL:-postgresql://${ELECTRIC_DB_USER:-electric}:${ELECTRIC_DB_PASSWORD:-electric_password}@${POSTGRES_HOST:-db}:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-surfsense}?sslmode=disable}
- - ELECTRIC_INSECURE=true
- - ELECTRIC_WRITE_TO_PG_MODE=direct
- restart: unless-stopped
- healthcheck:
- test: ["CMD", "curl", "-f", "http://localhost:3000/v1/health"]
- interval: 10s
- timeout: 5s
- retries: 5
+ DATABASE_URL: ${ELECTRIC_DATABASE_URL:-postgresql://${ELECTRIC_DB_USER:-electric}:${ELECTRIC_DB_PASSWORD:-electric_password}@${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-surfsense}?sslmode=${DB_SSLMODE:-disable}}
+ ELECTRIC_INSECURE: "true"
+ ELECTRIC_WRITE_TO_PG_MODE: direct
+ depends_on:
+ db:
+ condition: service_healthy
```
-No additional configuration is required - Electric SQL is pre-configured to work with the Docker PostgreSQL instance.
+No additional configuration is required — Electric SQL is pre-configured to work with the Docker PostgreSQL instance.
## Manual Setup
@@ -102,19 +55,16 @@ Follow the steps below based on your PostgreSQL setup.
Ensure your environment files are configured. If you haven't set up SurfSense yet, follow the [Manual Installation Guide](/docs/manual-installation) first.
-For Electric SQL, verify these variables are set:
-
-**Root `.env`:**
+For Electric SQL, verify these variables are set in `docker/.env`:
```bash
ELECTRIC_PORT=5133
-POSTGRES_HOST=host.docker.internal # Use 'db' for Docker PostgreSQL instance
ELECTRIC_DB_USER=electric
ELECTRIC_DB_PASSWORD=electric_password
NEXT_PUBLIC_ELECTRIC_URL=http://localhost:5133
```
-**Frontend `.env` (`surfsense_web/.env`):**
+**Frontend (`surfsense_web/.env`):**
```bash
NEXT_PUBLIC_ELECTRIC_URL=http://localhost:5133
@@ -125,32 +75,17 @@ NEXT_PUBLIC_ELECTRIC_AUTH_MODE=insecure
### Option A: Using Docker PostgreSQL
-If you're using the Docker-managed PostgreSQL instance, follow these steps:
-
-**1. Update environment variable:**
-
-In your root `.env` file, set:
+If you're using the Docker-managed PostgreSQL instance, no extra configuration is needed. Just start the services:
```bash
-POSTGRES_HOST=db
+docker compose up -d db electric
```
-**2. Start PostgreSQL and Electric SQL:**
-
-```bash
-docker-compose up -d db electric
-```
-
-**3. Run database migration:**
+Then run the database migration and start the backend:
```bash
cd surfsense_backend
uv run alembic upgrade head
-```
-
-**4. Start the backend:**
-
-```bash
uv run main.py
```
@@ -160,17 +95,17 @@ Electric SQL is now configured and connected to your Docker PostgreSQL database.
### Option B: Using Local PostgreSQL
-If you're using a local PostgreSQL installation, follow these steps:
+If you're using a local PostgreSQL installation (e.g. Postgres.app on macOS), follow these steps:
**1. Enable logical replication in PostgreSQL:**
-Open your `postgresql.conf` file using vim (or your preferred editor):
+Open your `postgresql.conf` file:
```bash
# Common locations:
-# macOS (Homebrew): /opt/homebrew/var/postgresql@15/postgresql.conf
-# Linux: /etc/postgresql/15/main/postgresql.conf
-# Windows: C:\Program Files\PostgreSQL\15\data\postgresql.conf
+# macOS (Postgres.app): ~/Library/Application Support/Postgres/var-17/postgresql.conf
+# macOS (Homebrew): /opt/homebrew/var/postgresql@17/postgresql.conf
+# Linux: /etc/postgresql/17/main/postgresql.conf
sudo vim /path/to/postgresql.conf
```
@@ -178,38 +113,51 @@ sudo vim /path/to/postgresql.conf
Add the following settings:
```ini
-# Enable logical replication (required for Electric SQL)
+# Required for Electric SQL
wal_level = logical
max_replication_slots = 10
max_wal_senders = 10
```
-After saving the changes (`:wq` in vim), restart your PostgreSQL server for the configuration to take effect.
+After saving, restart PostgreSQL for the settings to take effect.
-**2. Update environment variable:**
+**2. Create the Electric replication user:**
-In your root `.env` file, set:
+Connect to your local database as a superuser and run:
-```bash
-POSTGRES_HOST=host.docker.internal
+```sql
+CREATE USER electric WITH REPLICATION PASSWORD 'electric_password';
+GRANT CONNECT ON DATABASE surfsense TO electric;
+GRANT CREATE ON DATABASE surfsense TO electric;
+GRANT USAGE ON SCHEMA public TO electric;
+GRANT SELECT ON ALL TABLES IN SCHEMA public TO electric;
+GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO electric;
+ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO electric;
+ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON SEQUENCES TO electric;
+CREATE PUBLICATION electric_publication_default;
```
-**3. Start Electric SQL:**
+**3. Set `ELECTRIC_DATABASE_URL` in `docker/.env`:**
+
+Uncomment and update this line to point Electric at your local Postgres via `host.docker.internal` (the hostname Docker containers use to reach the host machine):
```bash
-docker-compose up -d electric
+ELECTRIC_DATABASE_URL=postgresql://electric:electric_password@host.docker.internal:5432/surfsense?sslmode=disable
```
-**4. Run database migration:**
+**4. Start Electric SQL only (skip the Docker `db` container):**
+
+```bash
+docker compose up -d --no-deps electric
+```
+
+The `--no-deps` flag starts only the `electric` service without starting the Docker-managed `db` container.
+
+**5. Run database migration and start the backend:**
```bash
cd surfsense_backend
uv run alembic upgrade head
-```
-
-**5. Start the backend:**
-
-```bash
uv run main.py
```
@@ -219,12 +167,13 @@ Electric SQL is now configured and connected to your local PostgreSQL database.
| Variable | Location | Description | Default |
|----------|----------|-------------|---------|
-| `ELECTRIC_PORT` | Root `.env` | Port to expose Electric SQL | `5133` |
-| `POSTGRES_HOST` | Root `.env` | PostgreSQL host (`db` for Docker, `host.docker.internal` for local) | `host.docker.internal` |
-| `ELECTRIC_DB_USER` | Root `.env` | Database user for Electric | `electric` |
-| `ELECTRIC_DB_PASSWORD` | Root `.env` | Database password for Electric | `electric_password` |
+| `ELECTRIC_PORT` | `docker/.env` | Port to expose Electric SQL | `5133` |
+| `ELECTRIC_DB_USER` | `docker/.env` | Database user for Electric replication | `electric` |
+| `ELECTRIC_DB_PASSWORD` | `docker/.env` | Database password for Electric replication | `electric_password` |
+| `ELECTRIC_DATABASE_URL` | `docker/.env` | Full connection URL override for Electric. Set to use `host.docker.internal` when pointing at a local Postgres instance | *(built from above defaults)* |
| `NEXT_PUBLIC_ELECTRIC_URL` | Frontend `.env` | Electric SQL server URL (PGlite connects to this) | `http://localhost:5133` |
| `NEXT_PUBLIC_ELECTRIC_AUTH_MODE` | Frontend `.env` | Authentication mode (`insecure` for dev, `secure` for production) | `insecure` |
+
## Verify Setup
To verify Electric SQL is running correctly:
@@ -262,7 +211,7 @@ You should receive:
### Data Not Syncing
-- Check Electric SQL logs: `docker logs electric`
+- Check Electric SQL logs: `docker compose logs electric`
- Verify PostgreSQL replication is working
- Ensure the Electric user has proper table permissions
diff --git a/surfsense_web/content/docs/how-to/meta.json b/surfsense_web/content/docs/how-to/meta.json
index 83a5ea9f4..c8ecb05d9 100644
--- a/surfsense_web/content/docs/how-to/meta.json
+++ b/surfsense_web/content/docs/how-to/meta.json
@@ -1,6 +1,6 @@
{
"title": "How to",
+ "pages": ["electric-sql", "realtime-collaboration", "migrate-from-allinone"],
"icon": "BookOpen",
- "pages": ["electric-sql", "realtime-collaboration"],
"defaultOpen": false
}
diff --git a/surfsense_web/content/docs/how-to/migrate-from-allinone.mdx b/surfsense_web/content/docs/how-to/migrate-from-allinone.mdx
new file mode 100644
index 000000000..3de0b043d
--- /dev/null
+++ b/surfsense_web/content/docs/how-to/migrate-from-allinone.mdx
@@ -0,0 +1,195 @@
+---
+title: Migrate from the All-in-One Container
+description: How to migrate your data from the legacy surfsense all-in-one Docker image to the current multi-container setup
+---
+
+The original SurfSense all-in-one image (`ghcr.io/modsetter/surfsense:latest`, run via `docker-compose.quickstart.yml`) stored all data — PostgreSQL, Redis, and configuration — in a single Docker volume named `surfsense-data`. The current setup uses separate named volumes and has upgraded PostgreSQL from **version 14 to 17**.
+
+Because PostgreSQL data files are not compatible between major versions, a **logical dump and restore** is required. This is a one-time migration.
+
+
+This guide only applies to users who ran the legacy `docker-compose.quickstart.yml` (the all-in-one `surfsense` container). If you were already using `docker/docker-compose.yml`, you do not need to migrate.
+
+
+---
+
+## Option A — One command (recommended)
+
+`install.sh` detects the legacy `surfsense-data` volume and handles the full migration automatically — no separate migration script needed. Just run the same install command you would use for a fresh install:
+
+```bash
+curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.sh | bash
+```
+
+**What it does automatically:**
+
+1. Downloads all SurfSense files (including `migrate-database.sh`) into `./surfsense/`
+2. Detects the `surfsense-data` volume and enters migration mode
+3. Stops the old all-in-one container if it is still running
+4. Starts a temporary PostgreSQL 14 container and dumps your database
+5. Recovers your `SECRET_KEY` from the old volume
+6. Starts PostgreSQL 17, restores the dump, runs a smoke test
+7. Starts all services
+
+Your original `surfsense-data` volume is **never deleted** — you remove it manually after verifying.
+
+### After it completes
+
+1. Open [http://localhost:3000](http://localhost:3000) and confirm your data is intact.
+2. Once satisfied, remove the old volume (irreversible):
+ ```bash
+ docker volume rm surfsense-data
+ ```
+3. Delete the dump file once you no longer need it as a backup:
+ ```bash
+ rm ./surfsense_migration_backup.sql
+ ```
+
+### If the migration fails mid-way
+
+The dump file is saved to `./surfsense_migration_backup.sql` as a checkpoint. Simply re-run `install.sh` — it will detect the existing dump and skip straight to the restore step without re-extracting.
+
+---
+
+## Option B — Manual migration script (custom credentials)
+
+If you launched the old all-in-one container with custom database credentials (`POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB` environment variables), the automatic path will use wrong credentials. Run `migrate-database.sh` manually first:
+
+```bash
+# 1. Extract data with your custom credentials
+bash ./surfsense/scripts/migrate-database.sh --db-user myuser --db-password mypass --db-name mydb
+
+# 2. Install and restore (detects the dump automatically)
+curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.sh | bash
+```
+
+Or download and run if you haven't run `install.sh` yet:
+
+```bash
+curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/migrate-database.sh -o migrate-database.sh
+bash migrate-database.sh --db-user myuser --db-password mypass --db-name mydb
+```
+
+### Migration script options
+
+| Flag | Description | Default |
+|------|-------------|---------|
+| `--db-user USER` | Old PostgreSQL username | `surfsense` |
+| `--db-password PASS` | Old PostgreSQL password | `surfsense` |
+| `--db-name NAME` | Old PostgreSQL database | `surfsense` |
+| `--yes` / `-y` | Skip confirmation prompts (used automatically by `install.sh`) | — |
+
+---
+
+## Option C — Manual steps
+
+For users who prefer full control or whose platform doesn't support bash scripts (e.g. Windows without WSL2).
+
+### Step 1 — Stop the old all-in-one container
+
+Before mounting the `surfsense-data` volume into a new container, stop the existing one to prevent two PostgreSQL processes from writing to the same data directory:
+
+```bash
+docker stop surfsense 2>/dev/null || true
+```
+
+### Step 2 — Start a temporary PostgreSQL 14 container
+
+```bash
+docker run -d --name surfsense-pg14-temp \
+ -v surfsense-data:/data \
+ -e PGDATA=/data/postgres \
+ -e POSTGRES_USER=surfsense \
+ -e POSTGRES_PASSWORD=surfsense \
+ -e POSTGRES_DB=surfsense \
+ pgvector/pgvector:pg14
+```
+
+Wait ~10 seconds, then confirm it is healthy:
+
+```bash
+docker exec surfsense-pg14-temp pg_isready -U surfsense
+```
+
+### Step 3 — Dump the database
+
+```bash
+docker exec -e PGPASSWORD=surfsense surfsense-pg14-temp \
+ pg_dump -U surfsense surfsense > surfsense_backup.sql
+```
+
+### Step 4 — Recover your SECRET\_KEY
+
+```bash
+docker run --rm -v surfsense-data:/data alpine cat /data/.secret_key
+```
+
+### Step 5 — Set up the new stack
+
+```bash
+mkdir -p surfsense/scripts
+curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/docker-compose.yml -o surfsense/docker-compose.yml
+curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/.env.example -o surfsense/.env.example
+curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/postgresql.conf -o surfsense/postgresql.conf
+curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/init-electric-user.sh -o surfsense/scripts/init-electric-user.sh
+chmod +x surfsense/scripts/init-electric-user.sh
+cp surfsense/.env.example surfsense/.env
+```
+
+Set `SECRET_KEY` in `surfsense/.env` to the value from Step 4.
+
+### Step 6 — Start PostgreSQL 17 and restore
+
+```bash
+cd surfsense
+docker compose up -d db
+docker compose exec db pg_isready -U surfsense # wait until ready
+docker compose exec -T db psql -U surfsense -d surfsense < ../surfsense_backup.sql
+```
+
+### Step 7 — Start all services
+
+```bash
+docker compose up -d
+```
+
+### Step 8 — Clean up
+
+```bash
+docker stop surfsense-pg14-temp && docker rm surfsense-pg14-temp
+docker volume rm surfsense-data # only after verifying migration succeeded
+```
+
+---
+
+## Troubleshooting
+
+### `install.sh` runs normally with a blank database (no migration happened)
+
+The legacy volume was not detected. Confirm it exists:
+
+```bash
+docker volume ls | grep surfsense-data
+```
+
+If it doesn't appear, the old container may have used a different volume name. Check with:
+
+```bash
+docker volume ls | grep -i surfsense
+```
+
+### Extraction fails with permission errors
+
+The script detects the UID of the data files and runs the temporary PG14 container as that user. If you see permission errors in `./surfsense-migration.log`, run `migrate-database.sh` manually and check the log for details.
+
+### Cannot find `/data/.secret_key`
+
+The all-in-one entrypoint always writes the key to `/data/.secret_key` unless you explicitly set `SECRET_KEY=` as an environment variable. If the key is missing, the migration script auto-generates a new one (with a warning). You can update it manually in `./surfsense/.env` afterwards. Note that a new key invalidates all existing browser sessions — users will need to log in again.
+
+### Restore errors after re-running `install.sh`
+
+If `surfsense-postgres` volume already exists from a previous partial run, remove it before retrying:
+
+```bash
+docker volume rm surfsense-postgres
+```
diff --git a/surfsense_web/docker-entrypoint.js b/surfsense_web/docker-entrypoint.js
new file mode 100644
index 000000000..7fe697e46
--- /dev/null
+++ b/surfsense_web/docker-entrypoint.js
@@ -0,0 +1,100 @@
+/**
+ * Runtime environment variable substitution for Next.js Docker images.
+ *
+ * Next.js inlines NEXT_PUBLIC_* values at build time. The Docker image is built
+ * with unique placeholder strings (e.g. __NEXT_PUBLIC_FASTAPI_BACKEND_URL__).
+ * This script replaces those placeholders with real values from the container's
+ * environment variables before the server starts.
+ *
+ * Runs once at container startup via docker-entrypoint.sh.
+ */
+
+const fs = require("fs");
+const path = require("path");
+
+const replacements = [
+ [
+ "__NEXT_PUBLIC_FASTAPI_BACKEND_URL__",
+ process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000",
+ ],
+ [
+ "__NEXT_PUBLIC_ELECTRIC_URL__",
+ process.env.NEXT_PUBLIC_ELECTRIC_URL || "http://localhost:5133",
+ ],
+ [
+ "__NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE__",
+ process.env.NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE || "LOCAL",
+ ],
+ [
+ "__NEXT_PUBLIC_ETL_SERVICE__",
+ process.env.NEXT_PUBLIC_ETL_SERVICE || "DOCLING",
+ ],
+ [
+ "__NEXT_PUBLIC_DEPLOYMENT_MODE__",
+ process.env.NEXT_PUBLIC_DEPLOYMENT_MODE || "self-hosted",
+ ],
+ [
+ "__NEXT_PUBLIC_ELECTRIC_AUTH_MODE__",
+ process.env.NEXT_PUBLIC_ELECTRIC_AUTH_MODE || "insecure",
+ ],
+];
+
+let filesProcessed = 0;
+let filesModified = 0;
+
+function walk(dir) {
+ let entries;
+ try {
+ entries = fs.readdirSync(dir, { withFileTypes: true });
+ } catch {
+ return;
+ }
+ for (const entry of entries) {
+ const full = path.join(dir, entry.name);
+ if (entry.isDirectory()) {
+ walk(full);
+ } else if (entry.name.endsWith(".js")) {
+ filesProcessed++;
+ let content = fs.readFileSync(full, "utf8");
+ let changed = false;
+ for (const [placeholder, value] of replacements) {
+ if (content.includes(placeholder)) {
+ content = content.replaceAll(placeholder, value);
+ changed = true;
+ }
+ }
+ if (changed) {
+ fs.writeFileSync(full, content);
+ filesModified++;
+ }
+ }
+ }
+}
+
+console.log("[entrypoint] Replacing environment variable placeholders...");
+for (const [placeholder, value] of replacements) {
+ console.log(` ${placeholder} -> ${value}`);
+}
+
+walk(path.join(__dirname, ".next"));
+
+const serverJs = path.join(__dirname, "server.js");
+if (fs.existsSync(serverJs)) {
+ let content = fs.readFileSync(serverJs, "utf8");
+ let changed = false;
+ filesProcessed++;
+ for (const [placeholder, value] of replacements) {
+ if (content.includes(placeholder)) {
+ content = content.replaceAll(placeholder, value);
+ changed = true;
+ }
+ }
+ if (changed) {
+ fs.writeFileSync(serverJs, content);
+ filesModified++;
+ }
+}
+
+console.log(
+ `[entrypoint] Done. Scanned ${filesProcessed} files, modified ${filesModified}.`
+);
diff --git a/surfsense_web/docker-entrypoint.sh b/surfsense_web/docker-entrypoint.sh
new file mode 100644
index 000000000..7f4dfbf25
--- /dev/null
+++ b/surfsense_web/docker-entrypoint.sh
@@ -0,0 +1,6 @@
+#!/bin/sh
+set -e
+
+node /app/docker-entrypoint.js
+
+exec node server.js