A newsletter you run on your own server

Docs

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.

# 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, holds everything, as with Docker.

# Back up
bin/kamal app stop -d production
ssh [email protected] '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 [email protected]:mailpiece.tgz .

# Restore
scp mailpiece.tgz [email protected]:
bin/kamal app stop -d production
ssh [email protected] '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.

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

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.