Logfire Self-Hosted: FusionFire Database Migration Guide
This guide moves the ff PostgreSQL database of a self-hosted Logfire deployment to a new PostgreSQL instance. The object storage bucket stays where it is.
The migration consists of three steps:
- Stop the FusionFire workloads that use the
ffdatabase. - Copy the database to the new instance, with one of these methods:
- Dump and restore. This method is the simplest. Queries are unavailable for as long as the dump and restore take.
- Promote a replica. Queries are unavailable for a few minutes. This method needs a physical replica of the old instance, for example a read replica from your PostgreSQL provider.
- Deploy the Helm chart with the new
postgresFFDsnand start the workloads again.
logfire-ff-ingest keeps running through all steps. It does not connect to PostgreSQL. It accepts telemetry and buffers it on disk until the other workloads start again. By default, it keeps retrying a batch for only 10 minutes, so Step 1 raises that limit for the length of the migration.
- Admin access to your Kubernetes cluster.
- The Helm CLI installed.
pg_dump,pg_restore, andpsqlwith the same major version as the new instance.- A new PostgreSQL instance that meets the PostgreSQL requirements. For dump and restore, it must run the same or a newer PostgreSQL major version than the old instance. A physical replica must run the same major version.
- An empty
ffdatabase on the new instance, owned by the user in the newpostgresFFDsn. That user must be able to create theintarrayandbtree_gistextensions. - Enough disk on the
logfire-ff-ingestvolumes to buffer incoming data for the length of the migration. - An estimate of how long the migration takes. Use a value that is longer than that estimate for
FF_INGEST_REMOTE_RETRY_MAX_DURATIONin Step 1.
Use the same chart version and the same values before and after the migration, except for postgresFFDsn. Do not upgrade the chart in the same window. Record the installed chart version, and pass it to every helm upgrade in this guide:
CHART_VERSION=$(helm list -n logfire -f '^logfire$' -o json | jq -r '.[0].chart | sub("^logfire-"; "")')
The examples below assume the Helm release is named logfire and installed in the logfire namespace. Adjust the commands if your deployment uses different names.
Pause the autoscalers of these workloads first. Every sizingPreset turns on HPA for them, and your values can also turn on HPA or KEDA. Otherwise, Kubernetes scales the workloads back up during the migration.
Also raise how long logfire-ff-ingest retries a batch. After FF_INGEST_REMOTE_RETRY_MAX_DURATION (10 minutes by default), ingest writes the batch to the _ingest_failover prefix of the bucket, and queries do not return that data. If your values already set logfire-ff-ingest.env, copy the other entries into this file too, because Helm replaces the whole list. Leave out any existing FF_INGEST_REMOTE_RETRY_MAX_DURATION entry.
Create a temporary migration-pause-ff.yaml file:
logfire-ff-ingest:
env:
- name: FF_INGEST_REMOTE_RETRY_MAX_DURATION
value: "6h"
logfire-ff-ingest-processor:
autoscaling:
hpa:
enabled: false
keda:
enabled: false
logfire-ff-compaction-worker:
autoscaling:
hpa:
enabled: false
keda:
enabled: false
logfire-ff-maintenance-worker:
autoscaling:
hpa:
enabled: false
keda:
enabled: false
logfire-ff-maintenance-scheduler:
autoscaling:
hpa:
enabled: false
keda:
enabled: false
logfire-ff-crud-api:
autoscaling:
hpa:
enabled: false
keda:
enabled: false
logfire-ff-query-api:
autoscaling:
hpa:
enabled: false
keda:
enabled: false
logfire-ff-query-worker:
autoscaling:
hpa:
enabled: false
keda:
enabled: false
Apply it with your current production values:
helm upgrade logfire pydantic/logfire \
--version "$CHART_VERSION" \
-f values.yaml \
-f migration-pause-ff.yaml \
--namespace logfire
Wait until every logfire-ff-ingest pod runs with the new retry limit. A pod that still has the 10-minute limit loses data once the workloads stop:
kubectl -n logfire rollout status statefulset/logfire-ff-ingest
Then scale the workloads down:
kubectl -n logfire scale deployment --replicas=0 \
-l 'app.kubernetes.io/component in (logfire-ff-ingest-processor,logfire-ff-compaction-worker,logfire-ff-maintenance-worker,logfire-ff-maintenance-scheduler,logfire-ff-crud-api,logfire-ff-query-api,logfire-ff-query-worker)'
Make sure that no client is still connected to the old ff database:
SELECT application_name, usename, client_addr, state
FROM pg_stat_activity
WHERE datname = 'ff' AND pid <> pg_backend_pid();
The query must return no rows before you continue. If it returns rows, find and stop the clients that it shows.
Use one of the two methods below.
Dump the old database. The directory format lets pg_dump and pg_restore use parallel jobs:
pg_dump \
--dbname="$OLD_FF_DSN" \
--format=directory \
--jobs=4 \
--no-owner \
--no-privileges \
--file=ff-dump
Restore it into the empty ff database on the new instance. Connect as the user in the new postgresFFDsn, so that this user owns the restored tables:
pg_restore \
--dbname="$NEW_FF_DSN" \
--jobs=4 \
--no-owner \
--no-privileges \
--exit-on-error \
ff-dump
The restore does not copy planner statistics. Collect them before Logfire uses the new database:
psql "$NEW_FF_DSN" -c 'ANALYZE;'
Create a physical replica of the old instance before Step 1, while Logfire is still running. Most managed PostgreSQL services call this a read replica. A physical replica copies every change, including the schema migration history, so the promoted instance is an exact copy.
This guide does not cover logical replication. Logical replication does not copy sequence values or schema changes.
After Step 1 is complete:
- Wait until the replica has replayed every change from the old instance. Your provider shows this as a replication lag of zero. On a self-managed replica, run
SELECT pg_current_wal_lsn();on the old instance after Step 1. Then wait untilSELECT pg_wal_lsn_diff(pg_last_wal_replay_lsn(), '<LSN>') >= 0;returnstrueon the replica. - Promote the replica to a standalone instance, with your provider’s promote action or with
pg_promote(). - Make sure that the user in the new
postgresFFDsncan sign in to the promoted instance and owns theffdatabase.
Update postgresFFDsn in your values, or the postgresFFDsn key of the Secret that postgresSecret.name references:
postgresFFDsn: postgresql://logfire_ff:PASSWORD@new-postgres.example.com:5432/ff
Deploy the updated Helm chart without migration-pause-ff.yaml, so Helm restores the autoscaling, replica, and ingest retry settings from your production values:
helm upgrade logfire pydantic/logfire \
--version "$CHART_VERSION" \
-f values.yaml \
--namespace logfire
The FusionFire migration job runs against the new database first. The chart version did not change, so the job has no migration to apply.
The chart restarts every pod that receives postgresFFDsn. That includes logfire-ff-ingest, which receives the value but does not use it. Its buffer is on a persistent volume, so the restart does not lose buffered data. If an external tool manages the Secret and the pods did not restart, restart the FusionFire deployments and the logfire-ff-ingest StatefulSet with kubectl rollout restart.
If the Helm upgrade does not restore the workloads, scale them back up manually to your previous replica counts.
-
Check pod status. All pods must be running without restarts:
Terminal kubectl -n logfire get pods -
Check the database connections. Run the
pg_stat_activityquery from Step 1 against both instances. The new instance shows FusionFire connections. The old instance shows none. -
Check the writer logs for database errors:
Terminal kubectl -n logfire logs -l app.kubernetes.io/component=logfire-ff-ingest-processor --tail=50 -
Query data in the Logfire UI. Query recent data, data from before the migration, and data sent during the migration window.
Keep the old database until you have finished the checks. Do not point Logfire at it again.