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.

This guide was contributed by a member of the NocoDB community, based on a migration they performed themselves. Steps may need small adjustments for your specific setup, so always back up your data before you begin.

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 nocodb

Step 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

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 datetime vs PostgreSQL timestamp, integer vs bigint)
  • 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:

  1. 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
  2. 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
  3. 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.
  4. 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

  1. Recreate broken views. Some filters can work differently on the new backend, especially datetime filters. Check your views for changes.

  2. 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

Latest product updates?See Changelog
Stay in the loop? Follow us onLinkedInLinkedInYouTubeYouTubeXX