feat: add Windows installation script and update README for Linux/MacOS users

This commit is contained in:
Anish Sarkar 2026-03-03 19:13:59 +05:30
parent d9ebc2c599
commit e7d6e5f5bd
3 changed files with 675 additions and 0 deletions

View file

@ -86,10 +86,18 @@ Run SurfSense on your own infrastructure for full data control and privacy.
> [!NOTE] > [!NOTE]
> Windows users: install [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) first and run the command below in the Ubuntu terminal. > Windows users: install [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) first and run the command below in the Ubuntu terminal.
#### For Linux/MacOS users:
```bash ```bash
curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.sh | bash curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.sh | bash
``` ```
#### For Windows users:
```bash
irm https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.ps1 | iex
```
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. 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.
For Docker Compose, manual installation, and other deployment options, see the [docs](https://www.surfsense.com/docs/). For Docker Compose, manual installation, and other deployment options, see the [docs](https://www.surfsense.com/docs/).

335
docker/scripts/install.ps1 Normal file
View file

@ -0,0 +1,335 @@
# =============================================================================
# SurfSense — One-line Install Script (Windows / PowerShell)
#
#
# Usage: irm https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.ps1 | iex
#
# To pass flags, save and run locally:
# .\install.ps1 -NoWatchtower
# .\install.ps1 -WatchtowerInterval 3600
#
# 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.
# =============================================================================
param(
[switch]$NoWatchtower,
[int]$WatchtowerInterval = 86400
)
$ErrorActionPreference = 'Stop'
# ── Configuration ───────────────────────────────────────────────────────────
$RepoRaw = "https://raw.githubusercontent.com/MODSetter/SurfSense/dev"
$InstallDir = ".\surfsense"
$OldVolume = "surfsense-data"
$DumpFile = ".\surfsense_migration_backup.sql"
$KeyFile = ".\surfsense_migration_secret.key"
$MigrationDoneFile = "$InstallDir\.migration_done"
$MigrationMode = $false
$SetupWatchtower = -not $NoWatchtower
$WatchtowerContainer = "watchtower"
# ── Output helpers ──────────────────────────────────────────────────────────
function Write-Info { param([string]$Msg) Write-Host "[SurfSense] " -ForegroundColor Cyan -NoNewline; Write-Host $Msg }
function Write-Ok { param([string]$Msg) Write-Host "[SurfSense] " -ForegroundColor Green -NoNewline; Write-Host $Msg }
function Write-Warn { param([string]$Msg) Write-Host "[SurfSense] " -ForegroundColor Yellow -NoNewline; Write-Host $Msg }
function Write-Step { param([string]$Msg) Write-Host "`n-- $Msg" -ForegroundColor Cyan }
function Write-Err { param([string]$Msg) Write-Host "[SurfSense] ERROR: $Msg" -ForegroundColor Red; exit 1 }
# ── Pre-flight checks ──────────────────────────────────────────────────────
Write-Step "Checking prerequisites"
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
Write-Err "Docker is not installed. Install Docker Desktop: https://docs.docker.com/desktop/install/windows-install/"
}
Write-Ok "Docker found."
docker info *>$null
if ($LASTEXITCODE -ne 0) {
Write-Err "Docker daemon is not running. Please start Docker Desktop and try again."
}
Write-Ok "Docker daemon is running."
docker compose version *>$null
if ($LASTEXITCODE -ne 0) {
Write-Err "Docker Compose is not available. It should be bundled with Docker Desktop."
}
Write-Ok "Docker Compose found."
# ── Wait-for-postgres helper ────────────────────────────────────────────────
function Wait-ForPostgres {
param([string]$DbUser)
$maxAttempts = 45
$attempt = 0
Write-Info "Waiting for PostgreSQL to accept connections..."
do {
$attempt++
if ($attempt -ge $maxAttempts) {
Write-Err "PostgreSQL did not become ready after $($maxAttempts * 2) seconds.`nCheck logs: cd $InstallDir; docker compose logs db"
}
Start-Sleep -Seconds 2
Push-Location $InstallDir
docker compose exec -T db pg_isready -U $DbUser -q 2>$null
$ready = $LASTEXITCODE -eq 0
Pop-Location
} while (-not $ready)
Write-Ok "PostgreSQL is ready."
}
# ── Download files ──────────────────────────────────────────────────────────
Write-Step "Downloading SurfSense files"
Write-Info "Installation directory: $InstallDir"
New-Item -ItemType Directory -Path "$InstallDir\scripts" -Force | Out-Null
$Files = @(
@{ Src = "docker/docker-compose.yml"; Dest = "docker-compose.yml" }
@{ Src = "docker/.env.example"; Dest = ".env.example" }
@{ Src = "docker/postgresql.conf"; Dest = "postgresql.conf" }
@{ Src = "docker/scripts/init-electric-user.sh"; Dest = "scripts/init-electric-user.sh" }
@{ Src = "docker/scripts/migrate-database.ps1"; Dest = "scripts/migrate-database.ps1" }
)
foreach ($f in $Files) {
$destPath = Join-Path $InstallDir $f.Dest
Write-Info "Downloading $($f.Dest)..."
try {
Invoke-WebRequest -Uri "$RepoRaw/$($f.Src)" -OutFile $destPath -UseBasicParsing
} catch {
Write-Err "Failed to download $($f.Dest). Check your internet connection and try again."
}
}
Write-Ok "All files downloaded to $InstallDir/"
# ── Legacy all-in-one detection ─────────────────────────────────────────────
$volumeList = docker volume ls --format '{{.Name}}' 2>$null
if (($volumeList -split "`n") -contains $OldVolume -and -not (Test-Path $MigrationDoneFile)) {
$MigrationMode = $true
if (Test-Path $DumpFile) {
Write-Step "Migration mode - using existing dump (skipping extraction)"
Write-Info "Found existing dump: $DumpFile"
Write-Info "Skipping data extraction - proceeding directly to restore."
Write-Info "To force a fresh extraction, remove the dump first: Remove-Item $DumpFile"
} else {
Write-Step "Migration mode - legacy all-in-one container detected"
Write-Warn "Volume '$OldVolume' found. Your data will be migrated automatically."
Write-Warn "PostgreSQL is being upgraded from version 14 to 17."
Write-Warn "Your original data will NOT be deleted."
Write-Host ""
Write-Info "Running data extraction (migrate-database.ps1 -Yes)..."
Write-Info "Full extraction log: ./surfsense-migration.log"
Write-Host ""
$migrateScript = Join-Path $InstallDir "scripts/migrate-database.ps1"
& $migrateScript -Yes
if ($LASTEXITCODE -ne 0) {
Write-Err "Data extraction failed. See ./surfsense-migration.log for details.`nYou can also run migrate-database.ps1 manually with custom flags."
}
Write-Host ""
Write-Ok "Data extraction complete. Proceeding with installation and restore."
}
}
# ── Set up .env ─────────────────────────────────────────────────────────────
Write-Step "Configuring environment"
$envPath = Join-Path $InstallDir ".env"
$envExamplePath = Join-Path $InstallDir ".env.example"
if (-not (Test-Path $envPath)) {
Copy-Item $envExamplePath $envPath
if ($MigrationMode -and (Test-Path $KeyFile)) {
$SecretKey = (Get-Content $KeyFile -Raw).Trim()
Write-Ok "Using SECRET_KEY recovered from legacy container."
} else {
$bytes = New-Object byte[] 32
$rng = [System.Security.Cryptography.RNGCryptoServiceProvider]::new()
$rng.GetBytes($bytes)
$rng.Dispose()
$SecretKey = [Convert]::ToBase64String($bytes)
Write-Ok "Generated new random SECRET_KEY."
}
$content = Get-Content $envPath -Raw
$content = $content -replace 'SECRET_KEY=replace_me_with_a_random_string', "SECRET_KEY=$SecretKey"
Set-Content -Path $envPath -Value $content -NoNewline
Write-Info "Created $envPath"
} else {
Write-Warn ".env already exists - keeping your existing configuration."
}
# ── Start containers ────────────────────────────────────────────────────────
if ($MigrationMode) {
$envContent = Get-Content $envPath
$DbUser = ($envContent | Select-String '^DB_USER=' | ForEach-Object { ($_ -split '=',2)[1].Trim('"') }) | Select-Object -First 1
$DbPass = ($envContent | Select-String '^DB_PASSWORD=' | ForEach-Object { ($_ -split '=',2)[1].Trim('"') }) | Select-Object -First 1
$DbName = ($envContent | Select-String '^DB_NAME=' | ForEach-Object { ($_ -split '=',2)[1].Trim('"') }) | Select-Object -First 1
if (-not $DbUser) { $DbUser = "surfsense" }
if (-not $DbPass) { $DbPass = "surfsense" }
if (-not $DbName) { $DbName = "surfsense" }
Write-Step "Starting PostgreSQL 17"
Push-Location $InstallDir
docker compose up -d db
Pop-Location
Wait-ForPostgres -DbUser $DbUser
Write-Step "Restoring database"
if (-not (Test-Path $DumpFile)) {
Write-Err "Dump file '$DumpFile' not found. The migration script may have failed."
}
Write-Info "Restoring dump into PostgreSQL 17 - this may take a while for large databases..."
$restoreErrFile = Join-Path $env:TEMP "surfsense_restore_err.log"
Push-Location $InstallDir
Get-Content $DumpFile | docker compose exec -T -e "PGPASSWORD=$DbPass" db psql -U $DbUser -d $DbName 2>$restoreErrFile | Out-Null
Pop-Location
$fatalErrors = @()
if (Test-Path $restoreErrFile) {
$fatalErrors = Get-Content $restoreErrFile |
Where-Object { $_ -match '^ERROR:' } |
Where-Object { $_ -notmatch 'already exists' } |
Where-Object { $_ -notmatch 'multiple primary keys' }
}
if ($fatalErrors.Count -gt 0) {
Write-Warn "Restore completed with errors (may be harmless pg_dump header noise):"
$fatalErrors | ForEach-Object { Write-Host $_ }
Write-Warn "If SurfSense behaves incorrectly, inspect manually."
} else {
Write-Ok "Database restored with no fatal errors."
}
# Smoke test
Push-Location $InstallDir
$tableCount = (docker compose exec -T -e "PGPASSWORD=$DbPass" db psql -U $DbUser -d $DbName -t -c "SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public';" 2>$null).Trim()
Pop-Location
if (-not $tableCount -or $tableCount -eq "0") {
Write-Warn "Smoke test: no tables found after restore."
Write-Warn "The restore may have failed silently. Check: cd $InstallDir; docker compose logs db"
} else {
Write-Ok "Smoke test passed: $tableCount table(s) restored successfully."
New-Item -Path $MigrationDoneFile -ItemType File -Force | Out-Null
}
Write-Step "Starting all SurfSense services"
Push-Location $InstallDir
docker compose up -d
Pop-Location
Write-Ok "All services started."
Remove-Item $KeyFile -ErrorAction SilentlyContinue
} else {
Write-Step "Starting SurfSense"
Push-Location $InstallDir
docker compose up -d
Pop-Location
Write-Ok "All services started."
}
# ── Watchtower (auto-update) ────────────────────────────────────────────────
if ($SetupWatchtower) {
$wtHours = [math]::Floor($WatchtowerInterval / 3600)
Write-Step "Setting up Watchtower (auto-updates every ${wtHours}h)"
$wtState = docker inspect -f '{{.State.Running}}' $WatchtowerContainer 2>$null
if ($LASTEXITCODE -ne 0) { $wtState = "missing" }
if ($wtState -eq "true") {
Write-Ok "Watchtower is already running - skipping."
} else {
if ($wtState -ne "missing") {
Write-Info "Removing stopped Watchtower container..."
docker rm -f $WatchtowerContainer *>$null
}
docker run -d `
--name $WatchtowerContainer `
--restart unless-stopped `
-v /var/run/docker.sock:/var/run/docker.sock `
nickfedor/watchtower `
--label-enable `
--interval $WatchtowerInterval *>$null
if ($LASTEXITCODE -eq 0) {
Write-Ok "Watchtower started - labeled SurfSense containers will auto-update."
} else {
Write-Warn "Could not start Watchtower. You can set it up manually or use: docker compose pull; docker compose up -d"
}
}
} else {
Write-Info "Skipping Watchtower setup (-NoWatchtower flag)."
}
# ── Done ────────────────────────────────────────────────────────────────────
Write-Host ""
Write-Host @"
.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
"@ -ForegroundColor White
$versionDisplay = if ($env:SURFSENSE_VERSION -and $env:SURFSENSE_VERSION -ne "latest") { "v$env:SURFSENSE_VERSION" } else { "latest" }
Write-Host " Your personal AI-powered search engine [$versionDisplay]" -ForegroundColor Yellow
Write-Host ("=" * 62) -ForegroundColor Cyan
Write-Host ""
Write-Info " Frontend: http://localhost:3000"
Write-Info " Backend: http://localhost:8000"
Write-Info " API Docs: http://localhost:8000/docs"
Write-Info ""
Write-Info " Config: $InstallDir\.env"
Write-Info " Logs: cd $InstallDir; docker compose logs -f"
Write-Info " Stop: cd $InstallDir; docker compose down"
Write-Info " Update: cd $InstallDir; docker compose pull; docker compose up -d"
Write-Info ""
if ($SetupWatchtower) {
Write-Info " Watchtower: auto-updates every ${wtHours}h (stop: docker rm -f $WatchtowerContainer)"
} else {
Write-Warn " Watchtower skipped. For auto-updates, re-run without -NoWatchtower."
}
Write-Info ""
if ($MigrationMode) {
Write-Warn " Migration complete! Open frontend and verify your data."
Write-Warn " Once verified, clean up the legacy volume and migration files:"
Write-Warn " docker volume rm $OldVolume"
Write-Warn " Remove-Item $DumpFile"
Write-Warn " Remove-Item $MigrationDoneFile"
} else {
Write-Warn " First startup may take a few minutes while images are pulled."
Write-Warn " Edit $InstallDir\.env to configure API keys, OAuth, etc."
}

