# Back up, restore and upgrade

This tutorial backs up the outpost configuration, access credentials,
device grants, monitoring checkpoints and queued notifications.
Original email messages remain with your email provider; this backup
restores the Mailwake instance.

## 1. What to preserve

A recoverable backup contains:

- The complete `/data` directory:
  `mailwake.db`, `secret.key` and related
  SQLite files. Keep the database and key together.
- Compose configuration: ports, volumes and environment settings.
- The running image: restore the same program version as the
  database.

The script below targets the standard Compose installation. It saves
configuration and the image, briefly stops the outpost, copies all data,
then restarts the outpost. Monitoring pauses during that interval. After
restart, the outpost resumes from saved checkpoints and catches up messages
still in the folder that were received within the last 24 hours.

If your installation uses reverse-proxy configuration or other host-mounted files, copy those files into the backup as well. Adjust their paths when restoring on another
machine.

## 2. Create your first backup

Download the
[backup script](/downloads/backup.sh), save it
as `backup.sh`, and place it in your server installation
directory, for example `~/mailwake/backup.sh`.

View the backup script

```sh
#!/bin/sh
# Copyright (C) 2026 Mailwake contributors
# SPDX-License-Identifier: AGPL-3.0-only
# License: https://www.gnu.org/licenses/agpl-3.0.html
# Back up a running Compose installation; execute as a user with Docker access.
set -eu

if [ "$#" -ne 2 ]; then
  echo "Usage: backup.sh /absolute/compose-directory /absolute/backup-directory" >&2
  exit 2
fi
case "$1:$2" in
  /*:/*) ;;
  *) echo "Use absolute directory paths." >&2; exit 2 ;;
esac
cd "$1"
umask 077
lock=".mailwake-backup.lock"
if ! mkdir "$lock" 2>/dev/null; then
  echo "Could not create $PWD/$lock. Check directory permissions and active backups." >&2
  exit 1
fi
restart=0
cleanup() {
  result=$?
  trap - EXIT
  if [ "$restart" -eq 1 ]; then
    docker compose start core >&2 || result=1
  fi
  rmdir "$lock" || result=1
  exit "$result"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
container=$(docker compose ps -q core)
if [ -z "$container" ]; then
  echo "Start the Core service before running this backup." >&2
  exit 1
fi
mkdir -p "$2"
backup="$2/mailwake-$(date +%Y%m%d-%H%M%S)"
test ! -e "$backup"
staging="$backup.incomplete"
mkdir "$staging"
mkdir "$staging/data"
docker compose config > "$staging/compose.yaml"
docker inspect --format '{{.Image}}' "$container" > "$staging/image-id.txt"
docker image save --output "$staging/image.tar" "$(cat "$staging/image-id.txt")"
restart=1
docker compose stop core >&2
docker compose cp core:/data/. "$staging/data/" >&2
docker compose start core >&2
restart=0
mv "$staging" "$backup"
printf 'Backup saved: %s\n' "$backup"

```

Run in your server terminal:

```
sh "$HOME/mailwake/backup.sh" "$HOME/mailwake" "$HOME/mailwake-backups"
```

**Check the result**: the terminal prints
`Backup saved:` with the new directory path, and
`docker compose ps` shows the outpost running again. If copying
fails, the script attempts to restart the outpost and returns a failure.
Directories ending in `.incomplete` are unfinished
backups; keep them for troubleshooting.

The lock directory prevents overlapping scheduled and manual
backups. If the host crashes or the process is forcibly killed,
first confirm that no backup is running, then remove
`.mailwake-backup.lock` from the installation directory
and rerun the script.

Each backup has this layout:

```
mailwake-20261008-030000/
  compose.yaml
  image-id.txt
  image.tar
  data/
    mailwake.db
    secret.key
    ...
```

Saving the image requires additional disk space and happens before
the outpost stops. The directory uses private permissions and contains both
the encrypted credentials and their key. Confirm
`data/mailwake.db`, `data/secret.key` and
`image.tar` exist, then perform the restore exercise
below at least once.

## 3. Restore into a new volume

Stop the original instance and keep its original volume. Both
instances share mailbox and device identities, so run only one at a
time.

```
cd ~/mailwake
docker compose stop core
```

Set `backup` to your actual backup path. Prepare a new
installation directory, its image and configuration:

```
backup="$HOME/mailwake-backups/mailwake-20261008-030000"
mkdir -p ~/mailwake-restored
cd ~/mailwake-restored
cp "$backup/compose.yaml" ./compose.yaml
docker load --input "$backup/image.tar"
restore_volume="mailwake-restored-$(date +%Y%m%d-%H%M%S)"
cat > compose.override.yaml <<EOF
name: mailwake-restored
services:
  core:
    image: $(cat "$backup/image-id.txt")
volumes:
  core-data:
    name: $restore_volume
networks:
  default:
    name: $restore_volume-network
EOF
```

Compose automatically loads `compose.override.yaml`. It
selects a fresh data volume and the exact backed-up image. Create
the container, copy data, set ownership for the container user, and
start:

