Olve Heitmann
11/19/2025, 8:12 PMMarvin
11/19/2025, 8:12 PMMarvin
11/19/2025, 8:18 PMPREFECT_SERVER_EVENTS_RETENTION_PERIOD=P2D
- Reduce the UI websocket backfill window and page size (speeds up UI load + lowers memory)
- PREFECT_SERVER_EVENTS_MAXIMUM_WEBSOCKET_BACKFILL=PT300S
- PREFECT_SERVER_EVENTS_WEBSOCKET_BACKFILL_PAGE_SIZE=100
- Optionally shorten the related resource cache TTL to keep memory in check
- PREFECT_SERVER_EVENTS_RELATED_RESOURCE_CACHE_TTL=PT300S
- Docs for these settings: Server Events settings
- SQLite itself
- SQLite is fine for local/dev but struggles under load (locking, big queries, no pooling). If you’re seeing 5GB+ and spikes, the single biggest improvement is switching to Postgres.
- Docs: Scale self-hosted Prefect and Server Database settings
- Services not running
- Make sure you are not starting the API with --no-services. The background services (including event cleanup) should be running.
- CLI help shows --no-services exists; avoid it unless you’re running services in a separate container.
- CLI: prefect server start --help confirms options.
Recommended next steps (fastest path)
1) Try these environment settings first (keep your current stack)
- Add these to your docker-compose env for the server, then restart:
PREFECT_SERVER_EVENTS_RETENTION_PERIOD=P2D
PREFECT_SERVER_EVENTS_MAXIMUM_WEBSOCKET_BACKFILL=PT300S
PREFECT_SERVER_EVENTS_WEBSOCKET_BACKFILL_PAGE_SIZE=100
PREFECT_SERVER_EVENTS_RELATED_RESOURCE_CACHE_TTL=PT300S
- Ensure you’re starting the server with services enabled (don’t use --no-services unless you run a separate “services” container).
2) Strongly consider moving from SQLite to Postgres
- Minimal docker-compose change:
- Add a postgres service
- Point Prefect at it via PREFECT_API_DATABASE_CONNECTION_URL=<postgresql+asyncpg://prefect:prefect@postgres:5432/prefect>
- Let the server migrate the DB on start with PREFECT_API_DATABASE_MIGRATE_ON_START=true
- Example snippet:
services:
postgres:
image: postgres:15-alpine
environment:
POSTGRES_USER: prefect
POSTGRES_PASSWORD: prefect
POSTGRES_DB: prefect
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U prefect"]
interval: 5s
timeout: 5s
retries: 5
ports:
- "5432:5432"
prefect-server:
image: prefecthq/prefect:3-latest
depends_on:
postgres:
condition: service_healthy
environment:
PREFECT_API_DATABASE_CONNECTION_URL: <postgresql+asyncpg://prefect:prefect@postgres:5432/prefect>
PREFECT_API_DATABASE_MIGRATE_ON_START: "true"
# keep the events tweaks from above here as well
PREFECT_SERVER_EVENTS_RETENTION_PERIOD: "P2D"
PREFECT_SERVER_EVENTS_MAXIMUM_WEBSOCKET_BACKFILL: "PT300S"
PREFECT_SERVER_EVENTS_WEBSOCKET_BACKFILL_PAGE_SIZE: "100"
PREFECT_SERVER_EVENTS_RELATED_RESOURCE_CACHE_TTL: "PT300S"
command: prefect server start --host 0.0.0.0
ports:
- "4200:4200"
volumes:
postgres_data:
- After switching to Postgres, you’ll gain proper connection pooling and more predictable performance under load.
- Server database settings: Settings reference — databaseMarvin
11/19/2025, 8:18 PMPREFECT_MESSAGING_BROKER=prefect.server.utilities.messaging.memory # keep memory for single container
PREFECT_MESSAGING_CACHE=prefect.server.utilities.messaging.memory # keep memory for single container
- Only switch to the Redis implementations in multi‑server setups.
Useful CLI while tuning
- Upgrade DB (if needed during version changes):
prefect server database upgrade -y
- Reset DB (destructive):
prefect server database reset -y
- CLI reference we verified:
- prefect server database upgrade --help
- prefect server database reset --help
- prefect server start --help
A couple of clarifying questions to tailor this further
- Which Prefect version are you on (e.g., 3.1.x)? If not current, upgrading can include performance fixes. Release notes: Changelog
- Roughly how many flow runs per day and how “chatty” are your logs?
- Can you share your current docker-compose for the Prefect services? I can mark up the exact env vars to add.
- Are you starting prefect server start with services enabled, or are you running services separately?
If you paste your compose file and the size of your events/log tables (or just total DB size), I’ll help you make the minimal changes to get memory usage back under control.Olve Heitmann
11/19/2025, 8:33 PMMarvin
11/19/2025, 8:33 PMMarvin
11/19/2025, 8:36 PMMarvin
11/19/2025, 8:41 PMPREFECT_API_DATABASE_CONNECTION_URL=<postgresql+asyncpg://prefect:prefect@postgres:5432/prefect>
- Healthcheck URL should be <http://127.0.0.1:4200/api/health> (no < >).
- Pin the Prefect image to match your server version during migration to avoid unplanned upgrades:
- image: prefecthq/prefect:3.4.17
Migration runbook (SQLite → Postgres with data preserved)
0) Quiesce and back up
- Stop writes:
- Temporarily scale workers to 0 or stop them so no new runs/logs are written during the move.
- Back up SQLite file from your volume:
docker run --rm -v brrr_prefect_data:/data -v $(pwd):/backup alpine sh -c "cp /data/prefect.db /backup/prefect.db.bak"
The SQLite file lives at /root/.prefect/prefect.db inside the container, which maps to your brrr_prefect_data volume.
1) Start Postgres and enable pg_trgm
- Bring up Postgres only:
docker compose up -d postgres
- Enable pg_trgm (recommended for search performance):
docker exec -it $(docker ps -qf "name=postgres") psql -U prefect -d prefect -c "CREATE EXTENSION IF NOT EXISTS pg_trgm;"
2) Migrate data with pgloader
- Use pgloader (best compatibility from SQLite → Postgres). This runs in the same Docker network and mounts your SQLite file:
docker run --rm --network qfin_default \
-v brrr_prefect_data:/root/.prefect \
dimitri/pgloader:latest \
pgloader sqlite:///root/.prefect/prefect.db <postgresql://prefect:prefect@postgres/prefect>
- Verify a few table counts (optional sanity checks):
docker exec -it $(docker ps -qf "name=postgres") psql -U prefect -d prefect -c "SELECT COUNT(*) FROM flow_run;"
docker exec -it $(docker ps -qf "name=postgres") psql -U prefect -d prefect -c "SELECT COUNT(*) FROM task_run;"
docker exec -it $(docker ps -qf "name=postgres") psql -U prefect -d prefect -c "SELECT COUNT(*) FROM log;"
docker exec -it $(docker ps -qf "name=postgres") psql -U prefect -d prefect -c "SELECT COUNT(*) FROM events;"
3) Point Prefect at Postgres and apply migrations
- Keep the events tuning you added:
PREFECT_API_DATABASE_CONNECTION_URL=<postgresql+asyncpg://prefect:prefect@postgres:5432/prefect>
PREFECT_API_DATABASE_MIGRATE_ON_START=true
PREFECT_SERVER_EVENTS_RETENTION_PERIOD=P2D
PREFECT_SERVER_EVENTS_MAXIMUM_WEBSOCKET_BACKFILL=PT300S
PREFECT_SERVER_EVENTS_WEBSOCKET_BACKFILL_PAGE_SIZE=100
PREFECT_SERVER_EVENTS_RELATED_RESOURCE_CACHE_TTL=PT300S
- Bring up the server:
docker compose up -d prefect-server
With PREFECT_API_DATABASE_MIGRATE_ON_START=true, Prefect will apply any Postgres‑specific schema migrations on startup. If you prefer to do it explicitly:
docker exec -it $(docker ps -qf "name=prefect-server") prefect server database upgrade -y
- Bring workers back up once the API is healthy.
4) Post‑migration checks
- UI loads quickly, lists of flow runs / deployments appear
- A few old flow runs show logs
- New runs write logs and events normally
Notes on performance with your workload
- You’ve already added the most impactful event settings:
- P2D retention, shorter websocket backfill and page size.
- Once on Postgres, connection pooling behaves much better. If you still see pressure, you can tune pool size/overflow:
PREFECT_SERVER_DATABASE_SQLALCHEMY_POOL_SIZE=10
PREFECT_SERVER_DATABASE_SQLALCHEMY_MAX_OVERFLOW=5
PREFECT_SERVER_DATABASE_SQLALCHEMY_POOL_TIMEOUT=30Marvin
11/19/2025, 8:41 PMdocker compose down && up -d? If you paste the omitted parts, I’ll annotate it so you can run it as-is.Olve Heitmann
11/19/2025, 10:59 PMMarvin
11/19/2025, 10:59 PMMarvin
11/19/2025, 11:00 PMstrftime(...) defaults that Postgres doesn’t understand, so we need to:
- create the Postgres schema with Prefect first, then
- run pgloader in data-only mode and exclude alembic_version.
Here’s the quickest path forward.
Why it failed
- pgloader attempted to create tables in Postgres from the SQLite schema, which contains SQLite-only default expressions like strftime(...). Postgres rejects those defaults, so the schema creation fails.
Correct approach
1) Stop Prefect (server and workers) so nothing writes during migration.
2) Create the Postgres schema with Prefect
- Run migrations once against Postgres to create the correct schema:
docker run --rm --network qfin_default \
-e PREFECT_API_DATABASE_CONNECTION_URL=<postgresql+asyncpg://prefect:prefect@postgres:5432/prefect> \
prefecthq/prefect:3.4.17 prefect server database upgrade -y
3) Use pgloader in data-only mode and exclude alembic_version
- Create a file called pgloader.load in your current directory with this content:
LOAD DATABASE
FROM sqlite:///root/.prefect/prefect.db
INTO <postgresql://prefect:prefect@postgres/prefect>
WITH data only,
batch rows = 5000,
prefetch rows = 5000,
on error stop
EXCLUDING TABLE NAMES LIKE 'alembic_version'
;
- Then run pgloader and mount both the SQLite volume and the load file:
docker run --rm --network qfin_default \
-v brrr_prefect_data:/root/.prefect \
-v $(pwd)/pgloader.load:/load.load \
dimitri/pgloader:latest pgloader /load.load
Why we exclude alembic_version
- The migration you ran in step 2 created the Postgres schema and wrote Postgres’s own migration revision into alembic_version.
- If you copy the SQLite alembic_version, you’ll end up with a mismatched revision and future migrations could fail.
4) Sanity checks
- Spot-check a few counts after pgloader finishes:
docker exec -it $(docker ps -qf "name=postgres") psql -U prefect -d prefect -c "SELECT COUNT(*) FROM flow_run;"
docker exec -it $(docker ps -qf "name=postgres") psql -U prefect -d prefect -c "SELECT COUNT(*) FROM task_run;"
docker exec -it $(docker ps -qf "name=postgres") psql -U prefect -d prefect -c "SELECT COUNT(*) FROM log;"
docker exec -it $(docker ps -qf "name=postgres") psql -U prefect -d prefect -c "SELECT COUNT(*) FROM events;"
5) Start Prefect against Postgres
- In your compose:
- Remove the angle brackets from the connection URL.
- Pin the image during migration: prefecthq/prefect:3.4.17.
- Keep the event settings you added.
- Bring the server back up, then the workers.
- If you want to be extra safe, run:
docker exec -it $(docker ps -qf "name=prefect-server") prefect server database upgrade -y
It should be a quick no-op.
If pgloader throws data casting errors
- JSON columns: your Prefect JSON/JSONB columns typically contain valid JSON; pgloader will usually cast fine. If it complains, we can add a CAST rule to the load file. Share the error and I’ll give you the exact CAST line.
- Foreign keys: pgloader usually orders inserts to satisfy FKs; if not, we can temporarily disable constraints, but that’s rarely needed for Prefect.
After you’re up
- Your new Postgres-backed server + the event settings should materially reduce RAM spikes and speed up the UI.
- You can later tune pool sizing if needed:
PREFECT_SERVER_DATABASE_SQLALCHEMY_POOL_SIZE=10
PREFECT_SERVER_DATABASE_SQLALCHEMY_MAX_OVERFLOW=5
PREFECT_SERVER_DATABASE_SQLALCHEMY_POOL_TIMEOUT=30
Want me to review your full compose (server + workers) and pin/clean it before you flip over? If you paste the omitted parts, I’ll annotate and hand back a ready-to-run file.Olve Heitmann
11/19/2025, 11:15 PMMarvin
11/19/2025, 11:15 PMMarvin
11/19/2025, 11:16 PMdocker compose up -d postgres
2) Re-run the migration on that network (remove the angle brackets and pin Prefect to 3.4.17):
docker run --rm --network qfin_default \
-e PREFECT_API_DATABASE_CONNECTION_URL=<postgresql+asyncpg://prefect:prefect@postgres:5432/prefect> \
prefecthq/prefect:3.4.17 prefect server database upgrade -y
3) Run pgloader in data-only mode (after creating a load file — see below).
Option B — Skip the Compose network and connect via the published host port
Since Postgres is published on 5432, just connect to the host from the container. On Docker Desktop (Windows/macOS), use host.docker.internal:
- Create schema with Prefect migrations:
docker run --rm \
-e PREFECT_API_DATABASE_CONNECTION_URL=<postgresql+asyncpg://prefect:prefect@host.docker.internal:5432/prefect> \
prefecthq/prefect:3.4.17 prefect server database upgrade -y
- Run pgloader (data-only) using host.docker.internal as the target:
Create pgloader.load in your working dir:
LOAD DATABASE
FROM sqlite:///root/.prefect/prefect.db
INTO <postgresql://prefect:prefect@host.docker.internal:5432/prefect>
WITH data only,
batch rows = 5000,
prefetch rows = 5000,
on error stop
EXCLUDING TABLE NAMES LIKE 'alembic_version'
;
Then run:
docker run --rm \
-v brrr_prefect_data:/root/.prefect \
-v $(pwd)/pgloader.load:/load.load \
dimitri/pgloader:latest pgloader /load.load
Notes and small fixes
- Remove angle brackets from your env values. Use:
- PREFECT_API_DATABASE_CONNECTION_URL=<postgresql+asyncpg://prefect:prefect@postgres:5432/prefect>
- Pin Prefect image to your running version during migration: prefecthq/prefect:3.4.17
- If you prefer using Compose for the migration, add a one-off service:
migrate:
image: prefecthq/prefect:3.4.17
depends_on:
postgres:
condition: service_healthy
environment:
PREFECT_API_DATABASE_CONNECTION_URL: <postgresql+asyncpg://prefect:prefect@postgres:5432/prefect>
command: prefect server database upgrade -y
Then:
docker compose run --rm migrate
After pgloader finishes
- Sanity-check a few counts:
docker exec -it $(docker ps -qf "name=postgres") \
psql -U prefect -d prefect -c "SELECT COUNT(*) FROM flow_run;"
docker exec -it $(docker ps -qf "name=postgres") \
psql -U prefect -d prefect -c "SELECT COUNT(*) FROM task_run;"
docker exec -it $(docker ps -qf "name=postgres") \
psql -U prefect -d prefect -c "SELECT COUNT(*) FROM log;"
docker exec -it $(docker ps -qf "name=postgres") \
psql -U prefect -d prefect -c "SELECT COUNT(*) FROM events;"
- Start your Prefect server with the Postgres URL and the event settings you added.
- Bring workers back up.
References
- Server CLI (database commands): Self-hosted Server CLI
- Database settings (connection URL and timeouts): Settings reference — database
- Events settings (retention/backfill): Settings reference — server events
If you want, paste your full compose and I’ll mark it up so you can run it end-to-end without switching networks mid-process.Olve Heitmann
11/19/2025, 11:24 PMMarvin
11/19/2025, 11:24 PMMarvin
11/19/2025, 11:24 PMLOAD DATABASE
FROM sqlite:///root/.prefect/prefect.db
INTO <postgresql://prefect:prefect@postgres/prefect>
WITH data only,
batch rows = 5000,
prefetch rows = 5000,
on error stop
-- Exclude SQLite internal tables and Alembic version
EXCLUDING TABLE NAMES LIKE 'sqlite_%'
EXCLUDING TABLE NAMES LIKE 'alembic_version'
-- Truncate small seeded table to avoid duplicate unique keys
BEFORE LOAD DO
$$ TRUNCATE TABLE configuration; $$
;
3) Run pgloader again
docker run --rm --network qfin_default \
-v brrr_prefect_data:/root/.prefect \
-v $(pwd)/pgloader.load:/load.load \
dimitri/pgloader:latest pgloader /load.load
4) If you see any other duplicate key errors
- It’s typically just configuration. If pgloader reports duplicates on another small seeded table, you can add another TRUNCATE in the “BEFORE LOAD DO” block, e.g.:
BEFORE LOAD DO
$$ TRUNCATE TABLE configuration; $$
$$ TRUNCATE TABLE <table_name>; $$
Paste the exact error and I’ll give you the exact line(s) to add.
5) Sanity check
docker exec -it $(docker ps -qf "name=postgres") \
psql -U prefect -d prefect -c "SELECT COUNT(*) FROM flow_run;"
docker exec -it $(docker ps -qf "name=postgres") \
psql -U prefect -d prefect -c "SELECT COUNT(*) FROM task_run;"
docker exec -it $(docker ps -qf "name=postgres") \
psql -U prefect -d prefect -c "SELECT COUNT(*) FROM log;"
docker exec -it $(docker ps -qf "name=postgres") \
psql -U prefect -d prefect -c "SELECT COUNT(*) FROM events;"
6) Start Prefect against Postgres
- In your compose, ensure:
- No angle brackets in the URL
- Pin the image for the migration: prefecthq/prefect:3.4.17
- Keep the event settings you added
- Bring up the server, then workers. If you want, run a final no-op migration:
docker exec -it $(docker ps -qf "name=prefect-server") prefect server database upgrade -y
Reference
- Server CLI (database commands): Self-hosted Server CLI
- Database settings: Settings reference — database
- Events settings (retention/backfill): Settings reference — server events
If the next pgloader run throws a different constraint error, paste it here and I’ll adjust the load file precisely (either TRUNCATE the target table or exclude it if it’s safe to keep Postgres’s seeded values).Olve Heitmann
11/19/2025, 11:28 PMMarvin
11/19/2025, 11:28 PMMarvin
11/19/2025, 11:28 PMMarvin
11/19/2025, 11:29 PMdocker compose up -d postgres
3) Reset the Postgres schema (wipes any partial data)
- This drops and recreates all tables. It will also seed some defaults, which we’ll truncate before the load.
docker run --rm --network qfin_default \
-e PREFECT_API_DATABASE_CONNECTION_URL=<postgresql+asyncpg://prefect:prefect@postgres:5432/prefect> \
prefecthq/prefect:3.4.17 prefect server database reset -y
Optional: Re-run upgrade just to be explicit.
docker run --rm --network qfin_default \
-e PREFECT_API_DATABASE_CONNECTION_URL=<postgresql+asyncpg://prefect:prefect@postgres:5432/prefect> \
prefecthq/prefect:3.4.17 prefect server database upgrade -y
4) Create a stronger pgloader config
- Replace pgloader.load with this. It:
- TRUNCATEs all public tables except alembic_version (so the DB is empty),
- excludes SQLite internal tables and alembic_version,
- uses smaller batch sizes to avoid the Lisp heap OOM you hit.
LOAD DATABASE
FROM sqlite:///root/.prefect/prefect.db
INTO <postgresql://prefect:prefect@postgres:5432/prefect>
WITH data only,
batch rows = 1000,
prefetch rows = 1000,
on error stop
BEFORE LOAD DO
$$
DO $do$
DECLARE r record;
BEGIN
FOR r IN
SELECT tablename
FROM pg_tables
WHERE schemaname = 'public'
AND tablename <> 'alembic_version'
LOOP
EXECUTE 'TRUNCATE TABLE public.' || quote_ident(r.tablename) || ' CASCADE';
END LOOP;
END;
$do$;
$$
EXCLUDING TABLE NAMES LIKE 'sqlite_%'
EXCLUDING TABLE NAMES LIKE 'alembic_version'
;
5) Run pgloader with the volume mounts
docker run --rm --network qfin_default \
-v brrr_prefect_data:/root/.prefect \
-v $(pwd)/pgloader.load:/load.load \
dimitri/pgloader:latest pgloader /load.load
Tip: If you still see heap exhausted, either:
- lower batch/prefetch further (e.g., 500), or
- give the container more memory: docker run --memory=4g ...
6) Sanity checks
docker exec -it $(docker ps -qf "name=postgres") psql -U prefect -d prefect -c "SELECT COUNT(*) FROM flow_run;"
docker exec -it $(docker ps -qf "name=postgres") psql -U prefect -d prefect -c "SELECT COUNT(*) FROM task_run;"
docker exec -it $(docker ps -qf "name=postgres") psql -U prefect -d prefect -c "SELECT COUNT(*) FROM log;"
docker exec -it $(docker ps -qf "name=postgres") psql -U prefect -d prefect -c "SELECT COUNT(*) FROM events;"
7) Start Prefect against Postgres (and keep your event settings)
- In compose:
- Remove angle brackets from the URL.
- Pin the image to 3.4.17 for now.
- Keep:
- PREFECT_SERVER_EVENTS_RETENTION_PERIOD=P2D
- PREFECT_SERVER_EVENTS_MAXIMUM_WEBSOCKET_BACKFILL=PT300S
- PREFECT_SERVER_EVENTS_WEBSOCKET_BACKFILL_PAGE_SIZE=100
- PREFECT_SERVER_EVENTS_RELATED_RESOURCE_CACHE_TTL=PT300S
- Bring up the server, then workers. Avoid running your “create-work-pools” init until after migration completes (it can re-create resources you already migrated).
Why this fixes the errors you saw
- The varchar/text warnings are harmless.
- sqlite_stat1 is excluded now.
- Duplicates (flow, logs, block types, work pools, etc) happen if the target already has rows — reset + TRUNCATE ensures the target is empty before loading.
- We keep Postgres’s alembic_version intact so future migrations work.Marvin
11/19/2025, 11:29 PMOlve Heitmann
11/19/2025, 11:32 PMMarvin
11/19/2025, 11:32 PMMarvin
11/19/2025, 11:32 PMMarvin
11/19/2025, 11:34 PMMarvin
11/19/2025, 11:36 PMalembic_version contains a SQLite migration revision (8bb517bae6f9). Alembic can’t find that revision in the Postgres migration chain, so reset can’t downgrade to base.
Two ways to recover — pick the one you prefer:
Option A: Stamp to a valid Postgres revision, then reset
1) Stamp the DB to the current Postgres head (this only fixes the revision pointer; it won’t touch data):
docker run --rm --network qfin_default \
-e PREFECT_API_DATABASE_CONNECTION_URL=<postgresql+asyncpg://prefect:prefect@postgres:5432/prefect> \
prefecthq/prefect:3.4.17 prefect server database stamp head
2) Now reset (downgrade to base then upgrade back to head):
docker run --rm --network qfin_default \
-e PREFECT_API_DATABASE_CONNECTION_URL=<postgresql+asyncpg://prefect:prefect@postgres:5432/prefect> \
prefecthq/prefect:3.4.17 prefect server database reset -y
3) Run the pgloader data-only import with a TRUNCATE of all tables except alembic_version to avoid duplicates:
Create/replace pgloader.load:
LOAD DATABASE
FROM sqlite:///root/.prefect/prefect.db
INTO <postgresql://prefect:prefect@postgres:5432/prefect>
WITH data only,
batch rows = 1000,
prefetch rows = 1000,
on error stop
BEFORE LOAD DO
$$
DO $do$
DECLARE r record;
BEGIN
FOR r IN
SELECT tablename
FROM pg_tables
WHERE schemaname = 'public'
AND tablename <> 'alembic_version'
LOOP
EXECUTE 'TRUNCATE TABLE public.' || quote_ident(r.tablename) || ' CASCADE';
END LOOP;
END;
$do$;
$$
EXCLUDING TABLE NAMES LIKE 'sqlite_%'
EXCLUDING TABLE NAMES LIKE 'alembic_version'
;
Run it:
docker run --rm --network qfin_default \
-v brrr_prefect_data:/root/.prefect \
-v $(pwd)/pgloader.load:/load.load \
dimitri/pgloader:latest pgloader /load.load
Option B: Nuke and recreate the Postgres DB (clean slate)
- Stop Postgres and delete the data volume (fastest if this DB is only for Prefect):
- Find the volume name: docker volume ls (likely qfin_postgres_data)
- Remove it: docker volume rm qfin_postgres_data
- Bring Postgres back up:
docker compose up -d postgres
- Create schema:
docker run --rm --network qfin_default \
-e PREFECT_API_DATABASE_CONNECTION_URL=<postgresql+asyncpg://prefect:prefect@postgres:5432/prefect> \
prefecthq/prefect:3.4.17 prefect server database upgrade -y
- Then run the same pgloader command above (the TRUNCATE block is harmless on empty tables).
Notes
- Keep Prefect server/workers/any init jobs stopped during migration to avoid new writes and duplicates.
- If pgloader runs out of heap again, lower `batch rows`/`prefetch rows` to 500 or add a memory limit override like --memory=4g.
- After pgloader completes, you can run a no-op sync:
docker run --rm --network qfin_default \
-e PREFECT_API_DATABASE_CONNECTION_URL=<postgresql+asyncpg://prefect:prefect@postgres:5432/prefect> \
prefecthq/prefect:3.4.17 prefect server database upgrade -y
- In your compose, pin the server to prefecthq/prefect:3.4.17, remove angle brackets in env vars, and keep your event settings:
- PREFECT_SERVER_EVENTS_RETENTION_PERIOD=P2D
- PREFECT_SERVER_EVENTS_MAXIMUM_WEBSOCKET_BACKFILL=PT300S
- PREFECT_SERVER_EVENTS_WEBSOCKET_BACKFILL_PAGE_SIZE=100
- PREFECT_SERVER_EVENTS_RELATED_RESOURCE_CACHE_TTL=PT300S
Sanity checks after load
docker exec -it $(docker ps -qf "name=postgres") psql -U prefect -d prefect -c "SELECT COUNT(*) FROM flow_run;"
docker exec -it $(docker ps -qf "name=postgres") psql -U prefect -d prefect -c "SELECT COUNT(*) FROM task_run;"
docker exec -it $(docker ps -qf "name=postgres") psql -U prefect -d prefect -c "SELECT COUNT(*) FROM log;"
docker exec -it $(docker ps -qf "name=postgres") psql -U prefect -d prefect -c "SELECT COUNT(*) FROM events;"Marvin
11/19/2025, 11:36 PMOlve Heitmann
11/19/2025, 11:41 PMMarvin
11/19/2025, 11:41 PMMarvin
11/19/2025, 11:42 PMPREFECT_API_DATABASE_CONNECTION_URL=<...>. Those must be removed — the URL should be a plain string.
- Using the same image tag for migration and for your server avoids revision mismatches. If you’re going to run the server as prefecthq/prefect:3-latest, use that for the migration too.
Let’s do a clean, predictable migration using docker compose run so we inherit the same env as your server.
1) Fix your compose env and pin the tag you intend to run
- In your docker-compose.yml, change these (remove angle brackets; keep your event settings):
services:
prefect-server:
image: prefecthq/prefect:3-latest
environment:
PREFECT_API_DATABASE_CONNECTION_URL: <postgresql+asyncpg://prefect:prefect@postgres:5432/prefect>
PREFECT_API_DATABASE_MIGRATE_ON_START: "false" # we will run migrations manually
PREFECT_SERVER_EVENTS_RETENTION_PERIOD: "P2D"
PREFECT_SERVER_EVENTS_MAXIMUM_WEBSOCKET_BACKFILL: "PT300S"
PREFECT_SERVER_EVENTS_WEBSOCKET_BACKFILL_PAGE_SIZE: "100"
PREFECT_SERVER_EVENTS_RELATED_RESOURCE_CACHE_TTL: "PT300S"
command: prefect server start --host 0.0.0.0
2) Nuke only the Postgres data volume to start fresh
- Stop everything and remove the Postgres data volume:
docker compose down
docker volume rm qfin_postgres_data
If your volume name is just postgres_data under this project, Docker will have named it qfin_postgres_data. You can confirm with:
docker volume ls | grep qfin
3) Bring Postgres up, then create schema via compose-run
docker compose up -d postgres
# optional but recommended
docker exec -it qfin-postgres-1 psql -U prefect -d prefect -c "CREATE EXTENSION IF NOT EXISTS pg_trgm;"
# run migrations using the same image/env as your server service
docker compose run --rm prefect-server prefect server database upgrade -y
Using compose run ensures the connection URL/env is exactly the same as your service. If this fails, please paste the FULL traceback so I can pinpoint it.
4) Use pgloader with a data-only load and TRUNCATE
- Create pgloader.load in your current dir:
LOAD DATABASE
FROM sqlite:///root/.prefect/prefect.db
INTO <postgresql://prefect:prefect@postgres:5432/prefect>
WITH data only,
batch rows = 1000,
prefetch rows = 1000,
on error stop
BEFORE LOAD DO
$$
DO $do$
DECLARE r record;
BEGIN
FOR r IN
SELECT tablename
FROM pg_tables
WHERE schemaname = 'public'
AND tablename <> 'alembic_version'
LOOP
EXECUTE 'TRUNCATE TABLE public.' || quote_ident(r.tablename) || ' CASCADE';
END LOOP;
END;
$do$;
$$
EXCLUDING TABLE NAMES LIKE 'sqlite_%'
EXCLUDING TABLE NAMES LIKE 'alembic_version'
;
- Run pgloader on the compose network with your SQLite file mounted:
docker run --rm --network qfin_default \
-v brrr_prefect_data:/root/.prefect \
-v $(pwd)/pgloader.load:/load.load \
dimitri/pgloader:latest pgloader /load.load
If it OOMs again, reduce batch/prefetch to 500 or add memory: --memory=4g.
5) Sanity checks
docker exec -it qfin-postgres-1 psql -U prefect -d prefect -c "SELECT COUNT(*) FROM flow_run;"
docker exec -it qfin-postgres-1 psql -U prefect -d prefect -c "SELECT COUNT(*) FROM task_run;"
docker exec -it qfin-postgres-1 psql -U prefect -d prefect -c "SELECT COUNT(*) FROM log;"
docker exec -it qfin-postgres-1 psql -U prefect -d prefect -c "SELECT COUNT(*) FROM events;"
6) Start Prefect and workers
docker compose up -d prefect-server
# once healthy, start workers
docker compose up -d prefect-prod-worker prefect-dev-worker
If “upgrade migrations” still errors
- It’s almost always one of:
- URL still malformed (angle brackets or quoting issues)
- DB not reachable (network mismatch)
- Postgres extension missing (pg_trgm)Marvin
11/19/2025, 11:42 PMOlve Heitmann
11/19/2025, 11:50 PMexternal: true to use an existing volume
[+] Creating 1/1
✔️ Container qfin-postgres-1 Running 0.0s
Running upgrade migrations ...
Migrations succeeded!
Prefect database at postgresql+asyncpg://prefect:***@postgres:5432/prefect upgraded!
got this warning on #3. Is that an issue? brrr_prefect_data is volume after my sqlite implementation and is still listed under volumes in docker compose
volumes:
prefect_data:
name: brrr_prefect_data
postgres_data:
also references under volumes in prefect-server:
volumes: # persist Prefect's local data/db
- prefect_data:/root/.prefectMarvin
11/19/2025, 11:50 PMMarvin
11/19/2025, 11:50 PMvolumes:
prefect_data:
external: true
name: brrr_prefect_data
postgres_data:
And keep the service mount as-is:
services:
prefect-server:
...
volumes:
- prefect_data:/root/.prefect
Notes:
- For a Postgres-backed server, mounting ~/.prefect is not required anymore. I’d keep it mounted until you finish the pgloader copy, then remove the mount (and delete the volume after you’ve backed up the SQLite file), to avoid confusion.
Finish the migration
1) Create/confirm the pgloader config file (data-only, exclude alembic_version and sqlite internals, TRUNCATE target tables):
LOAD DATABASE
FROM sqlite:///root/.prefect/prefect.db
INTO <postgresql://prefect:prefect@postgres:5432/prefect>
WITH data only,
batch rows = 1000,
prefetch rows = 1000,
on error stop
BEFORE LOAD DO
$$
DO $do$
DECLARE r record;
BEGIN
FOR r IN
SELECT tablename
FROM pg_tables
WHERE schemaname = 'public'
AND tablename <> 'alembic_version'
LOOP
EXECUTE 'TRUNCATE TABLE public.' || quote_ident(r.tablename) || ' CASCADE';
END LOOP;
END;
$do$;
$$
EXCLUDING TABLE NAMES LIKE 'sqlite_%'
EXCLUDING TABLE NAMES LIKE 'alembic_version'
;
2) Run pgloader on the compose network:
docker run --rm --network qfin_default \
-v brrr_prefect_data:/root/.prefect \
-v $(pwd)/pgloader.load:/load.load \
dimitri/pgloader:latest pgloader /load.load
- If it runs out of memory, drop batch/prefetch to 500 or add --memory=4g.
3) Sanity check a few counts:
docker exec -it qfin-postgres-1 psql -U prefect -d prefect -c "SELECT COUNT(*) FROM flow_run;"
docker exec -it qfin-postgres-1 psql -U prefect -d prefect -c "SELECT COUNT(*) FROM task_run;"
docker exec -it qfin-postgres-1 psql -U prefect -d prefect -c "SELECT COUNT(*) FROM log;"
docker exec -it qfin-postgres-1 psql -U prefect -d prefect -c "SELECT COUNT(*) FROM events;"
4) Start Prefect against Postgres (keep your event settings), then start workers.
Optional cleanup after verification
- Backup the SQLite file from the old volume:
docker run --rm -v brrr_prefect_data:/data -v $(pwd):/backup alpine sh -c "cp /data/prefect.db /backup/prefect.db.backup.sqlite"
- Remove the mount from prefect-server and, when you’re comfortable, remove the old volume:
docker volume rm brrr_prefect_data
If pgloader throws any new duplicate/constraint errors, paste the exact lines and I’ll tweak the load file further.Olve Heitmann
11/19/2025, 11:55 PMMarvin
11/19/2025, 11:55 PMMarvin
11/19/2025, 11:57 PMflow_run.total_run_time, task_run.total_run_time) are stored as epoch-based timestamps in SQLite (e.g., “1970-01-01 000000.082295”) but are INTERVAL in Postgres. We need to transform them.
- The FK errors on state tables happened because some parent rows failed to load earlier (due to the interval errors). Fixing the interval transform should unblock FK creation.
Let’s make pgloader do a text-then-convert approach so COPY succeeds, then we normalize to interval in Postgres.
1) Use this pgloader.load
- It truncates all target tables (except alembic_version), excludes SQLite internals, loads everything as-is, but first makes those columns TEXT; then after load converts the values from “YYYY-MM-DD HHMMSS.US” to “HHMMSS.US” and casts back to INTERVAL.
Create/replace pgloader.load with:
LOAD DATABASE
FROM sqlite:///root/.prefect/prefect.db
INTO <postgresql://prefect:prefect@postgres:5432/prefect>
WITH data only,
batch rows = 500,
prefetch rows = 500,
on error stop
BEFORE LOAD DO
$$
-- 1) Empty all public tables, keep alembic_version
DO $do$
DECLARE r record;
BEGIN
FOR r IN
SELECT tablename
FROM pg_tables
WHERE schemaname = 'public'
AND tablename <> 'alembic_version'
LOOP
EXECUTE 'TRUNCATE TABLE public.' || quote_ident(r.tablename) || ' CASCADE';
END LOOP;
END;
$do$;
-- 2) Temporarily make total_run_time columns TEXT so COPY won't fail
ALTER TABLE flow_run ALTER COLUMN total_run_time TYPE text;
ALTER TABLE task_run ALTER COLUMN total_run_time TYPE text;
$$
EXCLUDING TABLE NAMES LIKE 'sqlite_%'
EXCLUDING TABLE NAMES LIKE 'alembic_version'
AFTER LOAD DO
$$
-- 3) Normalize text values: strip the date part, keep only time part 'HH:MM:<http://SS.US|SS.US>'
UPDATE flow_run
SET total_run_time =
CASE
WHEN total_run_time IS NULL OR total_run_time = '' THEN NULL
WHEN position(' ' in total_run_time) > 0 THEN substring(total_run_time from position(' ' in total_run_time) + 1)
ELSE total_run_time
END;
UPDATE task_run
SET total_run_time =
CASE
WHEN total_run_time IS NULL OR total_run_time = '' THEN NULL
WHEN position(' ' in total_run_time) > 0 THEN substring(total_run_time from position(' ' in total_run_time) + 1)
ELSE total_run_time
END;
-- 4) Cast back to INTERVAL (Postgres accepts 'HH:MM:<http://SS.US|SS.US>' as an interval literal)
ALTER TABLE flow_run
ALTER COLUMN total_run_time TYPE interval
USING NULLIF(total_run_time, '')::interval;
ALTER TABLE task_run
ALTER COLUMN total_run_time TYPE interval
USING NULLIF(total_run_time, '')::interval;
$$
;
2) Run it with smaller batches (and consider more memory)
docker run --rm --network qfin_default \
-v brrr_prefect_data:/root/.prefect \
-v $(pwd)/pgloader.load:/load.load \
dimitri/pgloader:latest pgloader /load.load
- If you still see heap exhausted from pgloader, either:
- reduce to batch rows = 200 and prefetch rows = 200, or
- add memory to the container: docker run --memory=4g ..., and/or
- increase Docker Desktop/WSL memory in settings.
3) Why this resolves your errors
- Interval errors: COPY now inserts the SQLite value as TEXT; we then normalize to a time-like string and cast to INTERVAL after the load.
- Foreign key errors: those were a knock‑on effect of failed parent inserts; once flow_run/task_run insert cleanly, FK creation on state tables should succeed when pgloader reapplies constraints/indexes at the end.
4) Optional: reduce load size further
- If your logs/events are huge and not essential historically, you can temporarily exclude them to get unblocked, then circle back:
- Add to the load file:
- EXCLUDING TABLE NAMES LIKE 'events'
- EXCLUDING TABLE NAMES LIKE 'log'
- Or migrate those two tables separately later.
5) After load: sanity checks
```Marvin
11/19/2025, 11:57 PM6) Compose volume notice
- Your warning about the old volume is fine; declare it as external to silence:
volumes:
prefect_data:
external: true
name: brrr_prefect_data
postgres_data:
```
- Keep it mounted through the migration. After verification, you can remove that mount (Postgres is your source of truth now) and archive/remove the old SQLite volume whenever you’re ready.
If you hit any new errors (especially on AFTER LOAD), paste the exact lines and I’ll tweak the statements. If pgloader continues to struggle with memory on logs/events, we can exclude those tables for now to complete the migration, then either re‑ingest a recent slice or let your retention settings rebuild from fresh data.Olve Heitmann
11/20/2025, 12:01 AMMarvin
11/20/2025, 12:01 AMMarvin
11/20/2025, 12:02 AMLOAD DATABASE
FROM sqlite:///root/.prefect/prefect.db
INTO <postgresql://prefect:prefect@postgres:5432/prefect>
WITH data only,
truncate, -- pgloader will TRUNCATE each target table before copying
batch rows = 500,
prefetch rows = 500,
on error stop
EXCLUDING TABLE NAMES LIKE 'sqlite_%'
EXCLUDING TABLE NAMES LIKE 'alembic_version'
-- Temporarily change interval columns to text so COPY won’t fail on SQLite-formatted values
BEFORE LOAD DO $$ ALTER TABLE flow_run ALTER COLUMN total_run_time TYPE text; $$
BEFORE LOAD DO $$ ALTER TABLE task_run ALTER COLUMN total_run_time TYPE text; $$
-- Normalize the text values to time-only strings
AFTER LOAD DO $$
UPDATE flow_run
SET total_run_time =
CASE
WHEN total_run_time IS NULL OR total_run_time = '' THEN NULL
WHEN position(' ' in total_run_time) > 0 THEN substring(total_run_time from position(' ' in total_run_time) + 1)
ELSE total_run_time
END;
$$
AFTER LOAD DO $$
UPDATE task_run
SET total_run_time =
CASE
WHEN total_run_time IS NULL OR total_run_time = '' THEN NULL
WHEN position(' ' in total_run_time) > 0 THEN substring(total_run_time from position(' ' in total_run_time) + 1)
ELSE total_run_time
END;
$$
-- Cast back to INTERVAL
AFTER LOAD DO $$
ALTER TABLE flow_run
ALTER COLUMN total_run_time TYPE interval
USING NULLIF(total_run_time, '')::interval;
$$
AFTER LOAD DO $$
ALTER TABLE task_run
ALTER COLUMN total_run_time TYPE interval
USING NULLIF(total_run_time, '')::interval;
$$
;
Run it
docker run --rm --network qfin_default \
-v brrr_prefect_data:/root/.prefect \
-v $(pwd)/pgloader.load:/load.load \
dimitri/pgloader:latest pgloader /load.load
- If pgloader’s heap still exhausts, either lower to 200/200 or give it more memory:
docker run --rm --memory=4g --network qfin_default \
-v brrr_prefect_data:/root/.prefect \
-v $(pwd)/pgloader.load:/load.load \
dimitri/pgloader:latest pgloader /load.load
Optional: exclude logs and events on the first pass (helps with memory)
- Add these two lines to the EXCLUDING section to unblock the core entities:
EXCLUDING TABLE NAMES LIKE 'log'
EXCLUDING TABLE NAMES LIKE 'events'
- You can run a second pgloader job later just for those tables (or decide to keep fresh logs/events given you’ve set short retention).
Why this fixes the errors
- “cannot insert multiple commands into a prepared statement” is resolved by giving pgloader one statement per BEFORE/AFTER clause.
- Interval parse errors are resolved by temporarily making those columns text, normalizing their values, and casting back to interval after load.
- FK errors on state tables were fallout from failed parent inserts; once the parent tables load cleanly, FK creation should succeed.
Sanity-check after load
```
docker exec -it qfin-postgres-1 psql -U prefect -d prefect -c "SELECT COUNT(*) FROM flow_run;"
docker exec -it qfin-postgres-1 psql -U prefect -d prefect -c "SELECT COUNT(*) FROM task_run;"
docker exec -it qfin-postgres-1 psql -U prefect -d prefect -c "SELECT COUNT(*) FROM flow_run_state;"
docker exec -it qfin-postgres-1 psql -U prefect -d prefect -c "SELECT COUNT(*) FROM task_run_state;"Marvin
11/20/2025, 12:02 AMPREFECT_SERVER_EVENTS_RETENTION_PERIOD=P2D
- PREFECT_SERVER_EVENTS_MAXIMUM_WEBSOCKET_BACKFILL=PT300S
- PREFECT_SERVER_EVENTS_WEBSOCKET_BACKFILL_PAGE_SIZE=100
- PREFECT_SERVER_EVENTS_RELATED_RESOURCE_CACHE_TTL=PT300S
If this run surfaces any new specific table/constraint errors, paste those lines and I’ll adjust the load file accordingly.Olve Heitmann
11/20/2025, 12:05 AMMarvin
11/20/2025, 12:05 AMMarvin
11/20/2025, 12:07 AMLOAD DATABASE
FROM sqlite:///root/.prefect/prefect.db
INTO <postgresql://prefect:prefect@postgres:5432/prefect>
WITH data only,
truncate,
batch rows = 200,
prefetch rows = 200,
on error stop
EXCLUDING TABLE NAMES LIKE 'sqlite_%'
EXCLUDING TABLE NAMES LIKE 'alembic_version'
EXCLUDING TABLE NAMES LIKE 'log'
EXCLUDING TABLE NAMES LIKE 'events'
BEFORE LOAD DO $$ ALTER TABLE flow_run ALTER COLUMN total_run_time TYPE text; $$
BEFORE LOAD DO $$ ALTER TABLE task_run ALTER COLUMN total_run_time TYPE text; $$
AFTER LOAD DO $$
UPDATE flow_run
SET total_run_time =
CASE
WHEN total_run_time IS NULL OR total_run_time = '' THEN NULL
WHEN position(' ' in total_run_time) > 0 THEN substring(total_run_time from position(' ' in total_run_time) + 1)
ELSE total_run_time
END;
$$
AFTER LOAD DO $$
UPDATE task_run
SET total_run_time =
CASE
WHEN total_run_time IS NULL OR total_run_time = '' THEN NULL
WHEN position(' ' in total_run_time) > 0 THEN substring(total_run_time from position(' ' in total_run_time) + 1)
ELSE total_run_time
END;
$$
AFTER LOAD DO $$
ALTER TABLE flow_run
ALTER COLUMN total_run_time TYPE interval
USING NULLIF(total_run_time, '')::interval;
$$
AFTER LOAD DO $$
ALTER TABLE task_run
ALTER COLUMN total_run_time TYPE interval
USING NULLIF(total_run_time, '')::interval;
$$
;
Run it with extra memory (and keep your compose network):
docker run --rm --memory=6g --memory-swap=6g --network qfin_default \
-v brrr_prefect_data:/root/.prefect \
-v $(pwd)/pgloader.load:/load.load \
dimitri/pgloader:latest pgloader /load.load
After this, you’ll have flows, deployments, work pools/queues, blocks, runs/states, etc. Logs/events will start fresh (and you’ve already shortened retention, which helps UI performance).
If you later want recent logs/events, we can import just the last N days via CSV without stressing pgloader.
Option 2 — Migrate only metadata (easiest, smallest)
If you primarily care about deployments, blocks, work pools/queues, variables, and don’t need historical runs/logs/events, do a metadata-only copy. This is tiny and very reliable.
Example load file:
LOAD DATABASE
FROM sqlite:///root/.prefect/prefect.db
INTO <postgresql://prefect:prefect@postgres:5432/prefect>
WITH data only,
truncate,
batch rows = 500,
prefetch rows = 500,
on error stop
INCLUDING ONLY TABLE NAMES MATCHING
'block_type', 'block_schema', 'block_schema_reference',
'block_document', 'block_document_reference',
'deployment', 'deployment_version', 'flow', 'flow_tag',
'work_pool', 'work_pool_queue', 'worker', 'concurrency_limit',
'variable', 'automation%', 'artifact%'
EXCLUDING TABLE NAMES LIKE 'sqlite_%'
EXCLUDING TABLE NAMES LIKE 'alembic_version'
;
This preserves your deploys/blocks/pools so you can keep working immediately. Historical runs/logs/events will start fresh (usually fine given your new retention and that UI speed is your priority).
Option 3 — Skip pgloader for heavy tables; import recent slices via CSV
If you want, say, the last 3–7 days of logs/events:
- Export from SQLite with a WHERE filter (e.g., timestamp >= now - 7 days)Marvin
11/20/2025, 12:07 AMvolumes:
prefect_data:
external: true
name: brrr_prefect_data
postgres_data:
If you want me to:
- prepare a “metadata-only” pgloader file that exactly matches your DB (or)
- craft CSV-based commands to import a recent slice of logs/events
…just tell me which path you prefer and the time window you want for history (e.g., last 3/7/14 days).Olve Heitmann
11/20/2025, 12:18 AMMarvin
11/20/2025, 12:18 AMMarvin
11/20/2025, 12:19 AMdocker exec -it qfin-postgres-1 psql -U prefect -d prefect -c "SELECT now(), phase, relid::regclass, bytes_processed, tuples_processed FROM pg_stat_progress_copy;"
docker exec -it qfin-postgres-1 psql -U prefect -d prefect -c \"SELECT state, query, wait_event_type, wait_event FROM pg_stat_activity WHERE state <> 'idle';\"
If you see rows in these, it’s working; if not, it’s likely stalled on memory/transform.
If it’s stalling, here are the two most reliable paths:
Option A — Get you running now: migrate only metadata (no runs/logs/events)
This is the smallest copy, very reliable, and gets your server back up quickly. You keep deployments, blocks, work pools/queues, variables, automations, artifacts, etc. Historical runs/logs/events start fresh (usually fine given you want the UI fast and have shorter retention).
Use this pgloader.load:
LOAD DATABASE
FROM sqlite:///root/.prefect/prefect.db
INTO <postgresql://prefect:prefect@postgres:5432/prefect>
WITH data only,
truncate,
batch rows = 200,
prefetch rows = 200,
on error stop
INCLUDING ONLY TABLE NAMES MATCHING
'block_type', 'block_schema', 'block_schema_reference',
'block_document', 'block_document_reference',
'deployment', 'deployment_version', 'flow', 'flow_tag',
'work_pool', 'work_pool_queue', 'worker', 'concurrency_limit',
'variable', 'automation%', 'artifact%'
EXCLUDING TABLE NAMES LIKE 'sqlite_%'
EXCLUDING TABLE NAMES LIKE 'alembic_version'
;
Run:
docker run --rm --network qfin_default \
-v brrr_prefect_data:/root/.prefect \
-v $(pwd)/pgloader.load:/load.load \
--memory=6g --memory-swap=6g \
dimitri/pgloader:latest pgloader /load.load
Then bring up Prefect against Postgres (keep your event settings). You’ll be up and stable with good UI performance. If you want, we can later import a small recent slice of runs via CSV.
Option B — Bring over recent runs without crushing memory (CSV, targeted)
If you want the last N days of flow/task runs too, we can export a filtered CSV from SQLite and import into Postgres. Example for 7 days:
1) Export recent runs from SQLite to CSV
docker run --rm -v brrr_prefect_data:/data alpine sh -lc '
apk add --no-cache sqlite;
sqlite3 -csv /data/prefect.db "
.headers on
.mode csv
.output /data/flow_run_7d.csv
SELECT * FROM flow_run WHERE start_time >= datetime(\"now\",\"-7 day\");
.output /data/task_run_7d.csv
SELECT * FROM task_run WHERE start_time >= datetime(\"now\",\"-7 day\");
.output /data/flow_run_state_7d.csv
SELECT s.* FROM flow_run_state s JOIN flow_run r ON r.id=s.flow_run_id WHERE r.start_time >= datetime(\"now\",\"-7 day\");
.output /data/task_run_state_7d.csv
SELECT s.* FROM task_run_state s JOIN task_run r ON r.id=s.task_run_id WHERE r.start_time >= datetime(\"now\",\"-7 day\");
"
'
2) Import into Postgres with psql’s \copy (make sure columns match your schema; we can list explicit columns if needed)
docker exec -it qfin-postgres-1 bash -lc "
psql -U prefect -d prefect -c \"SET session_replication_role = replica;\"
psql -U prefect -d prefect -c \"\\copy flow_run FROM '/var/lib/postgresql/data/../flow_run_7d.csv' CSV HEADER\";
psql -U prefect -d prefect -c \"\\copy task_run FROM '/var/lib/postgresql/data/../task_run_7d.csv' CSV HEADER\";
psql -U prefect -d prefect -c \"\\copy flow_run_state FROM '/var/lib/postgresql/data/../flow_run_state_7d.csv' CSV HEADER\";
psql -U prefect -d prefect -c \"\\copy task_run_state FROM '/var/lib/postgresql/data/../task_run_state_7d.csv' CSV HEADER\";
psql -U prefect -d prefect -c \"SET session_replication_role = DEFAULT;\"
"Marvin
11/20/2025, 12:19 AMbrrr_prefect_data volume into the Postgres container as another mount (e.g., at /imports) and then use /imports/flow_run_7d.csv in the \copy commands. If you want, I’ll write the exact docker-compose snippet for that.
If you prefer to keep trying pgloader for full runs
- Try excluding logs and events first and upping memory:
- Add to EXCLUDING: log and events
- Run with --memory=6g --memory-swap=6g
- You can also exclude the biggest run tables first (flow_run, task_run, run states) to finish metadata now, then decide on run history later.
Quick sanity checks once you choose a path
docker exec -it qfin-postgres-1 psql -U prefect -d prefect -c "SELECT COUNT(*) FROM deployment;"
docker exec -it qfin-postgres-1 psql -U prefect -d prefect -c "SELECT COUNT(*) FROM flow;"
docker exec -it qfin-postgres-1 psql -U prefect -d prefect -c "SELECT COUNT(*) FROM work_pool;"
If you go with the CSV run-slice, also check counts for flow_run/task_run and states.
Finally, after you’re live on Postgres
- Consider removing the old prefect_data mount from the server (after you archive prefect.db) to avoid confusion.
- Keep your event settings to keep the UI snappy:
- PREFECT_SERVER_EVENTS_RETENTION_PERIOD=P2D
- PREFECT_SERVER_EVENTS_MAXIMUM_WEBSOCKET_BACKFILL=PT300S
- PREFECT_SERVER_EVENTS_WEBSOCKET_BACKFILL_PAGE_SIZE=100
- PREFECT_SERVER_EVENTS_RELATED_RESOURCE_CACHE_TTL=PT300S
Tell me which route you want:
- metadata-only now, or
- metadata + last N days of runs (and how many days), or
- keep pushing full pgloader with larger memory and excluding heavy tables.
I’ll give you the exact commands/snippets for your choice.Olve Heitmann
11/20/2025, 12:21 AMMarvin
11/20/2025, 12:21 AMMarvin
11/20/2025, 12:21 AMdocker exec -i qfin-postgres-1 psql -U prefect -d prefect -c "SELECT now(), relid::regclass, bytes_processed, bytes_total, tuples_processed, tuples_total, elapsed_time FROM pg_stat_progress_copy;"
- Active queries:
docker exec -i qfin-postgres-1 psql -U prefect -d prefect -c "SELECT pid, state, query, wait_event_type, wait_event FROM pg_stat_activity WHERE state <> 'idle';"
Second: stop fighting pgloader on heavy tables — migrate metadata only
Given the SBCL heap crashes and stalls, the fastest reliable path is to bring over just the Prefect metadata (deployments, blocks, pools/queues, variables, etc.) and skip runs/logs/events for now. This gets you live on Postgres with the UI snappy and avoids the biggest tables. You can always backfill a recent slice later.
Use this pgloader.load:
LOAD DATABASE
FROM sqlite:///root/.prefect/prefect.db
INTO <postgresql://prefect:prefect@postgres:5432/prefect>
WITH data only,
truncate,
batch rows = 200,
prefetch rows = 200,
on error stop
-- Only copy small metadata tables needed to keep working
INCLUDING ONLY TABLE NAMES MATCHING
'block_type', 'block_schema', 'block_schema_reference',
'block_document', 'block_document_reference',
'flow', 'deployment', 'deployment_version',
'work_pool', 'work_pool_queue', 'work_queue',
'worker', 'variable', 'concurrency_limit'
EXCLUDING TABLE NAMES LIKE 'sqlite_%'
EXCLUDING TABLE NAMES LIKE 'alembic_version'
;
Run it with more memory to avoid SBCL heap issues:
docker run --rm --network qfin_default \
--memory=6g --memory-swap=6g \
-v brrr_prefect_data:/root/.prefect \
-v $(pwd)/pgloader.load:/load.load \
dimitri/pgloader:latest pgloader /load.load
Then:
- Start Prefect against Postgres (keep your event settings) and start your workers
- Your deployments, blocks, pools/queues, variables, and concurrency limits will be there
- Historical runs/logs/events will start fresh (good for performance)
If you want some run history brought over
We can import just the last N days of runs/states via CSV without stressing pgloader. Tell me “N” (e.g., 3 or 7 days) and I’ll share a ready-to-run export/import snippet that mounts your SQLite volume for export and a path into Postgres for fast \copy.
Small compose cleanup (volume warning)
To silence the “created for project brrr” warning:
volumes:
prefect_data:
external: true
name: brrr_prefect_data
postgres_data:
And double-check your server env (no angle brackets):
PREFECT_API_DATABASE_CONNECTION_URL: <postgresql+asyncpg://prefect:prefect@postgres:5432/prefect>
PREFECT_API_DATABASE_MIGRATE_ON_START: "false" # you ran upgrade manually already
PREFECT_SERVER_EVENTS_RETENTION_PERIOD: "P2D"
PREFECT_SERVER_EVENTS_MAXIMUM_WEBSOCKET_BACKFILL: "PT300S"
PREFECT_SERVER_EVENTS_WEBSOCKET_BACKFILL_PAGE_SIZE: "100"
PREFECT_SERVER_EVENTS_RELATED_RESOURCE_CACHE_TTL: "PT300S"
References
- Server CLI (database commands)
- Settings reference — server events
- Settings reference — database
If you prefer a different set of tables (e.g., include artifacts/automations), tell me and I’ll tailor the load file precisely.Olve Heitmann
11/20/2025, 12:27 AMMarvin
11/20/2025, 12:27 AMMarvin
11/20/2025, 12:28 AMLOAD DATABASE
FROM sqlite:///root/.prefect/prefect.db
INTO <postgresql://prefect:prefect@postgres:5432/prefect>
WITH data only,
batch rows = 200,
prefetch rows = 200,
on error stop
INCLUDING ONLY TABLE NAMES LIKE 'block_type',
LIKE 'block_schema',
LIKE 'block_schema_reference',
LIKE 'block_document',
LIKE 'block_document_reference',
LIKE 'flow',
LIKE 'flow_tag',
LIKE 'deployment',
LIKE 'deployment_version',
LIKE 'work_pool',
LIKE 'work_pool_queue',
LIKE 'work_queue',
LIKE 'worker',
LIKE 'variable',
LIKE 'concurrency_limit'
EXCLUDING TABLE NAMES LIKE 'sqlite_%',
LIKE 'alembic_version'
-- Ensure a clean slate for the tables we are loading
BEFORE LOAD DO $$ TRUNCATE TABLE block_document_reference CASCADE; $$
BEFORE LOAD DO $$ TRUNCATE TABLE block_document CASCADE; $$
BEFORE LOAD DO $$ TRUNCATE TABLE block_schema_reference CASCADE; $$
BEFORE LOAD DO $$ TRUNCATE TABLE block_schema CASCADE; $$
BEFORE LOAD DO $$ TRUNCATE TABLE block_type CASCADE; $$
BEFORE LOAD DO $$ TRUNCATE TABLE flow_tag CASCADE; $$
BEFORE LOAD DO $$ TRUNCATE TABLE deployment_version CASCADE; $$
BEFORE LOAD DO $$ TRUNCATE TABLE deployment CASCADE; $$
BEFORE LOAD DO $$ TRUNCATE TABLE flow CASCADE; $$
BEFORE LOAD DO $$ TRUNCATE TABLE work_queue CASCADE; $$
BEFORE LOAD DO $$ TRUNCATE TABLE work_pool_queue CASCADE; $$
BEFORE LOAD DO $$ TRUNCATE TABLE work_pool CASCADE; $$
BEFORE LOAD DO $$ TRUNCATE TABLE worker CASCADE; $$
BEFORE LOAD DO $$ TRUNCATE TABLE variable CASCADE; $$
BEFORE LOAD DO $$ TRUNCATE TABLE concurrency_limit CASCADE; $$
;
Run it (give pgloader more memory to avoid SBCL heap issues):
docker run --rm --network qfin_default \
--memory=6g --memory-swap=6g \
-v brrr_prefect_data:/root/.prefect \
-v $(pwd)/pgloader.load:/load.load \
dimitri/pgloader:latest pgloader /load.load
Do I need to “start fresh”?
- Not the whole DB. The BEFORE LOAD DO TRUNCATE statements clear just the target tables (with CASCADE) so this run is clean and duplicate‑free.
- We deliberately exclude alembic_version, so your migration state stays correct.
Sanity checks after load
docker exec -i qfin-postgres-1 psql -U prefect -d prefect -c "SELECT COUNT(*) FROM deployment;"
docker exec -i qfin-postgres-1 psql -U prefect -d prefect -c "SELECT COUNT(*) FROM flow;"
docker exec -i qfin-postgres-1 psql -U prefect -d prefect -c "SELECT COUNT(*) FROM work_pool;"
docker exec -i qfin-postgres-1 psql -U prefect -d prefect -c "SELECT COUNT(*) FROM block_document;"
Then bring Prefect up against Postgres (keep your event settings), and start workers. Your deployments/blocks/pools/queues/variables will be there; runs/logs/events will start fresh, which should materially improve UI responsiveness and memory use.
If you want some recent history later (e.g., last 3–7 days of runs), I can share a CSV-based export/import that targets just those tables without stressing pgloader.
Useful refsMarvin
11/20/2025, 12:28 AMOlve Heitmann
11/20/2025, 12:30 AMMarvin
11/20/2025, 12:30 AMMarvin
11/20/2025, 12:30 AMLOAD DATABASE
FROM sqlite:///root/.prefect/prefect.db
INTO <postgresql://prefect:prefect@postgres:5432/prefect>
WITH data only,
truncate,
batch rows = 200,
prefetch rows = 200,
on error stop
INCLUDING ONLY TABLE NAMES LIKE 'block_%'
INCLUDING ONLY TABLE NAMES LIKE 'deployment%'
INCLUDING ONLY TABLE NAMES LIKE 'work_pool%'
INCLUDING ONLY TABLE NAMES LIKE 'work_queue'
INCLUDING ONLY TABLE NAMES LIKE 'flow'
INCLUDING ONLY TABLE NAMES LIKE 'flow_tag'
INCLUDING ONLY TABLE NAMES LIKE 'variable'
INCLUDING ONLY TABLE NAMES LIKE 'concurrency_limit'
INCLUDING ONLY TABLE NAMES LIKE 'worker'
EXCLUDING TABLE NAMES LIKE 'sqlite_%'
EXCLUDING TABLE NAMES LIKE 'alembic_version'
;
Run it (give pgloader more memory to avoid SBCL heap issues):
docker run --rm --network qfin_default \
--memory=6g --memory-swap=6g \
-v brrr_prefect_data:/root/.prefect \
-v $(pwd)/pgloader.load:/load.load \
dimitri/pgloader:latest pgloader /load.load
Notes
- WITH truncate makes pgloader empty the included target tables for you — you don’t need to reset/drop the whole DB to “start fresh”.
- If you still see heap issues, drop batch/prefetch to 100/100 or bump memory higher.
- The earlier “stuck” messages are often pgloader busy rather than hung; you can check Postgres activity with:
- Progress: docker exec -i qfin-postgres-1 psql -U prefect -d prefect -c "SELECT now(), relid::regclass, bytes_processed, bytes_total, tuples_processed, tuples_total, elapsed_time FROM pg_stat_progress_copy;"
- Active queries: docker exec -i qfin-postgres-1 psql -U prefect -d prefect -c "SELECT pid, state, query, wait_event_type, wait_event FROM pg_stat_activity WHERE state <> 'idle';"
Sanity checks after load
docker exec -i qfin-postgres-1 psql -U prefect -d prefect -c "SELECT COUNT(*) FROM deployment;"
docker exec -i qfin-postgres-1 psql -U prefect -d prefect -c "SELECT COUNT(*) FROM flow;"
docker exec -i qfin-postgres-1 psql -U prefect -d prefect -c "SELECT COUNT(*) FROM work_pool;"
docker exec -i qfin-postgres-1 psql -U prefect -d prefect -c "SELECT COUNT(*) FROM block_document;"
Then bring Prefect up against Postgres (keep your event settings) and start the workers. You’ll retain deployments, blocks, pools/queues, variables, etc., and start fresh on runs/logs/events — which should immediately improve UI performance and memory usage.
If you later want a small window of run history (e.g., last 3–7 days), I can give you a CSV export/import that targets just flow_run/task_run and their states without stressing pgloader. Tell me how many days you’d like and I’ll tailor the commands.Olve Heitmann
11/20/2025, 12:32 AMMarvin
11/20/2025, 12:33 AMMarvin
11/20/2025, 12:33 AMdocker exec -i qfin-postgres-1 psql -U prefect -d prefect -c "SELECT version();"
- Version-agnostic copy progress:
docker exec -i qfin-postgres-1 psql -U prefect -d prefect -c "
SELECT now(),
relid::regclass,
bytes_processed,
bytes_total,
tuples_processed
FROM pg_stat_progress_copy;"
If you see rows here, the copy is running. If you see nothing, pgloader is not actively copying at that moment.
- Active queries (simplified):
docker exec -i qfin-postgres-1 psql -U prefect -d prefect -c "
SELECT pid, state, wait_event_type, wait_event,
left(query, 120) AS query
FROM pg_stat_activity
WHERE datname = 'prefect' AND state <> 'idle'
ORDER BY query_start DESC
LIMIT 10;"
What to look for:
- COPY statements like COPY public.deployment FROM STDIN → data is moving
- If nothing shows for minutes while pgloader “warns”, it’s likely stalled (often memory/transform overhead)
On “start fresh”
- You do NOT need to drop the whole DB again. Using pgloader with:
- WITH truncate or
- explicit BEFORE LOAD DO TRUNCATE statements
…will clear just the target tables before copying, which is enough to avoid duplicates.
If pgloader still stalls
- The most reliable unblock is the “metadata-only” import (no runs/logs/events). Use the corrected load file syntax that pgloader accepts (one INCLUDING per line, using LIKE) and let pgloader truncate those targets:
LOAD DATABASE
FROM sqlite:///root/.prefect/prefect.db
INTO <postgresql://prefect:prefect@postgres:5432/prefect>
WITH data only,
truncate,
batch rows = 200,
prefetch rows = 200,
on error stop
INCLUDING ONLY TABLE NAMES LIKE 'block_%'
INCLUDING ONLY TABLE NAMES LIKE 'deployment%'
INCLUDING ONLY TABLE NAMES LIKE 'work_pool%'
INCLUDING ONLY TABLE NAMES LIKE 'work_queue'
INCLUDING ONLY TABLE NAMES LIKE 'flow'
INCLUDING ONLY TABLE NAMES LIKE 'flow_tag'
INCLUDING ONLY TABLE NAMES LIKE 'variable'
INCLUDING ONLY TABLE NAMES LIKE 'concurrency_limit'
INCLUDING ONLY TABLE NAMES LIKE 'worker'
EXCLUDING TABLE NAMES LIKE 'sqlite_%'
EXCLUDING TABLE NAMES LIKE 'alembic_version'
;
Run with extra memory to prevent SBCL heap issues:
docker run --rm --network qfin_default \
--memory=6g --memory-swap=6g \
-v brrr_prefect_data:/root/.prefect \
-v $(pwd)/pgloader.load:/load.load \
dimitri/pgloader:latest pgloader /load.load
Then sanity-check the counts:
docker exec -i qfin-postgres-1 psql -U prefect -d prefect -c "
SELECT relname, n_live_tup
FROM pg_stat_user_tables
WHERE relname IN ('deployment','deployment_version','flow','flow_tag','block_document','block_document_reference','block_schema','block_schema_reference','block_type','work_pool','work_pool_queue','work_queue','worker','variable','concurrency_limit')
ORDER BY relname;"
If those look reasonable, start Prefect against Postgres and bring your workers up. You’ll have deployments/blocks/pools/queues/variables ready to go; runs/logs/events will start fresh (which should help the UI and memory immediately).
If you’d like recent run history too, tell me how many days (e.g., last 3/7 days) and I’ll give you a CSV-based export/import for just flow/task runs and their states without stressing pgloader.
References:
- Self-hosted Server CLI
- Settings reference — database
- Settings reference — server eventsOlve Heitmann
11/20/2025, 12:36 AMMarvin
11/20/2025, 12:36 AMMarvin
11/20/2025, 12:37 AMdocker ps | grep pgloader
docker kill <container_id>
2) Kill stuck Postgres backends
- List active queries
docker exec -i qfin-postgres-1 psql -U prefect -d prefect -c "
SELECT pid, usename, application_name, state, wait_event_type, wait_event,
left(query, 160) AS query
FROM pg_stat_activity
WHERE datname = 'prefect' AND state <> 'idle'
ORDER BY query_start;"
"
- Cancel or terminate them (cancel first; if they don’t stop in ~10s, terminate)
-- cancel
docker exec -i qfin-postgres-1 psql -U prefect -d prefect -c "SELECT pg_cancel_backend(pid) FROM pg_stat_activity WHERE datname='prefect' AND state <> 'idle';"
-- terminate
docker exec -i qfin-postgres-1 psql -U prefect -d prefect -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='prefect' AND state <> 'idle';"
- If anything refuses to die, just restart Postgres:
docker restart qfin-postgres-1
3) Ensure heavy/partially loaded tables are empty
You do NOT need to reset the whole DB. Truncate only the heavy run/log/event tables so we start fresh on those and avoid conflicts:
docker exec -i qfin-postgres-1 psql -U prefect -d prefect -c "
TRUNCATE TABLE
events,
log,
flow_run_state,
task_run_state,
task_run,
flow_run
CASCADE;"
4) Run a metadata-only pgloader job (no runs/logs/events)
Use this load file (valid syntax). It includes deployments/blocks/pools/queues/variables and excludes runs/logs/events:
LOAD DATABASE
FROM sqlite:///root/.prefect/prefect.db
INTO <postgresql://prefect:prefect@postgres:5432/prefect>
WITH data only,
truncate,
batch rows = 200,
prefetch rows = 200,
on error stop
INCLUDING ONLY TABLE NAMES LIKE 'block_%'
INCLUDING ONLY TABLE NAMES LIKE 'deployment%'
INCLUDING ONLY TABLE NAMES LIKE 'work_pool%'
INCLUDING ONLY TABLE NAMES LIKE 'work_queue'
INCLUDING ONLY TABLE NAMES LIKE 'flow'
INCLUDING ONLY TABLE NAMES LIKE 'flow_tag'
INCLUDING ONLY TABLE NAMES LIKE 'variable'
INCLUDING ONLY TABLE NAMES LIKE 'concurrency_limit'
INCLUDING ONLY TABLE NAMES LIKE 'worker'
EXCLUDING TABLE NAMES LIKE 'sqlite_%'
EXCLUDING TABLE NAMES LIKE 'alembic_version'
;
Run it with extra memory so SBCL doesn’t choke:
docker run --rm --network qfin_default \
--memory=6g --memory-swap=6g \
-v brrr_prefect_data:/root/.prefect \
-v $(pwd)/pgloader.load:/load.load \
dimitri/pgloader:latest pgloader /load.load
5) Sanity-check and bring Prefect up
- Quick counts:
docker exec -i qfin-postgres-1 psql -U prefect -d prefect -c "
SELECT relname, n_live_tup
FROM pg_stat_user_tables
WHERE relname IN ('deployment','deployment_version','flow','flow_tag',
'block_type','block_schema','block_schema_reference',
'block_document','block_document_reference',
'work_pool','work_pool_queue','work_queue','worker',
'variable','concurrency_limit')
ORDER BY relname;"
"
- Start the server and workers (ensure no angle brackets in the URL and keep your event settings).
Notes
- Seeing COPY events in activity means an old pgloader was still copying; killing those backends (or restarting Postgres) clears it.
- The JSON vs JSONB warnings are harmless.
- If you later want a slice of recent run history, I can give you CSV export/import steps for, say, last 3–7 days of flow/task runs + states without stressing pgloader.
Want me to generate a CSV-based import for the last N days of runs? If yes, tell me N and I’ll paste exact commands.Olve Heitmann
11/20/2025, 12:43 AMMarvin
11/20/2025, 12:43 AMMarvin
11/20/2025, 12:45 AMLOAD DATABASE
FROM sqlite:///root/.prefect/prefect.db
INTO <postgresql://prefect:prefect@postgres:5432/prefect>
WITH data only,
truncate,
batch rows = 200,
prefetch rows = 200,
on error stop
INCLUDING ONLY TABLE NAMES LIKE 'flow'
INCLUDING ONLY TABLE NAMES LIKE 'deployment'
INCLUDING ONLY TABLE NAMES LIKE 'deployment_version'
INCLUDING ONLY TABLE NAMES LIKE 'deployment_schedule'
INCLUDING ONLY TABLE NAMES LIKE 'work_pool%'
INCLUDING ONLY TABLE NAMES LIKE 'work_queue'
EXCLUDING TABLE NAMES LIKE 'sqlite_%'
EXCLUDING TABLE NAMES LIKE 'alembic_version'
;
- Run it with extra memory:
docker run --rm --network qfin_default \
--memory=6g --memory-swap=6g \
-v brrr_prefect_data:/root/.prefect \
-v $(pwd)/pgloader.load:/load.load \
dimitri/pgloader:latest pgloader /load.load
- Sanity check:
docker exec -i qfin-postgres-1 psql -U prefect -d prefect -c "SELECT COUNT(*) FROM deployment;"
docker exec -i qfin-postgres-1 psql -U prefect -d prefect -c "SELECT COUNT(*) FROM deployment_schedule;"
- In the UI or with CLI:
prefect deployments ls
If the run still doesn’t copy deployments:
- It may be foreign key ordering (e.g., missing flows). The above includes flow and work_pool/work_queue to satisfy FKs. If you have infrastructure/storage block documents attached to deployments, we should include block tables too:
Add:
INCLUDING ONLY TABLE NAMES LIKE 'block_%'
and re-run.
2) How to restart or manage the server and services
- Restart only the server container:
docker compose restart prefect-server
- Bring the server down/up:
docker compose down
docker compose up -d prefect-server
- Restart Postgres:
docker restart qfin-postgres-1
- Check server health:
curl -s <http://localhost:4200/api/health>
- Verify deployments via CLI (from any machine with PREFECT_API_URL pointing to your server):
prefect deployments ls
prefect deployments inspect "<flow-name>/<deployment-name>"
prefect deployments schedule list "<flow-name>/<deployment-name>"
Pro tip: ensure your CLI is pointing at your self-hosted server
- Set:
export PREFECT_API_URL=<http://localhost:4200/api>
- Then:
prefect deployments ls
If you’d like, paste the output of:
- SELECT COUNT(*) FROM flow;
- SELECT COUNT(*) FROM work_pool;
- SELECT COUNT(*) FROM work_queue;
- SELECT COUNT(*) FROM block_document;
- SELECT COUNT(*) FROM deployment;
- SELECT COUNT(*) FROM deployment_schedule;
I can adjust the pgloader include set to pull exactly what’s missing, then you can restart the server and your existing schedules should just pick back up.Olve Heitmann
11/20/2025, 12:53 AMMarvin
11/20/2025, 12:54 AMMarvin
11/20/2025, 12:55 AMdocker run --rm -v brrr_prefect_data:/data alpine sh -lc '
apk add --no-cache sqlite;
sqlite3 /data/prefect.db "SELECT COUNT(*) FROM deployment;"
sqlite3 /data/prefect.db "SELECT COUNT(*) FROM deployment_schedule;"
'
If both are > 0, we can copy just those tables.
Targeted pgloader for deployments (with FK-friendly settings)
1) Clear just the deployment tables in Postgres so we start clean:
docker exec -i qfin-postgres-1 psql -U prefect -d prefect -c "
TRUNCATE TABLE deployment_schedule, deployment_version, deployment CASCADE;"
2) Create pgloader.load with this content (syntax is finicky; one INCLUDING per line):
LOAD DATABASE
FROM sqlite:///root/.prefect/prefect.db
INTO <postgresql://prefect:prefect@postgres:5432/prefect>
WITH data only,
truncate,
batch rows = 200,
prefetch rows = 200,
on error stop
INCLUDING ONLY TABLE NAMES LIKE 'flow'
INCLUDING ONLY TABLE NAMES LIKE 'deployment'
INCLUDING ONLY TABLE NAMES LIKE 'deployment_version'
INCLUDING ONLY TABLE NAMES LIKE 'deployment_schedule'
INCLUDING ONLY TABLE NAMES LIKE 'work_pool%'
INCLUDING ONLY TABLE NAMES LIKE 'work_queue'
INCLUDING ONLY TABLE NAMES LIKE 'block_%'
EXCLUDING TABLE NAMES LIKE 'sqlite_%'
EXCLUDING TABLE NAMES LIKE 'alembic_version'
-- Temporarily relax constraints so FK ordering does not block the load
BEFORE LOAD DO $$ SET session_replication_role = 'replica'; $$
AFTER LOAD DO $$ SET session_replication_role = 'origin'; $$
;
3) Run it (give pgloader memory):
docker run --rm --network qfin_default \
--memory=6g --memory-swap=6g \
-v brrr_prefect_data:/root/.prefect \
-v $(pwd)/pgloader.load:/load.load \
dimitri/pgloader:latest pgloader /load.load
4) Sanity check:
docker exec -i qfin-postgres-1 psql -U prefect -d prefect -c "SELECT COUNT(*) FROM deployment;"
docker exec -i qfin-postgres-1 psql -U prefect -d prefect -c "SELECT COUNT(*) FROM deployment_schedule;"
Then from your CLI (with PREFECT_API_URL pointing at your server):
prefect deployments ls
If deployment counts in SQLite are > 0 but still 0 in Postgres after this run
- It’s likely a data shape mismatch that pgloader doesn’t like. In that case, the fastest path is to re-register deployments from source (since you already deploy from git). This preserves schedules too if they’re defined in your project or deploy code.
Two ways to re-register from source
- Prefect project(s):
- In each project directory that contains your .prefect config, run:
prefect deploy
- This will recreate deployments and schedules defined in your project configuration.
- From code using from_source + deploy:
- If you define deployments in code, re-run your deployment script; e.g.:
from prefect import flow
flow.from_source("<https://github.com/owner/repo>", entrypoint="path/to/file.py:my_flow").deploy(
name="my-deployment",
work_pool_name="production-pool",
schedules=[...],
parameters={...},
)
- Re-running this for each flow will recreate the deployments and their schedules in Postgres.
I can help generate one-off scripts if you can share how your deployments are defined (Prefect project vs in-code deploy calls). If you have a project repo, pointing me at where deployments are declared is enough for me to suggest exact commands.
Server management commands you asked for
- Restart server:
docker compose restart prefect-server
- Stop/start server:
docker compose down
docker compose up -d prefect-server
- Restart Postgres:
docker restart qfin-postgres-1
- Check health:
curl -s <http://localhost:4200/api/health>
- Ensure your CLI points to this server:
```Marvin
11/20/2025, 12:55 AMOlve Heitmann
11/20/2025, 1:06 AMOlve Heitmann
11/20/2025, 2:01 PMMarvin
11/20/2025, 2:01 PMMarvin
11/20/2025, 2:01 PMdocker logs --since=15m qfin-prefect-server-1
Paste the Python traceback you see for the GET /api/flow_runs/<id> request.
2) Confirm the run row looks sane in Postgres
- Replace the ID with the failing one:
docker exec -i qfin-postgres-1 psql -U prefect -d prefect -c "
SELECT id, state_type, start_time, end_time, total_run_time
FROM flow_run
WHERE id = '01cbe7ce-1463-4b21-b746-688e309933f9';"
- We’re checking that the row exists and total_run_time is an INTERVAL (not TEXT) and not some malformed value.
3) Verify server and worker are on the same Prefect version
- Ask the server:
curl -s <http://localhost:4200/api/version>
- Check your worker image tag. If you’re using 3-latest, it can drift. It’s best to pin both server and workers to the same exact version you see above (e.g., prefecthq/prefect:3.4.17 or whatever the server reports). Mismatches can surface as 500s due to schema/model diffs.
4) Sanity-check the API from the host
- Replace the id:
curl -i <http://localhost:4200/api/flow_runs/01cbe7ce-1463-4b21-b746-688e309933f9>
If this also returns 500, it’s definitely a server-side exception we’ll catch in step 1 logs.
Common culprits after migration
- A schema mismatch due to version drift (server vs worker), or an out-of-date server image pulled mid-process
- An extension or type issue (e.g., interval, jsonb) — the traceback will tell us which column
- A related resource missing (e.g., a deployment schedule or block doc referenced by the flow run) — again, the server logs will make this clear
Restart/management commands you asked for
- Restart the server:
docker compose restart prefect-server
- Stop and start the server:
docker compose down
docker compose up -d prefect-server
- Restart Postgres:
docker restart qfin-postgres-1
- Health:
curl -s <http://localhost:4200/api/health>
- CLI to list deployments:
export PREFECT_API_URL=<http://localhost:4200/api>
prefect deployments ls
Next step
- Please paste the server logs traceback for the 500, plus the results of the psql query on the flow_run row and the server’s /api/version. With that, I can give you a precise fix (e.g., pin versions, adjust a column type, or re-create a missing related record).Olve Heitmann
11/20/2025, 2:09 PMMarvin
11/20/2025, 2:09 PMMarvin
11/20/2025, 2:10 PMdocker exec -i qfin-postgres-1 psql -U prefect -d prefect -c "
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = 'flow_run' AND column_name IN ('total_run_time');"
docker exec -i qfin-postgres-1 psql -U prefect -d prefect -c "
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = 'task_run' AND column_name IN ('total_run_time');"
Expected: both should be interval.
If either shows text (or anything else), fix them:
docker exec -i qfin-postgres-1 psql -U prefect -d prefect -c "
ALTER TABLE flow_run
ALTER COLUMN total_run_time TYPE interval
USING NULLIF(total_run_time::text, '')::interval;"
docker exec -i qfin-postgres-1 psql -U prefect -d prefect -c "
ALTER TABLE task_run
ALTER COLUMN total_run_time TYPE interval
USING NULLIF(total_run_time::text, '')::interval;"
2) Ensure server and workers run the same Prefect 3 image tag
- You mentioned /api/version returns 0.8.4 — that’s the internal server package version and can be okay, but to avoid drift, pin both server and workers to the same Prefect 3 tag.
- In docker-compose, set the same tag everywhere (example):
image: prefecthq/prefect:3.4.17
- Recreate only the server and workers with the pinned tag:
docker compose up -d --force-recreate --no-deps prefect-server
docker compose up -d --force-recreate --no-deps prefect-prod-worker prefect-dev-worker
- From inside the server container, confirm:
docker exec -i qfin-prefect-server-1 prefect version
Confirm worker version similarly:
docker exec -i <worker-container-name> prefect version
3) Confirm you are using asyncpg driver
- Check the env in your server container:
docker exec -i qfin-prefect-server-1 printenv | grep PREFECT_API_DATABASE_CONNECTION_URL
It must be:
<postgresql+asyncpg://prefect:prefect@postgres:5432/prefect>
4) Restart server and retry the run
- Restart the server cleanly and test the problematic API endpoint:
docker compose restart prefect-server
curl -i <http://localhost:4200/api/flow_runs/01cbe7ce-1463-4b21-b746-688e309933f9>
- Try a small test deployment/flow run again. If you still get a 500, paste the top of the new traceback — it should shift if the interval fix worked.
5) Double-check flow run row integrity
- Sanity on the specific run:
docker exec -i qfin-postgres-1 psql -U prefect -d prefect -c "
SELECT id, state_type, start_time, end_time, total_run_time
FROM flow_run
WHERE id = '01cbe7ce-1463-4b21-b746-688e309933f9';"
- If the run exists and total_run_time is a proper interval (e.g., 000000), the server should serialize it to a timedelta without error.
6) If deployments were re-registered from source
- Make sure your CLI points to your server:
export PREFECT_API_URL=<http://localhost:4200/api>
prefect deployments ls
- Run one deployment:
prefect deployments run "<flow-name>/<deployment-name>"
Server/worker management commands (quick reference)
- Restart server:
docker compose restart prefect-server
- Down/up server:
docker compose down
docker compose up -d prefect-server
- Restart Postgres:
docker restart qfin-postgres-1
- Health:
curl -s <http://localhost:4200/api/health>
If the error persists after ensuring interval columns are correct and versions align, please paste:
- The output of the two information_schema queries above
- The top 30–40 lines of the new server traceback around read_flow_run
- The output of prefect version from server and worker containersMarvin
11/20/2025, 2:10 PM