Migration Guide
Why this guide?
We recommend PostgreSQL as the database for the NC_DB environment variable in all NocoDB deployments. But many instances start on a different database. The reasons can be legacy setups, the resources of the user, or convenience. SQLite is the zero-config default and needs nothing more to run.
This becomes a problem when you move from the Community (free) edition to a paid edition. License activation needs PostgreSQL as the database. SQLite and MySQL cannot activate a license. This guide shows how to migrate an existing SQLite instance to PostgreSQL. You can then 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 you change anything, make several 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 next to NocoDB. For this, follow the Quickstart guide.
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. These tools can transfer raw data, but SQLite and PostgreSQL have fundamentally incompatible schemas. You will get 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 to recognize the database as initialized
In theory, you can fix these problems with sed transformations and post-import ALTER TABLE scripts. But this work is slow, and errors are likely.
Instead, use the built-in migration feature of NocoDB:
-
Start a temporary NocoDB container with your old SQLite data. Map the old volume that you disconnected from your old 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: go to 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 must show with correct row counts and no errors.
Step 4: Fix auto-increment sequences
After the migration, the PostgreSQL auto-increment sequences can be out of sync with the imported data. New rows then 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 can work differently on the new backend, especially datetime filters. Check your views for changes.
-
Remove old containers and volumes. Do this only after you check everything for a few days:
docker rm -f nocodb-old docker volume rm nocodb-data # Only after you are confident everything works
Last updated on