# Backups

Everything Mailpiece keeps is in its storage volume. Back it up and you can bring your newsletter back on any server.

Paste this into your agent:

> Set up backups for my Mailpiece install. Read mailpiece.dev/docs/backups.md first.

## Before you start

Keep the same `SECRET_KEY_BASE` beside a backup. Under another, each newsletter's relay token or SMTP password can't be read and has to be set again on Settings › Outgoing mail.

A restore brings back everything as of the backup, so anyone mailed since shows as unsent. Over SMTP they are mailed again, while the relay takes each delivery once and sends nothing new.

## With Docker

The volume holds the four databases, and uploads unless they go to a bucket.

```sh
# Back up
docker stop mailpiece
docker run --rm -v mailpiece:/rails/storage -v "$PWD":/backup alpine \
  tar czf /backup/mailpiece.tgz -C /rails/storage .
docker start mailpiece

# Restore
docker stop mailpiece
docker run --rm -v mailpiece:/rails/storage -v "$PWD":/backup alpine \
  sh -c "rm -rf /rails/storage/* && tar xzf /backup/mailpiece.tgz -C /rails/storage"
docker start mailpiece
```

## With Kamal

The volume, `mailpiece_live` in [Install with Kamal](https://mailpiece.dev/docs/install), holds everything, as with Docker.

```sh
# Back up
bin/kamal app stop -d production
ssh root@203.0.113.10 'docker run --rm -v mailpiece_live:/rails/storage -v /root:/backup alpine \
  tar czf /backup/mailpiece.tgz -C /rails/storage .'
bin/kamal app start -d production
scp root@203.0.113.10:mailpiece.tgz .

# Restore
scp mailpiece.tgz root@203.0.113.10:
bin/kamal app stop -d production
ssh root@203.0.113.10 'docker run --rm -v mailpiece_live:/rails/storage -v /root:/backup alpine \
  sh -c "rm -rf /rails/storage/* && tar xzf /backup/mailpiece.tgz -C /rails/storage"'
bin/kamal app start -d production
```

## Streaming with Litestream

For backups as they happen, to an S3-compatible bucket: Litestream can run beside the app as an accessory in your destination file, reading a `config/litestream.yml` that Git ignores. This one streams the four databases and keeps fifteen days. Its keys go in `.kamal/secrets.production`.

Uploads are not copied: in an `ACTIVE_STORAGE_SERVICE=s3` bucket they have no backup or history of their own, and on the server's disk Litestream skips them.

```sh
# config/deploy.production.yml
accessories:
  litestream:
    image: litestream/litestream:0.5.17
    roles:
      - web
    cmd: replicate
    files:
      - config/litestream.yml:/etc/litestream.yml
    options:
      user: "1000:1000"
      stop-timeout: 60
    volumes:
      - mailpiece_live:/rails/storage
    env:
      clear:
        LITESTREAM_BUCKET: my-newsletter-backups
      secret:
        - LITESTREAM_ACCESS_KEY_ID
        - LITESTREAM_ENDPOINT
        - LITESTREAM_SECRET_ACCESS_KEY

aliases:
  backups: accessory exec litestream "ltx -level all /rails/storage/production.sqlite3"
  restore-check: accessory exec litestream "restore -o /rails/storage/restored.sqlite3 /rails/storage/production.sqlite3"

# config/litestream.yml
dbs:
  - path: /rails/storage/production.sqlite3
    replica: { type: s3, bucket: $LITESTREAM_BUCKET, path: production, endpoint: $LITESTREAM_ENDPOINT, region: auto }
  - path: /rails/storage/production_queue.sqlite3
    replica: { type: s3, bucket: $LITESTREAM_BUCKET, path: production_queue, endpoint: $LITESTREAM_ENDPOINT, region: auto }
  - path: /rails/storage/production_cable.sqlite3
    replica: { type: s3, bucket: $LITESTREAM_BUCKET, path: production_cable, endpoint: $LITESTREAM_ENDPOINT, region: auto }
  - path: /rails/storage/production_cache.sqlite3
    replica: { type: s3, bucket: $LITESTREAM_BUCKET, path: production_cache, endpoint: $LITESTREAM_ENDPOINT, region: auto }
snapshot:
  retention: 360h
```

## Restoring from Litestream

`backups` lists every backup file, and `restore-check` writes the latest into `restored.sqlite3` beside the live database. Delete it afterwards; a second run refuses to overwrite it.

Keep the old server off, or two machines write to the same bucket. Never use `kamal setup` to bring a server back: it backs up empty databases on top of your history. Deploy, stop the app and Litestream, move the databases to `/rails/storage/aside` and restore the four. To restore to a moment instead, add `-timestamp 2026-10-06T17:15:00Z` after `restore`.

On a new server, Kamal hands Litestream its config and keys only when it boots it, so boot it once after the databases are out of the way.

Check the data before starting Litestream again with `bin/kamal accessory start litestream -d production`, then delete `aside`. If the data is wrong, stop the app and move the files in `aside` back.

```sh
bin/kamal backups -d production
bin/kamal restore-check -d production

bin/kamal server bootstrap -d production   # a new server only
bin/kamal deploy -d production
bin/kamal app stop -d production
bin/kamal accessory stop litestream -d production
bin/kamal app exec -d production \
  "bash -c 'mkdir /rails/storage/aside && mv /rails/storage/production*.sqlite3* /rails/storage/aside/ && rm -rf /rails/storage/.production*.sqlite3-litestream'"

bin/kamal accessory reboot litestream -d production   # a new server only
bin/kamal accessory stop litestream -d production     # a new server only

for db in production production_queue production_cable production_cache; do
  bin/kamal accessory exec litestream -d production "restore -integrity-check quick /rails/storage/$db.sqlite3"
done
bin/kamal app start -d production
```

Still need help? Ask on [GitHub](https://github.com/mailpiece/mailpiece).
