Skip to content

Back up and restore

pfCentral’s identity lives in two places that must match: PostgreSQL and the state volume. Back them up together, from the same moment. With both, you can restore the server and every firewall reconnects on its own. With only one, you start over.

ItemWhereWhy
PostgreSQL databaseYour databaseUsers, roles, devices, hashed secrets, NATS account records, sessions, audit events
State volume/data/state in the containerNATS operator and account keys, internal credentials, the NATS CA, JetStream data
Token pepperPFCENTRAL_TOKEN_PEPPER_*Without it no stored secret can be checked
Email encryption keyPFCENTRAL_EMAIL_ENCRYPTION_KEYWithout it queued emails cannot be sent
Your deployment filescompose.yaml, manifests, proxy configTo rebuild the same setup

The firewalls’ own configurations are not part of this server backup. As an admin or owner, use General Setup → Backup Config to download the complete saved active configuration from each online firewall. Protect the XML: it can contain passwords and private keys. Restore firewall configurations in pfSense (Diagnostics → Backup & Restore); pfCentral does not implement restore.

LostResult
State volume, database keptThe server refuses to start (desynchronised state). Recovery means a fresh install: new owner, and every firewall added and enrolled again.
Database, state volume keptThe server refuses to start for the same reason. Same recovery.
Only state/nats/js/ (JetStream)Harmless. Recent telemetry and caches are rebuilt.
Only state/nats/tls/The server makes a new CA on start. Every agent must download it again (Save in Services → pfCentral → Settings).

The keys in the state volume do not change after the first start, so a short pause is enough for a consistent copy.

  1. Dump the database.

    Terminal window
    docker compose exec -T postgres pg_dump -U pf_central -Fc pf_central > pf_central-$(date +%F).dump
  2. Archive the state volume. The image has no shell, so copy it with a helper container:

    Terminal window
    docker run --rm -v pf-central_state:/data:ro -v "$PWD":/backup alpine \
    tar czf /backup/pf_central-state-$(date +%F).tgz -C /data state --exclude=state/nats/js

    Replace pf-central_state with your volume name (docker volume ls). Excluding js/ keeps the archive small; it only holds telemetry.

  3. Encrypt and store both files together, with the date in the name, away from the server. They contain private keys.

Run this daily, and again before every upgrade.

  1. Stop pfCentral: docker compose stop pf-central.

  2. Restore the database into an empty database:

    Terminal window
    docker compose exec -T postgres pg_restore -U pf_central -d pf_central --clean --if-exists < pf_central-2026-10-17.dump
  3. Restore the state volume:

    Terminal window
    docker run --rm -v pf-central_state:/data -v "$PWD":/backup alpine \
    sh -c 'rm -rf /data/state && tar xzf /backup/pf_central-state-2026-10-17.tgz -C /data && chown -R 65532:65532 /data/state'
  4. Set the same token pepper and email key as before, and start: docker compose up -d pf-central.

  5. Check the log for server listening with no desynchronised error. Firewalls reconnect within a minute.

Devices enrolled after the backup are not in it. Enroll them again.

Restoring onto a new host is the same procedure. Keep the same DNS name, so agents find the new server without changes. If the name must change, every firewall needs a new NATS URL, and with TLS the new name must be in PFCENTRAL_NATS_TLS_HOSTS.

If you have no matching pair, stop the server, delete the state volume and the database, start again, create the owner, and re-enroll every firewall. On each firewall, paste a new token in Services → pfCentral → Settings and Save.