Deployment and operations#
scrap is one process over one data folder, so both supported installs are the same container with different supervisors: systemd and rootless podman on a single server, or a Helm chart on Kubernetes. Scheduled work (backup, gc, enrichment) runs as timers or CronJobs around that one folder; step-by-step is in the how-to guides.
Colours: blue is storage, orange is work that calls Claude, green is the person using scrap.
Single server#
systemd user units run one rootless podman container; Tailscale Serve is the only way in (16. Deployment: rootless podman units for one server, a Helm chart for Kubernetes, 18. Single-server installs listen on a Unix socket, reached only through Tailscale Serve).
flowchart TD
me(["Phone / browser"]):::user
subgraph host["One unix user, rootless podman"]
ts["Tailscale Serve"]
subgraph units["systemd user units"]
svc["scrap.service"]
tb["backup timers<br/>daily + monthly"]
tg["gc timer"]
tr["enrich-related timer"]
tt["enrich-topics timer"]
end
app["scrap container<br/>(serve)"]
data[("SCRAP_DATA<br/>scrap.db + files/")]:::store
bak[("SCRAP_BACKUPS<br/>timestamped runs + monthly/")]:::store
end
me -->|HTTPS, tailnet| ts
ts -->|"Unix socket<br/>scrap.sock"| app
svc -->|scrap-run| app
app --- data
tb -->|"scrap-backup<br/>(--network=none)"| bak
tb -.->|reads| data
tg -->|"scrap-gc<br/>(--network=none)"| data
tr -->|"podman exec<br/>enrich --related"| app:::claude
tt -->|"podman exec<br/>enrich --topics"| app
classDef store fill:#e8f1ff,stroke:#4a7bd0
classDef claude fill:#fff3e0,stroke:#e08a00
classDef user fill:#e9f7ef,stroke:#2e9d5b
Piece |
What it does |
|---|---|
|
|
Listener |
|
Backup, gc |
Separate throwaway containers with |
Enrichment |
|
Image |
One multi-arch image (amd64 + arm64) from the release workflow |
Five timers, all Persistent except gc:
Timer |
When |
Runs |
|---|---|---|
|
daily 03:30 |
backup, then retention |
|
1st of the month 03:50 |
backup into |
|
daily 03:41 |
|
|
Sundays 04:11 |
|
|
daily 04:30 |
|
Each has a 15 minute random delay. install.sh enables a timer only if the installed image has
its command, and says so when it does not.
Kubernetes#
The chart runs exactly one replica with the Recreate strategy, because SQLite has a single
writer (3. One process, one SQLite database, one folder of files).
flowchart TD
me(["Browser"]):::user
subgraph edge["Edge (your choice)"]
ing["Ingress<br/>Tailscale operator or<br/>Cloudflare Access"]
end
subgraph ns["Release namespace"]
np["NetworkPolicy<br/>(optional)"]
svc["Service :80"]
dep["Deployment<br/>1 replica, Recreate"]
pvc[("PVC /data<br/>scrap.db + files/")]:::store
subgraph jobs["CronJobs (beside the pod)"]
cb["backup 03:30<br/>+ monthly 1st"]
cr["enrich-related 03:41"]:::claude
ct["enrich-topics Sun 04:11"]:::claude
cg["gc 04:30"]
end
bpvc[("Backup PVC<br/>timestamped runs + monthly/")]:::store
end
me --> ing --> np --> svc --> dep
dep --- pvc
cb --> bpvc
cb -.-> pvc
cr & ct & cg -.-> pvc
cg -.->|"needs recent<br/>COMPLETE"| bpvc
classDef store fill:#e8f1ff,stroke:#4a7bd0
classDef claude fill:#fff3e0,stroke:#e08a00
classDef user fill:#e9f7ef,stroke:#2e9d5b
Same data shape.
/dataholdsscrap.db,files/and the regenerable preview cache; the cache is never backed up.No login in scrap. Reach it only through a private network or an authenticating proxy;
identityHeadernames the header recorded as the author andmcp.tokenSecretcan require a bearer token on/mcp(7. Tailscale Serve is the only way in; no login code, 23. Bearer token on /mcp and an identity-header setting).CronJobs share the claim. The data PVC is
ReadWriteOnce, so jobs schedule onto the pod’s node. The two enrich jobs exist only wheningest.enabled; gc needsbackup.enabledunlessgc.requireBackupis false.Secrets are never values. The chart names an existing Secret for the API key, notify URL and MCP token and never creates one. Both PVCs carry
helm.sh/resource-policy: keep.Hardening. Restricted Pod Security: non-root uid 10001, read-only root filesystem, all capabilities dropped,
RuntimeDefaultseccomp.Health.
GET /healthzreturns 200 withstatus,version,max_upload_bytesandpaused(why the Claude queue is paused, else null); 503 if the ingest worker has stopped.
Settings are in HTTP API and CLI; every chart value is in charts/scrap/values.yaml,
checked by values.schema.json.
Data protection loop#
Backup, retention, gc and restore form one loop, and gc will not delete unless the loop has recently worked.
flowchart LR
b["Backup<br/>SQLite online backup<br/>+ files/ copy"] --> v["Integrity check<br/>write COMPLETE"]
v --> r["Retention<br/>keep newest N"]
r --> g{"COMPLETE run<br/>under 26 h old?"}
g -->|yes| d["gc --delete"]
g -->|no| c["gc counts only,<br/>job fails loudly"]
r -.-> x[("Backups<br/>newest N runs")]:::store
x --> s["Restore"]
s --> k{"Checks"}
k -->|"integrity ok<br/>schema not newer"| ok["Swap in DB,<br/>add files"]
k -->|"non-empty dir<br/>without --force"| no["Refused"]
classDef store fill:#e8f1ff,stroke:#4a7bd0
Backup is safe while serving. It snapshots the database first and copies
files/second. Blobs are written before the transaction that names them, so the snapshot never names a missing file.COMPLETEmarks a good run. Only a checked snapshot gets one; a failed run’s directory is removed and older runs are untouched.Retention (
SCRAP_BACKUP_KEEPon a single server,backup.keepin Helm; install.sh writes 14, the chart defaults to 7) deletes only directories named like a run (20261019T033000Z); symlinks and other files are never touched. Both add a monthly tier inmonthly/(backup.monthlyin Helm,SCRAP_BACKUP_MONTHLY_KEEPon a single server; newest 12) for changes noticed late.gc follows backup. It takes the same lock, so a file removed mid-backup cannot be one the backup’s database names. Without a recent
COMPLETErun it only counts and exits non-zero, so a missing backup shows up as a failed job.Restore copies the backup database beside the live one and opens only the copy, so read-only media work. The copy must pass
integrity_checkand not be from a newer schema than the code; it is then migrated and swapped in. Files are only added. A refusal leaves the data folder as it was, and a non-empty folder needs--forcewith scrap stopped.A local copy is not an off-site copy. Copy
SCRAP_BACKUPS(or the backup PVC) elsewhere.
See also#
Architecture, the architecture landing page; Data model and storage for the database and files; MCP server and access for tokens and identity; Job queue and Claude features for the jobs the timers start.
Scheduled jobs and HTTP API and CLI for commands and settings.
ADRs: 3. One process, one SQLite database, one folder of files, 7. Tailscale Serve is the only way in; no login code, 16. Deployment: rootless podman units for one server, a Helm chart for Kubernetes, 18. Single-server installs listen on a Unix socket, reached only through Tailscale Serve, 21. Ingest on a Claude seat: claude -p with a long-lived token, 23. Bearer token on /mcp and an identity-header setting.