Backups and restore
Back up the database, uploaded files and the secrets that make them readable, and restore them.
A complete backup has three parts. Missing any of them means data loss. The background-job volume inngest_data holds only queued and scheduled runs and does not need a backup.
| Part | Where it lives (Docker Compose) | Why |
|---|---|---|
| PostgreSQL database | volume postgres_data | All records, users, settings, audit log, embeddings, encrypted keys and tokens |
| Uploaded files | volume minio_data | Documents, avatars, invoice PDFs |
| Secrets | your .env or platform settings; values you did not set are in volume app_data (/app/data/secrets) | EMAIL_ENCRYPTION_KEY decrypts stored keys, tokens and mailbox passwords; BETTER_AUTH_SECRET keeps sessions valid |
A database backup without the matching EMAIL_ENCRYPTION_KEY restores the records, but every saved AI key, mailbox password, Google Calendar connection, Calendly setting and plugin secret is unreadable. Store the key with your backups, separately from the backup files themselves.
Docker Compose prefixes volume names with the project name (by default the folder name), so on the host they appear as, for example, nextcrm-app_postgres_data. The commands below go through docker compose and do not need the full name. Run them from the folder with the compose file.
Generated secrets
If the entrypoint generated any secrets, copy them out once and store them with your backups:
docker compose exec app sh -c 'cd /app/data/secrets && for f in *; do echo "$f=$(cat "$f")"; done'The output has .env format. Putting those lines into .env makes the values independent of the volume, which is the simplest way to restore them.
Database
Create a dump in Postgres' custom format:
docker compose exec -T postgres \
pg_dump -U nextcrm -d nextcrm -Fc > nextcrm-$(date +%F).dumpReplace nextcrm with your POSTGRES_USER and POSTGRES_DB if you changed them. pg_dump takes a consistent snapshot while the app is running.
Uploaded files
Archive the MinIO data directory from a throwaway container that mounts the minio service's volume:
docker compose stop minio
docker run --rm --volumes-from "$(docker compose ps -aq minio)" \
-v "$PWD":/backup alpine \
tar czf /backup/minio-data-$(date +%F).tgz -C /data .
docker compose start minioStopping MinIO for the copy gives a consistent archive; uploads fail during those seconds. If you use external S3 storage instead of the bundled MinIO, back up the bucket with your provider's tools.
Restore
Start from an empty stack with the same secrets
Use the same .env, especially the same EMAIL_ENCRYPTION_KEY and BETTER_AUTH_SECRET (add the generated values you copied out, see Generated secrets). Start only the storage services:
docker compose up -d postgres minioRestore the database
docker compose exec -T postgres \
pg_restore -U nextcrm -d nextcrm --clean --if-exists --no-owner < nextcrm-2026-01-31.dumpRestore the files
docker compose stop minio
docker run --rm --volumes-from "$(docker compose ps -aq minio)" \
-v "$PWD":/backup alpine \
sh -c "rm -rf /data/* /data/.[!.]* ; tar xzf /backup/minio-data-2026-01-31.tgz -C /data"
docker compose start minioStart the app
docker compose up -dThe entrypoint applies any migrations newer than the dump. Because the Users table is not empty, the seed does not run.
Test a restore on a separate machine from time to time. A backup you have never restored is a guess.