Backups and restore
Everything but media lives in one database: the plan, texts, approvals, accounts, connections and settings. Back that up, keep the media where it already is, and keep your secrets somewhere safe. The database side in detail is in Databases.
What a backup holds, and what it doesn't
- Holds: the plan and its texts, approvals, posted records, accounts, connections (the credentials encrypted), settings, keys and passkeys.
- Doesn't hold the secrets. Credentials in the database are encrypted with
ENCRYPTION_KEY: a restore without the sameENCRYPTION_KEY(andMEDIA_SIGNING_KEY, the VAPID pair) can't read them. Keep the values fromwrangler secret putor your.envin a password manager (Configuration). - Doesn't hold the media. See below.
- Passkeys belong to a hostname: a restored instance on the same
PUBLIC_URLandRP_IDkeeps working; on a new hostname, add the devices again.
Cloudflare (D1)
From apps/cloudflare:
pnpm exec wrangler d1 export bongsu --remote --output backup.sql
That is SQL for SQLite. To restore, create an empty D1 database, load the dump into it, and point both wrangler files at it:
pnpm exec wrangler d1 execute <database> --remote --file=backup.sql
To move to Docker instead, make a SQLite file with the Node build (pnpm --filter @bongsu/node build), from apps/node:
DATABASE_PATH=bongsu.db node dist/server.mjs load-sql backup.sql
load-sql only makes a new database: it refuses a file that exists. Put the file into the volume as the node user
(see the Docker restore below, with your file in place of the backup).
Docker (SQLite)
The database is one file in the bongsu-data volume. A consistent copy, safe while Bongsu runs:
docker compose exec bongsu node server.mjs backup /data/backup-$(date +%F).db
docker compose cp bongsu:/data/backup-$(date +%F).db .
To restore, with the copy in the current directory (the container runs as node, so copy inside it to keep the
ownership; the $PWD mount is read-only):
docker compose stop bongsu
docker compose run --rm -v "$PWD":/restore:ro bongsu sh -c \
'cp /restore/backup-2026-10-31.db /data/bongsu.db && rm -f /data/bongsu.db-wal /data/bongsu.db-shm'
docker compose up -d
(Use --profile https on up if you started with it.) Without Docker, the same commands run as
node dist/server.mjs backup <file>, export <file> and import <file> from apps/node, with DATABASE_PATH set.
Postgres
docker compose exec -T postgres pg_dump -U bongsu bongsu > backup.sql
Restore the dump into an empty database, then start Bongsu (it migrates at start):
docker compose --profile postgres up -d postgres
docker compose exec -T postgres psql -U bongsu bongsu < backup.sql
docker compose --profile postgres up -d
A copy that moves between databases
node server.mjs export <file> writes every table as JSON, the same on every database, and import <file> loads it
into a fresh one (it refuses a database with data in it, and an export made by another version: update both sides
first):
docker compose exec bongsu node server.mjs export /data/bongsu-export.json
The file holds the encrypted credentials: treat it like a backup. The moves from SQLite or D1 to Postgres are written out in Databases.
The plan as a file
bongsu export --format csv --texts > plan.csv writes every plan item. It is for reading and scripts, not a backup:
it holds no approvals, accounts or connections (The command line).
Media
The media stores keep the files; the database only references them (which stores are set up, with their keys and
grants, is in the database backup, encrypted with ENCRYPTION_KEY).
- Drive: the files are already Drive's own copy; Bongsu only reads them and moves posted ones to
UPLOADS/_posted/(Google Drive). - A bucket: the files are the bucket's; turn on its versioning or replication if you want copies (S3-compatible buckets).
- This server's disk (
MEDIA_DIR): back up the directory, on Docker thebongsu-mediavolume (docker compose cp bongsu:/media .copies it out).
Test a restore
A backup you haven't restored is a guess. Once, restore into a scratch copy (DATABASE_PATH=test.db, a different
PORT) and open it. The copy holds your live connections, so start it with SCHEDULES=off (it serves and runs no
schedules) and DRY_RUN=1: without them it would publish the same posts as the real instance.