> ## Documentation Index
> Fetch the complete documentation index at: https://www.mill.fyi/llms.txt
> Use this file to discover all available pages before exploring further.

# Backup and recovery

> Back up PostgreSQL and test a full restore.

A full PostgreSQL backup preserves accounts, sessions, passkeys and their recovery-code digests, boards, tasks, comments, members, activity, notifications and API-key/OAuth records. Retained pre-launch archive schemas are included when present. Database backup is the supported recovery method; Mill has no portable work export/import.

Also save the original `MILL_SECRET` and runtime settings securely. Protected database values require that secret even after a successful restore. Record both API/UI versions or digests, the Compose project name and PostgreSQL major version with each backup.

## Back up an existing or managed database

Use your PostgreSQL provider's supported backup and restore procedures, or PostgreSQL's `pg_dump` and `pg_restore` against the database in `DATABASE_URL`. The backup must cover the complete Mill database, not just task tables. Rehearse restoration into a separate database, configure a separate Mill API/UI pair with its connection URL and the original `MILL_SECRET`, and verify the restored workspace before relying on the backup.

## Back up bundled PostgreSQL

If you installed the optional PostgreSQL Compose overlay, use its packaged `pg_dump`. You do not need Git, Node or a source checkout:

```sh theme={"system"}
umask 077
mkdir -p backups
backup_file="backups/mill-$(date +%Y%m%d-%H%M%S).dump"
docker compose --project-name mill -f docker-compose.yml -f docker-compose.postgres.yml exec -T postgres sh -c 'exec pg_dump --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" --format=custom --no-owner --no-acl' > "$backup_file"
```

Confirm the command succeeded before relying on the file. A failed dump may leave a partial file; remove that file and investigate the failure. Do not overwrite an existing backup. `pg_dump` takes a consistent database snapshot while ordinary writes continue.

Encrypt backups and a separate copy of the runtime secrets, store a copy off the Docker host and periodically rehearse recovery. Keep enough free space for both the archive and a separate restored database.

## Rehearse bundled PostgreSQL recovery

Use the same compatible API and UI images as the backup. Copy the installation's two Compose files and `.env` into a separate recovery directory. In the copied `docker-compose.yml`, change the web service's host port to a free loopback port and the API service's host port to another free loopback port. In the copied `.env`, set `MILL_WEB_URL` and `MILL_API_URL` to those respective loopback origins and preserve `MILL_SECRET`, `POSTGRES_PASSWORD` and the private PostgreSQL URL. Keep the copy private. The separate project name below creates its own PostgreSQL volume.

Run these commands from that recovery directory:

```sh theme={"system"}
chmod 600 .env
docker compose --project-name mill-recovery -f docker-compose.yml -f docker-compose.postgres.yml up --detach --wait postgres
```

Restore into that new empty database. Replace the example filename with your backup:

```sh theme={"system"}
docker compose --project-name mill-recovery -f docker-compose.yml -f docker-compose.postgres.yml exec -T postgres sh -c 'exec pg_restore --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" --no-owner --no-acl --exit-on-error --single-transaction' < backups/mill-YYYYMMDD-HHMMSS.dump
docker compose --project-name mill-recovery -f docker-compose.yml -f docker-compose.postgres.yml up --detach --wait
curl --fail http://localhost:4322/health/ready
```

Run the second command only after restore succeeds. Keep the API and UI stopped if restore fails. Do not run this empty-database recipe against an existing workspace.

Sign in at the recovery URL and check a board, task, comment, member, notification and a client credential that you can safely test. OAuth may require reconnection if its resource URL points to the original installation. Password sign-in works at the recovery origin; physical passkeys stay bound to their original origin. To test production passkeys, use the original HTTPS origin in a controlled recovery environment.

After the rehearsal, remove only the disposable recovery project:

```sh theme={"system"}
docker compose --project-name mill-recovery -f docker-compose.yml -f docker-compose.postgres.yml down --volumes
```

## Recover the running installation

Put the public UI into maintenance. Back up the existing database before replacing it, then stop **both `api` and `web`**. Restore to a separate project first using the compatible images, verify it, and switch the HTTPS proxy to its UI. Retain the original database until the recovered installation is confirmed.

If you deliberately need to replace an existing database, use PostgreSQL's documented restore process or the reviewed repository helper with explicit project confirmation. Do not add `--clean` casually to a restore command: it removes existing database objects.

## Optional repository helpers

Operators using bundled PostgreSQL who keep a source checkout can use the guarded scripts. They create private backup files, reject overwrite, validate the archive and require explicit target confirmation:

```sh theme={"system"}
bash tools/backup.sh --project mill --env-file .env --output backups/mill.dump
bash tools/restore.sh --project mill-recovery --confirm-project mill-recovery --env-file recovery.env --input backups/mill.dump
```

For these repository helpers, `recovery.env` identifies the separate bundled project; if its copied Compose file has a different port, use the direct recovery commands above. The restore helper stops both API and UI, rejects a nonempty target unless `--replace` is explicitly supplied, restores in one transaction, and starts both services only after success. A failed restore leaves them stopped. These helpers are optional; a published-image installation does not require them.

See [upgrades](/upgrades) for schema compatibility and private pre-launch conversion. Historical portable JSON exports have no importer in v1; retain them separately if needed, but use database backups for supported recovery.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.