Migrate a Plone site from FileStorage#
What we will build#
In this tutorial we will migrate an existing Plone site from FileStorage (the default Data.fs + blob directory) to zodb-pgjsonb, storing all object state as queryable JSONB in PostgreSQL.
By the end we will have a running Plone site backed entirely by PostgreSQL, with the original FileStorage preserved as a backup.
Prerequisites#
A working Plone 6 site using FileStorage (the default storage)
PostgreSQL 15+ running and accessible
Python 3.12+ with the same virtual environment as the Plone site
uv (recommended) or pip
Step 1: install zodb-pgjsonb#
Activate the virtual environment that our Plone site uses and install zodb-pgjsonb:
source .venv/bin/activate
uv pip install zodb-pgjsonb
This also installs zodbconvert, which is part of the ZODB package.
Step 2: prepare the PostgreSQL database#
Create a dedicated database for the ZODB data. Connect to PostgreSQL as a superuser and run:
psql -h localhost -p 5432 -U postgres
CREATE USER zodb WITH PASSWORD 'zodb';
CREATE DATABASE zodb OWNER zodb;
\q
Verify the connection:
psql -h localhost -p 5432 -U zodb -d zodb -c "SELECT version();"
We should see the PostgreSQL version string.
zodb-pgjsonb creates all required tables (object_state, transaction_log, blob_state, etc.) automatically on first connection.
Step 3: stop the Plone site#
We must stop Plone before migrating to avoid writes to the source FileStorage during the copy.
Use Ctrl+C in the terminal where Zope is running, or stop the process through your process manager (supervisord, systemd, etc.).
Step 4: write the zodbconvert configuration#
Create a file called migrate.cfg in the project directory:
<source>
<filestorage>
path /path/to/var/filestorage/Data.fs
blob-dir /path/to/var/blobstorage
</filestorage>
</source>
<destination>
%import zodb_pgjsonb
<pgjsonb>
dsn dbname=zodb user=zodb password=zodb host=localhost port=5432
</pgjsonb>
</destination>
Replace /path/to/var/filestorage/Data.fs and /path/to/var/blobstorage with the actual paths from the existing Zope configuration.
Tip
Look in the current zope.conf for the <filestorage> section to find the exact paths.
A typical Plone installation uses instance/var/filestorage/Data.fs and instance/var/blobstorage.
Step 5: run the migration#
Run zodbconvert to copy all transactions from FileStorage to PostgreSQL:
.venv/bin/zodbconvert --clear migrate.cfg
The --clear flag ensures the destination database starts empty.
We should see progress output as transactions are copied:
Copying transactions from FileStorage to PGJsonbStorage
[00:15] 8,200 / 28,394 records (28.9%) | 547 rec/s | 12.3 MB/s
[00:30] 17,500 / 28,394 records (61.6%) | 620 rec/s | 11.8 MB/s
Copied 1,542 transactions with 28,394 records in 45.2s (628 rec/s, 12.3 MB/s)
Note
The migration speed depends on database size, network latency, and disk I/O. A typical Plone site with 50,000 objects migrates in under 5 minutes.
Parallel migration for large databases#
For large databases, zodb-pgjsonb supports parallel migration using multiple PostgreSQL connections.
The zodbconvert CLI does not expose a --workers flag, so we use a short Python script instead:
Create a file called migrate_parallel.py:
from ZODB.FileStorage import FileStorage
from ZODB.blob import BlobStorage
from zodb_pgjsonb import PGJsonbStorage
# Open the source FileStorage (read-only)
fs = FileStorage("/path/to/var/filestorage/Data.fs", read_only=True)
source = BlobStorage("/path/to/var/blobstorage", fs)
# Open the destination with a larger connection pool
dest = PGJsonbStorage(
dsn="dbname=zodb user=zodb password=zodb host=localhost port=5432",
pool_max_size=8,
)
# Copy with 4 parallel workers
dest.copyTransactionsFrom(source, workers=4)
source.close()
dest.close()
Run the script:
python migrate_parallel.py
The main thread reads from FileStorage and decodes pickles. Worker threads write to PostgreSQL concurrently. OID ordering is preserved automatically.
We should see progress output with throughput and ETA:
Copying transactions with 4 parallel workers (total OIDs: 128,503)
[00:12] 15,200 / 128,503 OIDs (11.8%) | 1,267 obj/s | ETA 01:30
[00:24] 31,400 / 128,503 OIDs (24.4%) | 1,350 obj/s | ETA 01:12
[00:48] 72,100 / 128,503 OIDs (56.1%) | 1,502 obj/s | ETA 00:38
[01:38] All 8,421 transactions copied with 4 workers.
Tip
Set pool_max_size to at least the number of workers.
Using 4 to 6 workers provides the best throughput for most deployments.
Step 6: update zope.conf#
Replace the <filestorage> section in zope.conf with a <pgjsonb> section:
%import zodb_pgjsonb
<zodb_db main>
mount-point /
cache-size 30000
<pgjsonb>
dsn dbname=zodb user=zodb password=zodb host=localhost port=5432
blob-temp-dir ./instance/var/blobtemp
</pgjsonb>
</zodb_db>
Create the blob temp directory:
mkdir -p instance/var/blobtemp
Note
Remove or comment out the old <filestorage> and <blobstorage> sections.
Do not delete the original Data.fs and blob directory yet – keep them as a backup until we verify the migration.
Step 7: start and verify#
Start the Plone site:
.venv/bin/runwsgi instance/etc/zope.ini
We should see log output ending with:
INFO [waitress:486] Serving on http://0.0.0.0:8081
Open the site in a browser and verify:
Log in and browse existing content.
Check that images and file downloads work (blob data).
Create a new page to confirm writes work.
Verify with SQL#
Open a second terminal and connect to PostgreSQL:
psql -h localhost -p 5432 -U zodb -d zodb
Count the objects:
SELECT count(*) FROM object_state;
List the content types:
SELECT class_mod || '.' || class_name AS class,
count(*) AS count
FROM object_state
GROUP BY 1
ORDER BY 2 DESC
LIMIT 10;
We should see the same objects that existed in the original FileStorage.
What we learned#
zodb-pgjsonb implements
IStorageRestoreable, making it compatible withzodbconvertThe standard
zodbconverttool copies all transactions, including blobsFor large databases, the Python API
copyTransactionsFrom(source, workers=N)uses parallel PostgreSQL connections for higher throughputAfter migration, we swap the
<filestorage>section for<pgjsonb>inzope.confAll ZODB data is now queryable JSON in PostgreSQL
Next steps#
Quickstart with Docker to explore SQL queryability and S3 blob tiering
Configuration options for the complete list of configuration options