apps/backup/README.md 6.3 K raw
1
# Backup
2
3
Automated SQLite backups for Jotts, Sipp, Cellar, Posts, Feeds, Library, Bookmarks, Parcels, and Easel to Cloudflare R2. Runs every 6 hours via cron inside a Docker container and prunes backups older than 30 days.
4
5
## Setup
6
7
1. **Create an R2 bucket:**
8
   - Log in to the [Cloudflare dashboard](https://dash.cloudflare.com).
9
   - Select your account, then navigate to **R2 Object Storage** in the sidebar.
10
   - Click **Create bucket** and name it `andromeda-backups` (or a name of your choice).
11
12
2. **Find your account ID and endpoint:**
13
   - Your account ID is in the Cloudflare dashboard URL: `https://dash.cloudflare.com/<account-id>`.
14
   - You can also find it on the **R2 Overview** page under **Account ID**.
15
   - Your R2 endpoint is `https://<account-id>.r2.cloudflarestorage.com`.
16
17
3. **Generate R2 API credentials:**
18
   - On the **R2 Overview** page, click **Manage R2 API Tokens**.
19
   - Click **Create API Token**.
20
   - Give it a name (e.g. `andromeda-backup`).
21
   - Set **Permissions** to **Object Read & Write**.
22
   - Under **Specify bucket(s)**, select the bucket you created (or apply to all buckets).
23
   - Click **Create API Token**.
24
   - Copy the **Access Key ID** and **Secret Access Key** — these are only shown once.
25
26
4. **Configure the environment:**
27
28
```sh
29
cp .env.example .env
30
```
31
32
Fill in the values from the previous steps:
33
34
```
35
R2_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
36
AWS_ACCESS_KEY_ID=<your-r2-access-key>
37
AWS_SECRET_ACCESS_KEY=<your-r2-secret-key>
38
R2_BUCKET=andromeda-backups
39
```
40
41
4. If your Docker volume names differ from the defaults, set them in `.env`:
42
43
```
44
JOTTS_VOLUME=jotts_jotts-data
45
SIPP_VOLUME=sipp_sipp-data
46
CELLAR_VOLUME=cellar_cellar-data
47
POSTS_VOLUME=posts_posts-data
48
FEEDS_VOLUME=feeds_feeds-data
49
LIBRARY_VOLUME=library_library-data
50
BOOKMARKS_VOLUME=bookmarks_bookmarks-data
51
PARCELS_VOLUME=parcels_parcels_data
52
EASEL_VOLUME=easel_easel-data
53
```
54
55
Run `docker volume ls` to check the actual names on your host.
56
57
5. Start the backup container:
58
59
**Option A: Build from source**
60
61
```sh
62
docker compose up -d --build
63
```
64
65
**Option B: Use the pre-built image from GHCR**
66
67
Override the `build` directive with `image` in your compose file or use a separate override:
68
69
```sh
70
docker compose -f docker-compose.yml -f docker-compose.ghcr.yml up -d
71
```
72
73
Create a `docker-compose.ghcr.yml` override file:
74
75
```yaml
76
services:
77
  backup:
78
    image: ghcr.io/stevedylandev/andromeda-backup:latest
79
    build: !reset null
80
```
81
82
Or simply run the image directly:
83
84
```sh
85
docker run -d --restart unless-stopped \
86
  --env-file .env \
87
  -v jotts_jotts-data:/data/jotts:ro \
88
  -v sipp_sipp-data:/data/sipp:ro \
89
  -v cellar_cellar-data:/data/cellar:ro \
90
  -v posts_posts-data:/data/posts:ro \
91
  -v feeds_feeds-data:/data/feeds:ro \
92
  -v library_library-data:/data/library:ro \
93
  -v bookmarks_bookmarks-data:/data/bookmarks:ro \
94
  -v parcels_parcels_data:/data/parcels:ro \
95
  -v easel_easel-data:/data/easel:ro \
96
  ghcr.io/stevedylandev/andromeda-backup:latest
97
```
98
99
## Running a Manual Backup
100
101
```sh
102
docker compose exec backup /usr/local/bin/backup.sh
103
```
104
105
## Checking Logs
106
107
```sh
108
docker compose exec backup cat /var/log/backup.log
109
```
110
111
## Restoring from a Backup
112
113
Use `restore.sh` to download a backup from R2 and write it into the target Docker volume.
114
Run it on the **host** (it shells out to `docker run`; the backup container mounts the data
115
volumes read-only). It reads the same `.env` as the backup (`R2_ENDPOINT`, `R2_BUCKET`,
116
AWS keys, optional `*_VOLUME` overrides) — `source .env` first, or prefix the env inline.
117
118
```sh
119
restore.sh <app|all> [--timestamp <ts>] [--list] [--yes]
120
```
121
122
- `<app>` — one of: `jotts sipp cellar posts feeds library bookmarks parcels easel`, or `all`.
123
- `--timestamp <ts>` — restore a specific backup (e.g. `2026-04-04T060000Z`); the
124
  `.sqlite.gz` suffix is optional. Default: the latest backup. Not valid with `all`.
125
- `--list` — list available backups for the app(s) and exit (no restore).
126
- `--yes` — skip the interactive confirmation prompt.
127
128
> **Warning:** Restoring overwrites live data. Stop the target service before restoring and
129
> restart it after for a clean restore — `restore.sh` does **not** stop or start services.
130
131
Examples:
132
133
```sh
134
# Load credentials/volume overrides
135
set -a; . ./.env; set +a
136
137
# See what's available
138
restore.sh jotts --list
139
140
# Restore the latest jotts backup (prompts for confirmation)
141
restore.sh jotts
142
143
# Restore a specific backup, no prompt
144
restore.sh jotts --timestamp 2026-04-04T060000Z --yes
145
146
# Restore every app's latest backup
147
restore.sh all
148
```
149
150
For each app the script: resolves the backup key (latest or `--timestamp`), downloads and
151
decompresses it, runs `PRAGMA integrity_check`, confirms the target volume exists, then
152
copies the database to the volume root via a throwaway `debian:bookworm-slim` container.
153
154
### Manual restore (fallback)
155
156
1. List backups: `aws s3 ls s3://andromeda-backups/jotts/ --endpoint-url https://<account-id>.r2.cloudflarestorage.com`
157
2. Download: `aws s3 cp s3://andromeda-backups/jotts/<timestamp>.sqlite.gz ./restore.sqlite.gz --endpoint-url <endpoint>`
158
3. Decompress: `gunzip restore.sqlite.gz`
159
4. Stop the target service so nothing is writing to the database.
160
5. Copy into the volume: `docker run --rm -v jotts_jotts-data:/data -v $(pwd):/backup debian:bookworm-slim cp /backup/restore.sqlite /data/jotts.sqlite`
161
6. Restart the service.
162
163
## Configuration
164
165
| Variable | Default | Description |
166
|---|---|---|
167
| `R2_ENDPOINT` | — | Cloudflare R2 S3-compatible endpoint |
168
| `AWS_ACCESS_KEY_ID` | — | R2 access key |
169
| `AWS_SECRET_ACCESS_KEY` | — | R2 secret key |
170
| `R2_BUCKET` | `andromeda-backups` | R2 bucket name |
171
| `RETENTION_DAYS` | `30` | Days to keep backups before pruning |
172
| `JOTTS_VOLUME` | `jotts_jotts-data` | Docker volume name for Jotts data |
173
| `SIPP_VOLUME` | `sipp_sipp-data` | Docker volume name for Sipp data |
174
| `CELLAR_VOLUME` | `cellar_cellar-data` | Docker volume name for Cellar data |
175
| `POSTS_VOLUME` | `posts_posts-data` | Docker volume name for Posts data |
176
| `FEEDS_VOLUME` | `feeds_feeds-data` | Docker volume name for Feeds data |
177
| `LIBRARY_VOLUME` | `library_library-data` | Docker volume name for Library data |
178
| `BOOKMARKS_VOLUME` | `bookmarks_bookmarks-data` | Docker volume name for Bookmarks data |
179
| `PARCELS_VOLUME` | `parcels_parcels_data` | Docker volume name for Parcels data |
180
| `EASEL_VOLUME` | `easel_easel-data` | Docker volume name for Easel data |