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