```
(
  set -eu
  docker compose create --pull never core
  docker compose run --rm --no-deps -T --user 0:0 --entrypoint sh core \
    -c 'entries=$(ls -A /data) && test -z "$entries"'
  docker compose cp "$backup/data/." core:/data/
  docker compose run --rm --no-deps -T --user 0:0 --cap-add CHOWN --entrypoint chown core \
    -R 10001:10001 /data
  docker compose up -d --pull never
  docker compose logs --tail=50 core
)
```

Continue only after each command succeeds. If the empty-volume check
fails, choose a fresh `restore_volume`, rewrite the
override and recreate the container. A populated destination could
mix old files with restored data; this procedure uses an empty
volume.

**Check the result**: sign in with your original
administrator password. Mailboxes and folder selections remain
present, monitoring resumes, and device grants come from the backup.
If you see a new setup screen, stop the restored instance and check
the copy path and volume mount. For decryption errors, confirm the
database and `secret.key` came from the same backup.

## 4. Verify the restored instance

1. Check mailbox counts, selected folders and check methods against
   the original installation.
2. Send a channel test notification and confirm it arrives on your
   phone.
3. Send a new email from another mailbox into a watched folder and
   check its delivery record.
4. Restart the restored instance, sign in again, and confirm its
   configuration and monitoring persist.

Once verified, point the reverse proxy at the restored instance and
manage it from `~/mailwake-restored`. Update the backup
job to use that directory. Leave the original instance stopped and
retain its volume and the backup until recovery is confirmed.

Pending or failed notifications return to their backed-up state and
may be delivered again. Use the same procedure to roll back an
upgrade, restoring the old data and image together.

## 5. Schedule backups and keep an off-site copy

On a Linux server, first complete a manual backup, then use
`crontab -e` to add the following. Replace
`/home/you` with your absolute paths:

```
PATH=/usr/local/bin:/usr/bin:/bin
0 3 * * * /bin/sh /home/you/mailwake/backup.sh /home/you/mailwake /home/you/mailwake-backups >> /home/you/mailwake-backup.log 2>&1
```

This runs daily at 03:00 in the server's local timezone. The
scheduled user needs Docker access. On a NAS, use the same Shell
command in its task scheduler. The following day, check for a new
directory and `Backup saved:` in the log. Address
failures; a scheduler cannot establish that a backup is recoverable.

A backup on the same disk does not cover disk or machine failure.
Copy completed directories to another machine or protected backup
storage, for example over SSH:

```
scp -r "$HOME/mailwake-backups/mailwake-20261008-030000" \
  backup-user@backup-host:/srv/mailwake-backups/
```

Replace the source, remote user, host and destination. Restrict
access at the destination to backup maintainers. Use encrypted
storage or encrypt the archive before uploading when encryption at
rest is needed. A possible retention policy is seven daily and four
weekly copies. Before deleting older backups, verify the off-site
copy and a recent restore exercise.

## Back up a Docker Run installation

The Docker Run installation uses a container named
`mailwake` and volume `mailwake-data`. These
commands run in a subshell and attempt to restart the original
container if copying fails:

```
(
  set -eu
  umask 077
  backup="$HOME/mailwake-backups/mailwake-$(date +%Y%m%d-%H%M%S)"
  mkdir -p "$backup/data"
  docker inspect --format '{{.Image}}' mailwake > "$backup/image-id.txt"
  docker image save --output "$backup/image.tar" "$(cat "$backup/image-id.txt")"
  trap 'docker start mailwake >/dev/null' EXIT
  docker stop mailwake
  docker cp mailwake:/data/. "$backup/data/"
  docker start mailwake
  trap - EXIT
  printf 'Backup saved: %s\n' "$backup"
)
```

Save your Docker Run command alongside the data. To restore, stop
the original container, load `image.tar`, create a new
volume and container, and copy data before starting:

```
(
  set -eu
  backup="$HOME/mailwake-backups/mailwake-20261008-030000"
  image=$(cat "$backup/image-id.txt")
  restore_volume="mailwake-restored-$(date +%Y%m%d-%H%M%S)"
  docker stop mailwake
  docker load --input "$backup/image.tar"
  docker volume create "$restore_volume"
  docker create --name mailwake-restored --restart unless-stopped \
    --stop-timeout 30 --read-only --security-opt no-new-privileges:true --cap-drop ALL \
    -p 127.0.0.1:8080:8080 -v "$restore_volume:/data" "$image"
  docker run --rm --user 0:0 --entrypoint sh -v "$restore_volume:/data" "$image" \
    -c 'entries=$(ls -A /data) && test -z "$entries"'
  docker cp "$backup/data/." mailwake-restored:/data/
  docker run --rm --user 0:0 --entrypoint chown -v "$restore_volume:/data" "$image" \
    -R 10001:10001 /data
  docker start mailwake-restored
)
```

Continue only after each command succeeds and follow the
verification checklist above. Keep the original container and
volume, leaving the original container stopped while the restored
instance runs.