View file

@ -0,0 +1,332 @@
# =============================================================================
# SurfSense — Database Migration Script (Windows / PowerShell)
#
# 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.ps1 to restore.
#
# Usage:
# .\migrate-database.ps1 [options]
#
# Options:
# -DbUser USER Old PostgreSQL username (default: surfsense)
# -DbPassword PASS Old PostgreSQL password (default: surfsense)
# -DbName NAME Old PostgreSQL database (default: surfsense)
# -Yes Skip all confirmation prompts
#
# Prerequisites:
# - Docker Desktop 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.ps1
#
# What this script does NOT do:
# - Delete the original surfsense-data volume (do this manually after verifying)
# - Install the new SurfSense stack (install.ps1 handles that automatically)
#
# Note:
# install.ps1 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 (-DbUser / -DbPassword / -DbName)
# or if the automatic migration inside install.ps1 fails at the extraction step.
# =============================================================================
param(
[string]$DbUser = "surfsense",
[string]$DbPassword = "surfsense",
[string]$DbName = "surfsense",
[switch]$Yes
)
$ErrorActionPreference = 'Stop'
# ── Constants ────────────────────────────────────────────────────────────────
$OldVolume = "surfsense-data"
$TempContainer = "surfsense-pg14-migration"
$DumpFile = ".\surfsense_migration_backup.sql"
$KeyFile = ".\surfsense_migration_secret.key"
$PG14Image = "pgvector/pgvector:pg14"
$LogFile = ".\surfsense-migration.log"
# ── Output helpers ───────────────────────────────────────────────────────────
function Write-Info { param([string]$Msg) Write-Host "[SurfSense] " -ForegroundColor Cyan -NoNewline; Write-Host $Msg }
function Write-Ok { param([string]$Msg) Write-Host "[SurfSense] " -ForegroundColor Green -NoNewline; Write-Host $Msg }
function Write-Warn { param([string]$Msg) Write-Host "[SurfSense] " -ForegroundColor Yellow -NoNewline; Write-Host $Msg }
function Write-Step { param([string]$Step, [string]$Msg) Write-Host "`n-- Step ${Step}: $Msg" -ForegroundColor Cyan }
function Write-Err { param([string]$Msg) Write-Host "[SurfSense] ERROR: $Msg" -ForegroundColor Red; exit 1 }
function Log { param([string]$Msg) Add-Content -Path $LogFile -Value $Msg }
function Confirm-Action {
param([string]$Prompt)
if ($Yes) { return }
$reply = Read-Host "[SurfSense] $Prompt [y/N]"
if ($reply -notmatch '^[Yy]$') {
Write-Warn "Aborted."
exit 0
}
}
# ── Cleanup helper ───────────────────────────────────────────────────────────
function Remove-TempContainer {
$containers = docker ps -a --format '{{.Names}}' 2>$null
if ($containers -and ($containers -split "`n") -contains $TempContainer) {
Write-Info "Cleaning up temporary container '$TempContainer'..."
docker stop $TempContainer *>$null
docker rm $TempContainer *>$null
}
}
# Register cleanup on script exit
Register-EngineEvent PowerShell.Exiting -Action {
$containers = docker ps -a --format '{{.Names}}' 2>$null
if ($containers -and ($containers -split "`n") -contains "surfsense-pg14-migration") {
docker stop "surfsense-pg14-migration" *>$null
docker rm "surfsense-pg14-migration" *>$null
}
} | Out-Null
# ── Wait-for-postgres helper ────────────────────────────────────────────────
function Wait-ForPostgres {
param(
[string]$Container,
[string]$User,
[string]$Label = "PostgreSQL"
)
$maxAttempts = 45
$attempt = 0
Write-Info "Waiting for $Label to accept connections..."
do {
$attempt++
if ($attempt -ge $maxAttempts) {
Write-Err "$Label did not become ready after $($maxAttempts * 2) seconds. Check: docker logs $Container"
}
Start-Sleep -Seconds 2
docker exec $Container pg_isready -U $User -q 2>$null
} while ($LASTEXITCODE -ne 0)
Write-Ok "$Label is ready."
}
Write-Info "Migrating data from legacy database (PostgreSQL 14 -> 17)"
"Migration started at $(Get-Date)" | Out-File $LogFile
# ── Step 0: Pre-flight checks ───────────────────────────────────────────────
Write-Step "0" "Pre-flight checks"
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
Write-Err "Docker is not installed. Install Docker Desktop: https://docs.docker.com/desktop/install/windows-install/"
}
docker info *>$null
if ($LASTEXITCODE -ne 0) {
Write-Err "Docker daemon is not running. Please start Docker Desktop and try again."
}
$volumeList = docker volume ls --format '{{.Name}}' 2>$null
if (-not (($volumeList -split "`n") -contains $OldVolume)) {
Write-Err "Legacy volume '$OldVolume' not found. Are you sure you ran the old all-in-one SurfSense container?"
}
Write-Ok "Found legacy volume: $OldVolume"
$oldContainer = (docker ps --filter "volume=$OldVolume" --format '{{.Names}}' 2>$null | Select-Object -First 1)
if ($oldContainer) {
Write-Warn "Container '$oldContainer' is running and using the '$OldVolume' volume."
Write-Warn "It must be stopped before migration to prevent data file corruption."
Confirm-Action "Stop '$oldContainer' now and proceed with data extraction?"
docker stop $oldContainer *>$null
if ($LASTEXITCODE -ne 0) {
Write-Err "Failed to stop '$oldContainer'. Try: docker stop $oldContainer"
}
Write-Ok "Container '$oldContainer' stopped."
}
if (Test-Path $DumpFile) {
Write-Warn "Dump file '$DumpFile' already exists."
Write-Warn "If a previous extraction succeeded, just run install.ps1 now."
Write-Warn "To re-extract, remove the file first: Remove-Item $DumpFile"
Write-Err "Aborting to avoid overwriting an existing dump."
}
$staleContainers = docker ps -a --format '{{.Names}}' 2>$null
if ($staleContainers -and ($staleContainers -split "`n") -contains $TempContainer) {
Write-Warn "Stale migration container '$TempContainer' found - removing it."
docker stop $TempContainer *>$null
docker rm $TempContainer *>$null
}
$drive = (Get-Item .).PSDrive
$freeMB = [math]::Floor($drive.Free / 1MB)
if ($freeMB -lt 500) {
Write-Warn "Low disk space: $freeMB MB free. At least 500 MB recommended for the dump."
Confirm-Action "Continue anyway?"
} else {
Write-Ok "Disk space: $freeMB MB free."
}
Write-Ok "All pre-flight checks passed."
# ── Confirmation prompt ──────────────────────────────────────────────────────
Write-Host ""
Write-Host "Extraction plan:" -ForegroundColor White
Write-Host " Source volume : " -NoNewline; Write-Host "$OldVolume" -ForegroundColor Yellow -NoNewline; Write-Host " (PG14 data at /data/postgres)"
Write-Host " Old credentials : user=" -NoNewline; Write-Host "$DbUser" -ForegroundColor Yellow -NoNewline; Write-Host " db=" -NoNewline; Write-Host "$DbName" -ForegroundColor Yellow
Write-Host " Dump saved to : " -NoNewline; Write-Host "$DumpFile" -ForegroundColor Yellow
Write-Host " SECRET_KEY to : " -NoNewline; Write-Host "$KeyFile" -ForegroundColor Yellow
Write-Host " Log file : " -NoNewline; Write-Host "$LogFile" -ForegroundColor Yellow
Write-Host ""
Confirm-Action "Start data extraction? (Your original data will not be deleted or modified.)"
# ── Step 1: Start temporary PostgreSQL 14 container ──────────────────────────
Write-Step "1" "Starting temporary PostgreSQL 14 container"
Write-Info "Pulling $PG14Image..."
docker pull $PG14Image *>$null
if ($LASTEXITCODE -ne 0) {
Write-Warn "Could not pull $PG14Image - using cached image if available."
}
$dataUid = docker run --rm -v "${OldVolume}:/data" alpine stat -c '%u' /data/postgres 2>$null
if (-not $dataUid -or $dataUid -eq "0") {
Write-Warn "Could not detect data directory UID - falling back to default (may chown files)."
$userFlag = @()
} else {
Write-Info "Data directory owned by UID $dataUid - starting temp container as that user."
$userFlag = @("--user", $dataUid)
}
$dockerRunArgs = @(
"run", "-d",
"--name", $TempContainer,
"-v", "${OldVolume}:/data",
"-e", "PGDATA=/data/postgres",
"-e", "POSTGRES_USER=$DbUser",
"-e", "POSTGRES_PASSWORD=$DbPassword",
"-e", "POSTGRES_DB=$DbName"
) + $userFlag + @($PG14Image)
docker @dockerRunArgs *>$null
if ($LASTEXITCODE -ne 0) {
Write-Err "Failed to start temporary PostgreSQL 14 container."
}
Write-Ok "Temporary container '$TempContainer' started."
Wait-ForPostgres -Container $TempContainer -User $DbUser -Label "PostgreSQL 14"
# ── Step 2: Dump the database ────────────────────────────────────────────────
Write-Step "2" "Dumping PostgreSQL 14 database"
Write-Info "Running pg_dump - this may take a while for large databases..."
$pgDumpErrFile = Join-Path $env:TEMP "surfsense_pgdump_err.log"
docker exec -e "PGPASSWORD=$DbPassword" $TempContainer pg_dump -U $DbUser --no-password $DbName > $DumpFile 2>$pgDumpErrFile
if ($LASTEXITCODE -ne 0) {
if (Test-Path $pgDumpErrFile) { Get-Content $pgDumpErrFile | Write-Host -ForegroundColor Red }
Remove-TempContainer
Write-Err "pg_dump failed. See above for details."
}
if (-not (Test-Path $DumpFile) -or (Get-Item $DumpFile).Length -eq 0) {
Remove-TempContainer
Write-Err "Dump file '$DumpFile' is empty. Something went wrong with pg_dump."
}
$dumpContent = Get-Content $DumpFile -TotalCount 5 -Raw
if ($dumpContent -notmatch "PostgreSQL database dump") {
Remove-TempContainer
Write-Err "Dump file does not contain a valid PostgreSQL dump header - the file may be corrupt."
}
$dumpLines = (Get-Content $DumpFile | Measure-Object -Line).Lines
if ($dumpLines -lt 10) {
Remove-TempContainer
Write-Err "Dump has only $dumpLines lines - suspiciously small. Aborting."
}
$dumpSize = "{0:N1} MB" -f ((Get-Item $DumpFile).Length / 1MB)
Write-Ok "Dump complete: $dumpSize ($dumpLines lines) -> $DumpFile"
Write-Info "Stopping temporary PostgreSQL 14 container..."
docker stop $TempContainer *>$null
docker rm $TempContainer *>$null
Write-Ok "Temporary container removed."
# ── Step 3: Recover SECRET_KEY ───────────────────────────────────────────────
Write-Step "3" "Recovering SECRET_KEY"
$recoveredKey = ""
$keyCheck = docker run --rm -v "${OldVolume}:/data" alpine sh -c 'test -f /data/.secret_key && cat /data/.secret_key' 2>$null
if ($LASTEXITCODE -eq 0 -and $keyCheck) {
$recoveredKey = $keyCheck.Trim()
Write-Ok "Recovered SECRET_KEY from '$OldVolume'."
} else {
Write-Warn "No SECRET_KEY file found at /data/.secret_key in '$OldVolume'."
Write-Warn "This means the all-in-one container was launched with SECRET_KEY set as an explicit env var."
if ($Yes) {
$bytes = New-Object byte[] 32
$rng = [System.Security.Cryptography.RNGCryptoServiceProvider]::new()
$rng.GetBytes($bytes)
$rng.Dispose()
$recoveredKey = [Convert]::ToBase64String($bytes)
Write-Warn "Non-interactive mode: generated a new SECRET_KEY automatically."
Write-Warn "All active browser sessions will be logged out after migration."
Write-Warn "To restore your original key, update SECRET_KEY in .\surfsense\.env afterwards."
} else {
Write-Warn "Enter the SECRET_KEY from your old container's environment"
$recoveredKey = Read-Host "[SurfSense] (press Enter to generate a new one - existing sessions will be invalidated)"
if (-not $recoveredKey) {
$bytes = New-Object byte[] 32
$rng = [System.Security.Cryptography.RNGCryptoServiceProvider]::new()
$rng.GetBytes($bytes)
$rng.Dispose()
$recoveredKey = [Convert]::ToBase64String($bytes)
Write-Warn "Generated a new SECRET_KEY. All active browser sessions will be logged out after migration."
}
}
}
Set-Content -Path $KeyFile -Value $recoveredKey -NoNewline
Write-Ok "SECRET_KEY saved to $KeyFile"
# ── Done ─────────────────────────────────────────────────────────────────────
Write-Host ""
Write-Host ("=" * 62) -ForegroundColor Green
Write-Host " Data extraction complete!" -ForegroundColor Green
Write-Host ("=" * 62) -ForegroundColor Green
Write-Host ""
Write-Ok "Dump file : $DumpFile ($dumpSize)"
Write-Ok "Secret key: $KeyFile"
Write-Host ""
Write-Info "Next step - run install.ps1 from this same directory:"
Write-Host ""
Write-Host " irm https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.ps1 | iex" -ForegroundColor Cyan
Write-Host ""
Write-Info "install.ps1 will detect the dump, restore your data into PostgreSQL 17,"
Write-Info "and start the full SurfSense stack automatically."
Write-Host ""
Write-Warn "Keep both files until you have verified the migration:"
Write-Warn " $DumpFile"
Write-Warn " $KeyFile"
Write-Warn "Full log saved to: $LogFile"
Write-Host ""
Log "Migration extraction completed successfully at $(Get-Date)"