NextCRM

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.

PartWhere it lives (Docker Compose)Why
PostgreSQL databasevolume postgres_dataAll records, users, settings, audit log, embeddings, encrypted keys and tokens
Uploaded filesvolume minio_dataDocuments, avatars, invoice PDFs
Secretsyour .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).dump

Replace 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 minio

Stopping 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 minio

Restore the database

docker compose exec -T postgres \
  pg_restore -U nextcrm -d nextcrm --clean --if-exists --no-owner < nextcrm-2026-01-31.dump

Restore 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 minio

Start the app

docker compose up -d

The 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.

On this page