Migration guide
Migrate a self-hosted NocoDB instance from a non-Postgres database to PostgreSQL so you can activate a paid license without losing data.
Why this guide?
We recommend PostgreSQL as the database behind the NC_DB environment variable for every NocoDB deployment. In practice, though, many instances start out on a different database: sometimes for legacy reasons, sometimes because of the resources available to the user, and often simply for convenience, since SQLite is the zero-config default and needs nothing extra to get running.
This becomes a blocker the moment you move from the Community (free) edition to a paid edition. License activation requires PostgreSQL as the underlying database; SQLite and MySQL cannot activate a license. This guide walks you through migrating an existing SQLite-backed instance to PostgreSQL so you can upgrade cleanly and keep all of your data.
SQLite to PostgreSQL migration
Prerequisites
- Docker and Docker Compose
- A running NocoDB instance using the default SQLite backend
- A NocoDB Business license (required for the built-in migration tool)
Step 1: Back up your SQLite data
Before touching anything, create multiple independent backups:
# Stop NocoDB so the SQLite file is in a consistent state
docker stop nocodb
# Copy the entire volume to a timestamped backup location
docker run --rm -v nocodb-data:/data -v "$(pwd)":/backup \
alpine sh -c "cp -a /data /backup/nocodb-backup-$(date +%Y%m%d)"
# Copy the SQLite database file out of the container
docker cp nocodb:/usr/app/data/noco.db ./noco-snapshot-$(date +%Y%m%d).db
# Create a SQL dump as a secondary backup
sqlite3 noco-snapshot-$(date +%Y%m%d).db ".dump" > nocodb-dump-$(date +%Y%m%d).sql
# Restart NocoDB
docker start nocodbStep 2: Set up Postgres, Redis, and a worker
Update your docker-compose.yml to add Postgres, Redis, and a worker container alongside NocoDB. Follow the dedicated Quickstart guide for this.
Remove the mapping to the SQLite container, but do not remove its volume.
Step 3: Start only Postgres, then migrate the data
The recommended approach: NocoDB's built-in migration tool
Do not use sqlite3 .dump piped into psql, or pgloader, for this migration. While these tools can transfer raw data, SQLite and PostgreSQL have fundamentally incompatible schemas. You will run into problems with:
- Boolean columns (SQLite stores 0/1, PostgreSQL expects true/false)
- Type mismatches (SQLite
datetimevs PostgreSQLtimestamp,integervsbigint) - Identifier case sensitivity (SQLite is case-insensitive, PostgreSQL folds unquoted identifiers to lowercase)
- Junction table integrity (SQLite allows NULL in NOT NULL columns, PostgreSQL does not)
- Migration tracking tables (
xc_knex_migrationsv0) that NocoDB needs in order to recognize the database as initialized
These can in theory be resolved with sed transformations and post-import ALTER TABLE scripts, but it is tedious and error prone.
Instead, use NocoDB's built-in migration feature:
-
Start a temporary NocoDB container with your old SQLite data. Here you map the previous volume that was disconnected from your previous container.
docker run -d --name nocodb-old \ -v nocodb-data:/usr/app/data \ -p 8082:8080 \ nocodb/nocodb:latest -
Start your new Postgres-backed NocoDB (fresh, empty database).
# Make sure NC_MIGRATIONS_DISABLED is NOT set # Let NocoDB initialize a clean Postgres schema docker compose up -d nocodb -
Activate your license on the new Postgres instance, then use the migration tool:
- On the new instance: Base homepage, then Import Data, then NocoDB, then Generate and Copy URL.
- On the old instance: Open the base context menu, then Settings, then Migrate, paste the URL, and click Migrate.
-
Verify the migration completed. All tables should appear with correct row counts and no errors.
Step 4: Fix auto-increment sequences
After migration, PostgreSQL's auto-increment sequences may be out of sync with the imported data (new rows get ID 1, which already exists). Reset all sequences:
docker exec nocodb-postgres psql -U nocodb -d nocodb -c "
DO \$\$
DECLARE
r RECORD;
max_id bigint;
BEGIN
FOR r IN
SELECT
n.nspname AS schemaname,
c.relname AS tablename,
a.attname AS columnname,
pg_get_serial_sequence(format('%I.%I', n.nspname, c.relname), a.attname) AS sequencename
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
JOIN pg_attribute a ON a.attrelid = c.oid
WHERE c.relkind = 'r'
AND n.nspname NOT IN ('pg_catalog','information_schema')
AND a.attnum > 0
AND NOT a.attisdropped
AND (
a.attidentity <> ''
OR pg_get_serial_sequence(format('%I.%I', n.nspname, c.relname), a.attname) IS NOT NULL
)
LOOP
EXECUTE format('SELECT MAX(%I) FROM %I.%I', r.columnname, r.schemaname, r.tablename)
INTO max_id;
IF max_id IS NULL THEN
EXECUTE format('SELECT setval(%L, 1, false)', r.sequencename);
ELSE
EXECUTE format('SELECT setval(%L, %s, true)', r.sequencename, max_id);
END IF;
END LOOP;
END \$\$;
"Step 5: Post-migration cleanup
-
Recreate broken views. Some filters may behave differently on the new backend (especially datetime filters), so check your views for any changes.
-
Remove old containers and volumes (only after verifying everything for a few days):
docker rm -f nocodb-old docker volume rm nocodb-data # Only after you are confident everything works