SurfSense Docs
How to

Migrate from the All-in-One Container

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.


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:

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 and confirm your data is intact.
  2. Once satisfied, remove the old volume (irreversible):
    docker volume rm surfsense-data
  3. Delete the dump file once you no longer need it as a backup:
    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:

# 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:

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

FlagDescriptionDefault
--db-user USEROld PostgreSQL usernamesurfsense
--db-password PASSOld PostgreSQL passwordsurfsense
--db-name NAMEOld PostgreSQL databasesurfsense
--yes / -ySkip 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:

docker stop surfsense 2>/dev/null || true

Step 2 — Start a temporary PostgreSQL 14 container

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:

docker exec surfsense-pg14-temp pg_isready -U surfsense

Step 3 — Dump the database

docker exec -e PGPASSWORD=surfsense surfsense-pg14-temp \
  pg_dump -U surfsense surfsense > surfsense_backup.sql

Step 4 — Recover your SECRET_KEY

docker run --rm -v surfsense-data:/data alpine cat /data/.secret_key

Step 5 — Set up the new stack

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

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

docker compose up -d

Step 8 — Clean up

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:

docker volume ls | grep surfsense-data

If it doesn't appear, the old container may have used a different volume name. Check with:

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:

docker volume rm surfsense-postgres

On this page