diff --git a/docs/pnpm-workspace.yaml b/docs/pnpm-workspace.yaml new file mode 100644 index 00000000..5ed0b5af --- /dev/null +++ b/docs/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +allowBuilds: + esbuild: true diff --git a/docs/src/.vitepress/config.mts b/docs/src/.vitepress/config.mts index 59d9577f..6618e396 100644 --- a/docs/src/.vitepress/config.mts +++ b/docs/src/.vitepress/config.mts @@ -5,28 +5,47 @@ export default defineConfig({ description: "Web UI and orchestrator for restic backup", base: "/backrest/", cleanUrls: true, + lastUpdated: true, + sitemap: { + hostname: 'https://garethgeorge.github.io/backrest/' + }, themeConfig: { - logo: '/logo.svg', // Assuming there's a logo or comment out if none + logo: '/logo.svg', search: { provider: 'local' }, nav: [ { text: 'Home', link: '/' }, - { text: 'Docs', link: '/introduction/getting-started' }, + { text: 'Guide', link: '/introduction/getting-started' }, + { text: 'Reference', link: '/docs/operations' }, ], sidebar: [ { - text: 'Introduction', + text: 'Getting Started', items: [ - { text: 'Getting Started', link: '/introduction/getting-started' }, - { text: 'Restore Files', link: '/introduction/restore-files' } + { text: 'Introduction & Concepts', link: '/introduction/getting-started' }, + { text: 'Installation', link: '/introduction/installation' }, + { text: 'Your First Backup', link: '/introduction/first-backup' }, + { text: 'Restoring Files', link: '/introduction/restore-files' } ] }, { - text: 'Documentation', + text: 'Guides', items: [ - { text: 'Operations', link: '/docs/operations' }, + { text: 'Scheduling Backups', link: '/guides/scheduling' }, + { text: 'Retention & Repo Health', link: '/guides/repo-health' }, + { text: 'Storage Backends', link: '/guides/storage-backends' }, + { text: 'SFTP & SSH Remotes', link: '/guides/sftp' }, + { text: 'Notifications', link: '/guides/notifications' }, + { text: 'Authentication & Security', link: '/guides/security' } + ] + }, + { + text: 'Reference', + items: [ + { text: 'Operational Model', link: '/docs/operations' }, + { text: 'Configuration & Paths', link: '/docs/configuration' }, { text: 'Hooks', link: '/docs/hooks' }, { text: 'Multihost Sync', link: '/docs/multihost' }, { text: 'API', link: '/docs/api' } @@ -36,9 +55,9 @@ export default defineConfig({ text: 'Cookbooks', items: [ { text: 'Command Hook Examples', link: '/cookbooks/command-hook-examples' }, - { text: 'Reverse Proxy Examples', link: '/cookbooks/reverse-proxy-examples' }, - { text: 'Slack Hook (Build Kit)', link: '/cookbooks/slack-hook-build-kit-examples' }, - { text: 'SSH Remote', link: '/cookbooks/ssh-remote' } + { text: 'Reverse Proxies', link: '/cookbooks/reverse-proxy-examples' }, + { text: 'Slack Hook (Block Kit)', link: '/cookbooks/slack-hook-build-kit-examples' }, + { text: 'SFTP in Docker (Manual Keys)', link: '/cookbooks/ssh-remote' } ] } ], diff --git a/docs/src/cookbooks/command-hook-examples.md b/docs/src/cookbooks/command-hook-examples.md index a74f4020..4c954dd9 100644 --- a/docs/src/cookbooks/command-hook-examples.md +++ b/docs/src/cookbooks/command-hook-examples.md @@ -33,6 +33,10 @@ curl -fsS --retry 3 https://hc-ping.com/your-uuid {{ end -}} ``` +::: tip +Backrest now has a native Healthchecks hook type that handles pinging for you — no command hook required. See the [Hooks reference](/docs/hooks) and the [Notifications guide](/guides/notifications). Use the command hook approach above only if you need custom behavior. +::: + ### System Notifications #### MacOS System Notifications @@ -87,19 +91,19 @@ fi ``` #### Battery Level Check -Verify sufficient battery level before backup. +Cancel the backup if the battery level is below 20%. **Event:** `CONDITION_SNAPSHOT_START` **Error Behavior:** `ON_ERROR_CANCEL` ```bash #!/bin/bash -if [ $(cat /sys/class/power_supply/BAT0/capacity) -gt 80 ]; then - echo "Battery level is above 20%" - exit 0 -else - echo "Battery level is below 20%" +if [ $(cat /sys/class/power_supply/BAT0/capacity) -lt 20 ]; then + echo "Battery level is below 20%, cancelling backup" exit 1 +else + echo "Battery level is at or above 20%" + exit 0 fi ``` diff --git a/docs/src/cookbooks/reverse-proxy-examples.md b/docs/src/cookbooks/reverse-proxy-examples.md index 2e03ab84..4aba14bc 100644 --- a/docs/src/cookbooks/reverse-proxy-examples.md +++ b/docs/src/cookbooks/reverse-proxy-examples.md @@ -1,18 +1,31 @@ -# Reverse Proxy Examples +# Reverse Proxies ## Introduction -Reverse proxies like [Caddy](https://caddyserver.com/) and [Traefik](https://traefik.io/traefik/) can be configured to front and protect your Backrest endpoint. +Reverse proxies like [Caddy](https://caddyserver.com/), [Traefik](https://traefik.io/traefik/), and [nginx](https://nginx.org/) can be configured to front your Backrest instance, adding TLS termination and an extra layer of access control. -## Using Caddy -For this example, we'll be running Caddy alongside Backrest via docker-compose.yaml but you can adapt this config to your environment. +Backrest's WebUI and API are served over [ConnectRPC](https://connectrpc.com/), which works over both HTTP/1.1 and HTTP/2 and relies on long-lived server-streaming requests (e.g. the operation event stream that keeps the WebUI updated in real time). [Multihost Sync](/docs/multihost) additionally uses a long-lived bidirectional stream that requires HTTP/2 end-to-end. -Here is an example docker-compose.yaml: -``` -version: "3.2" +This means your proxy should: + +- **Not buffer responses** — streamed events must be flushed to the browser as they happen. +- **Allow long-lived requests** — read/write timeouts should be hours, not seconds. The streams are intentionally persistent. +- **Speak HTTP/2 (or h2c) to Backrest if you use multihost sync** — the sync stream will not work across an HTTP/1.1 hop. The WebUI alone works fine over HTTP/1.1. + +::: warning +A reverse proxy is not a substitute for authentication. Enable Backrest's built-in authentication, or place an authenticating proxy in front, before exposing the WebUI beyond your trusted network. See [Authentication & Security](/guides/security). +::: + +## Caddy + +For this example, we'll be running Caddy alongside Backrest via `docker-compose.yml`, but you can adapt this config to your environment. + +Here is an example `docker-compose.yml`: + +```yaml services: backrest: - image: garethgeorge/backrest + image: ghcr.io/garethgeorge/backrest:latest container_name: backrest hostname: volumes: @@ -26,8 +39,6 @@ services: - BACKREST_CONFIG=/config/config.json # path for the backrest config file. - XDG_CACHE_HOME=/cache # path for the restic cache which greatly improves performance. restart: unless-stopped - depends_on: - - caddy caddy: image: caddy container_name: caddy @@ -40,7 +51,8 @@ services: ``` Your Caddyfile should look like this: -``` + +```Caddyfile { https_port 443 } @@ -52,8 +64,122 @@ backrest.example.com { ``` Some items to note: + - The `reverse_proxy` line in your Caddyfile **must** match your Backrest container's name! +- Caddy applies no read/write timeouts by default and automatically flushes streaming responses, so this minimal config already handles the WebUI's long-lived operation streams correctly. - You can extend this with [acme_dns](https://github.com/caddy-dns/acmedns) to obtain certificates for your endpoint. - `tls internal` means that Caddy will generate and utilize a self-signed certificate. - You can create an [authentication portal](https://caddyserver.com/docs/json/apps/http/servers/routes/handle/auth_portal/) to allow login via Google, etc. - You can opt to have Caddy listen to requests on port 80 (HTTP) but that's not recommended for security reasons. + +### Caddy for Multihost Sync + +If your instance acts as a [Multihost Sync](/docs/multihost) server for remote clients, you only need to expose the sync RPC path — the UI and admin API can stay on your trusted network: + +```Caddyfile +backrest.example.com { + @sync path /v1sync.BackrestSyncService/* + reverse_proxy @sync h2c://backrest:9898 { + flush_interval -1 + transport http { + read_timeout 24h + write_timeout 24h + } + } +} +``` + +Why each setting matters: + +- `@sync path /v1sync.BackrestSyncService/*` exposes only the sync endpoint; every other path returns 404, keeping the WebUI and admin API off the public internet. +- `h2c://` proxies to Backrest over cleartext HTTP/2. The sync protocol is a long-lived bidirectional stream and **requires HTTP/2 on the upstream hop** — a plain `http://` upstream will not work. +- `flush_interval -1` disables response buffering so stream messages are forwarded immediately. +- `read_timeout 24h` / `write_timeout 24h` keep the intentionally persistent sync stream from being cut off by proxy timeouts. + +If you also want to serve the WebUI through the same proxy, add a second `reverse_proxy backrest:9898` block without the path matcher — but be aware this exposes the admin API too, so keep authentication enabled. + +## Traefik + +Traefik integrates with Docker via container labels. Traefik streams responses without buffering by default, so the WebUI works with a minimal router + service definition: + +```yaml +services: + backrest: + image: ghcr.io/garethgeorge/backrest:latest + container_name: backrest + volumes: + - ./backrest/data:/data + - ./backrest/config:/config + - ./backrest/cache:/cache + - /MY-BACKUP-DATA:/userdata + labels: + - "traefik.enable=true" + - "traefik.http.routers.backrest.rule=Host(`backrest.example.com`)" + - "traefik.http.routers.backrest.entrypoints=websecure" + - "traefik.http.routers.backrest.tls.certresolver=letsencrypt" + - "traefik.http.services.backrest.loadbalancer.server.port=9898" + restart: unless-stopped + + traefik: + image: traefik:v3 + container_name: traefik + command: + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--entrypoints.websecure.address=:443" + - "--certificatesresolvers.letsencrypt.acme.email=you@example.com" + - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json" + - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true" + ports: + - "443:443" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./letsencrypt:/letsencrypt + restart: unless-stopped +``` + +Notes for multihost sync through Traefik: + +- The sync stream needs HTTP/2 on the upstream hop. Tell Traefik to speak cleartext HTTP/2 to Backrest by adding the label `traefik.http.services.backrest.loadbalancer.server.scheme=h2c` (the WebUI works over h2c as well). +- The sync stream keeps its request body open indefinitely, so the entrypoint's read timeout must not apply. If sync connections drop after about a minute, disable it: `--entrypoints.websecure.transport.respondingTimeouts.readTimeout=0`. + +## nginx + +nginx works well in front of the Backrest WebUI. The key requirements are HTTP/1.1 to the upstream, disabled buffering, and long read timeouts for the operation event streams: + +```nginx +server { + listen 443 ssl; + http2 on; # nginx >= 1.25.1; on older versions use "listen 443 ssl http2;" + server_name backrest.example.com; + + ssl_certificate /etc/nginx/certs/backrest.example.com.crt; + ssl_certificate_key /etc/nginx/certs/backrest.example.com.key; + + location / { + proxy_pass http://127.0.0.1:9898; + + # ConnectRPC requires at least HTTP/1.1 on the upstream connection. + proxy_http_version 1.1; + + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Required for the WebUI's long-lived operation event streams: + proxy_buffering off; # flush streamed responses to the client immediately + proxy_request_buffering off; # pass request bodies through as they arrive + proxy_read_timeout 1d; # keep idle streams open instead of timing out at 60s + proxy_send_timeout 1d; + } +} +``` + +::: warning +nginx's `proxy_pass` speaks HTTP/1.1 to the upstream, which is fine for the WebUI but **not sufficient for multihost sync** — the sync stream requires HTTP/2 end-to-end. If this instance serves remote sync clients, front the `/v1sync.BackrestSyncService/` path with Caddy or Traefik (using an h2c upstream) instead. +::: + +## See Also + +- [Multihost Sync](/docs/multihost) — the sync protocol's proxy requirements and troubleshooting. +- [Authentication & Security](/guides/security) — locking down Backrest before exposing it to a network. diff --git a/docs/src/cookbooks/slack-hook-build-kit-examples.md b/docs/src/cookbooks/slack-hook-build-kit-examples.md index f2862e0c..1189b699 100644 --- a/docs/src/cookbooks/slack-hook-build-kit-examples.md +++ b/docs/src/cookbooks/slack-hook-build-kit-examples.md @@ -7,7 +7,7 @@ When using the Slack Hook you can provide a simple message or a [Slack Block Kit ## Clean Job Summary
-Settings View +Slack message showing a Backrest backup job summary rendered with Block Kit
```json v-pre diff --git a/docs/src/cookbooks/ssh-remote.md b/docs/src/cookbooks/ssh-remote.md index eab36d44..34bf364f 100644 --- a/docs/src/cookbooks/ssh-remote.md +++ b/docs/src/cookbooks/ssh-remote.md @@ -4,6 +4,10 @@ This guide details how to configure a Backrest container to back up to a remote This is an advanced topic that assumes you have a basic familiarity with SSH, public key authentication, and Docker Compose. +::: tip +For most SFTP backups you don't need this manual setup — Backrest has native SFTP support that generates keys and manages known hosts for you. See [SFTP & SSH Remotes](/guides/sftp). Use this cookbook when you want full manual control over the SSH keys and configuration mounted into a Backrest Docker container. +::: + ## Prerequisites - A working Docker Compose setup for Backrest. @@ -29,7 +33,7 @@ mkdir -p ./backrest/ssh Next, generate a new SSH key pair specifically for Backrest. ```bash -ssh-keygen -t ed25519 -f ./backrest/ssh/id_rsa -C "backrest-backup-key" +ssh-keygen -t ed25519 -f ./backrest/ssh/id_ed25519 -C "backrest-backup-key" ``` When prompted for a passphrase, you can leave it empty by pressing Enter. Using a passphrase adds another layer of security but requires more complex setup to use with an automated tool like Backrest. @@ -40,7 +44,7 @@ Copy the public key to your remote server's `authorized_keys` file. The `ssh-cop ```bash # Replace your-username and example.com with your remote server's details -ssh-copy-id -i ./backrest/ssh/id_rsa.pub your-username@example.com +ssh-copy-id -i ./backrest/ssh/id_ed25519.pub your-username@example.com ``` ### Step 4: Create the SSH Config and Known Hosts Files @@ -53,7 +57,7 @@ cat > ./backrest/ssh/config << EOF Host backrest-remote HostName example.com User your-username - IdentityFile /root/.ssh/id_rsa + IdentityFile /root/.ssh/id_ed25519 Port 22 EOF @@ -65,7 +69,7 @@ ssh-keyscan -H example.com >> ./backrest/ssh/known_hosts - **`Host backrest-remote`**: This is a custom alias. You will use this name in the Backrest UI. - **`HostName`**: The actual IP address or hostname of your remote server. - **`User`**: The username on the remote server. -- **`IdentityFile`**: This **must be `/root/.ssh/id_rsa`**. This is the path *inside* the container where the key will be mounted. +- **`IdentityFile`**: This **must be `/root/.ssh/id_ed25519`**. This is the path *inside* the container where the key will be mounted. - **`Port`**: The SSH port of your remote server. ### Step 5: Set Secure Permissions @@ -85,7 +89,7 @@ Now, edit your `docker-compose.yml` to mount the `backrest/ssh` directory into t version: "3.8" services: backrest: - image: garethgeorge/backrest:latest + image: ghcr.io/garethgeorge/backrest:latest container_name: backrest # ... other configuration ... volumes: @@ -118,17 +122,17 @@ docker compose up -d --force-recreate - **Connection Errors:** First, test your SSH connection from the host machine to isolate issues. This command uses the exact same configuration files that the container will use. - ```bash host - # This command should connect without asking for a password + ```bash + # Run on the host. This command should connect without asking for a password. ssh -vF ./backrest/ssh/config -o UserKnownHostsFile=./backrest/ssh/known_hosts backrest-remote - # -o UserKnownHostsFile ensures that ssh uses ony the public keys passed to the backrest container instead of the global public keys of the host. + # -o UserKnownHostsFile ensures that ssh uses only the public keys passed to the backrest container instead of the global public keys of the host. ``` Attempt connection to the server from the container and compare the results : - ```bash host + ```bash # From the host open a bash terminal to the backrest container docker exec -it backrest bash ``` - ```bash container + ```bash # Attempt to connect to the server via ssh. If prompt abort the command, saved keys would be ephemeral. ssh -vF /root/.ssh/config backrest-remote ``` diff --git a/docs/src/docs/api.md b/docs/src/docs/api.md index fdf1cec6..1ec70b0f 100644 --- a/docs/src/docs/api.md +++ b/docs/src/docs/api.md @@ -8,6 +8,8 @@ All of Backrest's API endpoints are defined as a gRPC service and are exposed ov Only the APIs documented below are considered stable, other endpoints may be subject to change. ::: +The full (unstable) surface — including config management, snapshot listing, restore, and repo tasks — is defined in [service.proto](https://github.com/garethgeorge/backrest/blob/main/proto/v1/service.proto) and [operations.proto](https://github.com/garethgeorge/backrest/blob/main/proto/v1/operations.proto); any RPC there can be called the same way as the examples below, with no stability guarantees. + ### Backup API The backup API can be used to trigger execution of a plan e.g. diff --git a/docs/src/docs/configuration.md b/docs/src/docs/configuration.md new file mode 100644 index 00000000..466226aa --- /dev/null +++ b/docs/src/docs/configuration.md @@ -0,0 +1,120 @@ +# Configuration & Paths + +A reference for where Backrest keeps its files, the environment variables and flags it recognizes, and the shape of its configuration file. + +## File Locations + +| What | Linux / macOS | Windows | Docker (default) | +| --- | --- | --- | --- | +| Config file | `$XDG_CONFIG_HOME/backrest/config.json`, falling back to `~/.config/backrest/config.json` | `%APPDATA%\backrest\config.json` | `/config/config.json` | +| Data directory | `$XDG_DATA_HOME/backrest`, falling back to `~/.local/share/backrest` | `%APPDATA%\backrest\data` | `/data` | +| restic cache | `$XDG_CACHE_HOME` (restic's default rules otherwise) | restic default | `/cache` | +| SSH keys for SFTP | `/.backrest-ssh` | — | `/config/.backrest-ssh` | + +The **data directory** holds Backrest's operational state: the operation log (`oplog.sqlite`), task logs (`tasklogs`), Backrest's own process logs (`processlogs`), the JWT signing secret (`jwt-secret`), and, if no system restic is used, the managed restic binary (`restic-x.x.x`). + +::: tip What to back up +Rebuilding a Backrest install from scratch requires only the config file, which holds repository definitions, credentials, and plans; keep a copy somewhere safe, such as a password manager. The data directory holds operational history: losing it removes the UI history and statistics but does not affect backup data. +::: + +## Environment Variables and Flags + +Each setting can be provided as an environment variable or a command-line flag; **flags take precedence over environment variables**, which take precedence over defaults. + +| Environment variable | Flag | Default | Purpose | +| --- | --- | --- | --- | +| `BACKREST_PORT` | `--bind-address` | `127.0.0.1:9898` (Docker images: `0.0.0.0:9898`) | Address/port to serve on. A bare number like `9898` is treated as `:9898`. | +| `BACKREST_CONFIG` | `--config-file` | see table above | Path to `config.json` | +| `BACKREST_DATA` | `--data-dir` | see table above | Path to the data directory | +| `BACKREST_RESTIC_COMMAND` | `--restic-cmd` | Backrest-managed restic | Use a specific restic binary | +| `BACKREST_MULTIHOST_HEARTBEAT_INTERVAL` | `--multihost-heartbeat-interval` | `600s` | Keepalive interval for [multihost sync](/docs/multihost) connections (lower it below your reverse proxy's idle timeout if sync connections drop) | +| `XDG_CONFIG_HOME` / `XDG_DATA_HOME` / `XDG_CACHE_HOME` | — | platform defaults | Standard XDG overrides for the paths above | +| `TMPDIR` | — | system default | Temp space (the Docker images set `/tmp`) | +| `TZ` | — | system default | Timezone used by cron schedules with the Local clock — set this in Docker | + +Standalone flags: `--version` prints version and commit; `--install-deps-only` downloads restic and exits (used by the Docker image build); tray-enabled builds add `--tray` (macOS/Linux) and `--windows-tray`. + +Backrest also passes its process environment through to restic, so restic's own variables (`RESTIC_PASSWORD`, `AWS_ACCESS_KEY_ID`, `RCLONE_*`, etc.) work when set globally, though per-repository env vars in the config are usually the better place for credentials. See [Storage Backends](/guides/storage-backends) for precedence details. + +## The Config File + +Backrest stores all of its configuration (instance identity, repositories, plans, users, and sync settings) in a single JSON file. The UI is the intended way to edit it, but understanding its structure is useful for backups, audits, and occasional manual fixes. + +An annotated example (not every field shown; omitted fields take defaults): + +```json +{ + "modno": 42, // internal change counter, managed by Backrest + "version": 4, // config format version, managed by Backrest + "instance": "home-server", + "repos": [ + { + "id": "mydrive", + "uri": "s3:s3.amazonaws.com/my-bucket/backrest", + "guid": "...", // derived from the restic repo's identity, managed by Backrest + "password": "...", // repository encryption password (plaintext -- protect this file) + "env": ["AWS_ACCESS_KEY_ID=...", "AWS_SECRET_ACCESS_KEY=..."], + "flags": ["--limit-upload", "4000"], + "prunePolicy": { + "schedule": { "maxFrequencyDays": 30, "clock": "CLOCK_LAST_RUN_TIME" }, + "maxUnusedPercent": 25 + }, + "checkPolicy": { + "schedule": { "maxFrequencyDays": 30, "clock": "CLOCK_LAST_RUN_TIME" }, + "readDataSubsetPercent": 0 + }, + "autoUnlock": false, // remove stale repo locks automatically before operations + "autoInitialize": false, // initialize the repo if it does not exist yet + "commandPrefix": { // resource limits applied to restic (Unix) + "ioNice": "IO_BEST_EFFORT_LOW", + "cpuNice": "CPU_LOW" + }, + "hooks": [] // repo-level hooks, see the Hooks reference + } + ], + "plans": [ + { + "id": "mydrive-documents", + "repo": "mydrive", + "paths": ["/home/me/Documents"], + "excludes": ["*node_modules*"], + "iexcludes": [".cache"], // case-insensitive excludes + "schedule": { "cron": "0 2 * * *", "clock": "CLOCK_LOCAL" }, + "retention": { + "policyTimeBucketed": { "daily": 7, "weekly": 4, "monthly": 12 } + // or: "policyKeepLastN": 30 + // or: "policyKeepAll": true + }, + "backup_flags": ["--one-file-system"], + "skipIfUnchanged": true, + "hooks": [] + } + ], + "auth": { + "disabled": false, + "users": [{ "name": "me", "passwordBcrypt": "..." }] + }, + "sync": {} // multihost identity, peers, and permissions +} +``` + +Notes on specific fields: + +- **`schedule`** appears on plans and on prune/check/forget policies; it holds exactly one of `cron`, `maxFrequencyHours`, `maxFrequencyDays`, or `disabled: true`, plus a `clock` (`CLOCK_LOCAL`, `CLOCK_UTC`, or `CLOCK_LAST_RUN_TIME`). Semantics are covered in the [Scheduling guide](/guides/scheduling). +- **`retention`** holds exactly one policy variant. A repo may additionally define a scheduled `forgetPolicy` (`schedule` + `retention`), which **replaces all per-plan retention** for that repo — see [Retention & Repo Health](/guides/repo-health). +- **`env` and `flags`** support `${VAR}` expansion from Backrest's process environment, which is useful for keeping secrets out of the file itself. +- **`shared` / `originInstanceId`** on repos are managed by [multihost sync](/docs/multihost); repos pushed from another instance are not locally editable. + +## Editing the Config Directly + +Backrest owns this file while running: it validates the file on load and rewrites it on every change made in the UI. If you need to edit it by hand: + +1. Stop Backrest first, make your edits, then start it again. Edits made while Backrest is running can be overwritten. +2. Keep the JSON valid, and do not modify the `modno`, `version`, or `guid` fields, which are managed by Backrest. +3. If the config fails validation, Backrest reports the error at startup; check the process logs. + +The most common manual edit is resetting a lost password: delete the `"users"` key under `"auth"` and restart, and first-launch user creation will run again. See [Authentication & Security](/guides/security) for details. + +::: warning +`config.json` contains repository passwords and storage credentials in plaintext (Backrest writes it with owner-only permissions). Treat backups of this file with the same care as the passwords themselves. +::: diff --git a/docs/src/docs/hooks.md b/docs/src/docs/hooks.md index c9ab9702..7ccc0cfa 100644 --- a/docs/src/docs/hooks.md +++ b/docs/src/docs/hooks.md @@ -1,6 +1,8 @@ # Hooks -Hooks in Backrest allow you to respond to various operation lifecycle events, enabling automation and monitoring of your backup operations. This document explains how to configure and use hooks effectively. +Hooks in Backrest allow you to respond to various operation lifecycle events, enabling automation and monitoring of your backup operations. This page is the reference for hook conditions, actions, error policies, and templates. For a task-oriented walkthrough of setting up notifications, see the [Notifications guide](/guides/notifications). + +Hooks can be attached to a **repository** (fires for all activity in that repo, including `_system_` maintenance) or to a **plan** (fires only for that plan's operations). For each event, repository hooks run before plan hooks, and each hook fires at most once per event — its first matching condition wins. ## Event Types @@ -11,7 +13,8 @@ Hooks can be triggered by the following events: - `CONDITION_SNAPSHOT_END`: Triggered when a backup operation completes (regardless of success/failure) - `CONDITION_SNAPSHOT_SUCCESS`: Triggered when a backup operation completes successfully - `CONDITION_SNAPSHOT_ERROR`: Triggered when a backup operation fails -- `CONDITION_SNAPSHOT_WARNING`: Triggered when a backup operation encounters non-fatal issues +- `CONDITION_SNAPSHOT_WARNING`: Triggered when a backup operation encounters non-fatal issues (e.g. some files could not be read; a snapshot is still created) +- `CONDITION_SNAPSHOT_SKIPPED`: Triggered when a backup is skipped because nothing changed (only with the plan's *skip if unchanged* option enabled) ### Prune Events - `CONDITION_PRUNE_START`: Triggered when a prune operation begins @@ -40,9 +43,15 @@ Backrest supports multiple notification services for hook delivery: | Discord | Send notifications to Discord channels | [Discord Webhooks Guide](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks) | | Slack | Send notifications to Slack channels | [Slack Webhooks Guide](https://api.slack.com/messaging/webhooks) | | Gotify | Send notifications via Gotify server | [Gotify Documentation](https://github.com/gotify/server) | -| Shoutrrr | Multi-provider notification service | [Shoutrrr Documentation](https://containrrr.dev/shoutrrr/v0.8/) | +| Telegram | Send messages via a Telegram bot (bot token + chat ID) | [Telegram Bot API](https://core.telegram.org/bots) | +| Shoutrrr | Multi-provider notification service (ntfy, Pushover, email, and more) | [Shoutrrr Documentation](https://containrrr.dev/shoutrrr/v0.8/) | | Healthchecks | Ping Healthchecks.io monitoring URLs | [Healthchecks API](https://healthchecks.io/docs/http_api/) | -| Command | Execute custom commands | See [command cookbook](../cookbooks/command-hook-examples) | +| Webhook | Send the rendered template to any HTTP endpoint (GET or POST) | — | +| Command | Execute custom shell commands | See [command cookbook](/cookbooks/command-hook-examples) | + +::: warning Command hooks and the scratch Docker image +Command hooks run through a shell, which the minimal `ghcr.io/garethgeorge/backrest:scratch` image does not include — use the default (alpine-based) `latest` image if you rely on command hooks. +::: ### Healthchecks.io Integration @@ -58,11 +67,16 @@ It also sends the formatted template summary as the HTTP POST body in plain text ## Error Handling -Command hooks support specific error behaviors that determine how Backrest responds to hook failures: +Every hook has an **error behavior** that determines how Backrest responds if the hook itself fails (a non-zero exit code for command hooks, or a delivery failure for notifications): -- `ON_ERROR_IGNORE`: Continue execution despite hook failure -- `ON_ERROR_CANCEL`: Stop the operation but don't trigger error handlers -- `ON_ERROR_FATAL`: Stop the operation and trigger error handler hooks +- `ON_ERROR_IGNORE`: Continue execution despite the hook failure +- `ON_ERROR_CANCEL`: Stop the operation, marking it cancelled. Error-condition hooks are *not* triggered, which makes this suitable for pre-backup checks that should skip a run without raising an error (only meaningful on `*_START` conditions). +- `ON_ERROR_FATAL`: Stop the operation, marking it failed. Error-condition hooks *are* triggered (only meaningful on `*_START` conditions). +- `ON_ERROR_RETRY_1MINUTE`: Retry the hook every minute until it succeeds +- `ON_ERROR_RETRY_10MINUTES`: Retry every 10 minutes +- `ON_ERROR_RETRY_EXPONENTIAL_BACKOFF`: Retry with doubling delays (10s, 20s, 40s, …) capped at 1 hour + +While a hook is retrying, the operation that triggered it stays pending; retry policies are a good fit for notification hooks that should survive transient network failures. ## Template System @@ -129,3 +143,9 @@ Backup Statistics: {{ end }} ``` + +## See Also + +- [Notifications guide](/guides/notifications) — step-by-step setup for the services above +- [Command hook examples](/cookbooks/command-hook-examples) — pre-backup checks, filesystem snapshots, desktop notifications +- [Slack Block Kit examples](/cookbooks/slack-hook-build-kit-examples) — richly formatted Slack messages diff --git a/docs/src/docs/multihost.md b/docs/src/docs/multihost.md index 116ae74b..63bf4f77 100644 --- a/docs/src/docs/multihost.md +++ b/docs/src/docs/multihost.md @@ -157,7 +157,11 @@ backrest.example.com { Replace `127.0.0.1:9898` with your Backrest instance's bind address. Any path other than `/v1sync.BackrestSyncService/*` will return 404, keeping the UI and admin API off the public internet. -If you also want to expose the UI publicly (not recommended without additional auth in front), add a second `reverse_proxy` block without the path matcher — but be aware this also exposes the admin API. +If you also want to expose the UI publicly (not recommended without additional auth in front), add a second `reverse_proxy` block without the path matcher — but be aware this also exposes the admin API. For Traefik and nginx configurations, and for proxying the full UI, see the [Reverse Proxies cookbook](/cookbooks/reverse-proxy-examples). + +::: tip Keepalives through proxies +Sync connections send a heartbeat every 10 minutes by default. If a proxy between peers enforces a shorter idle timeout, either raise the proxy's timeout or lower the heartbeat via `BACKREST_MULTIHOST_HEARTBEAT_INTERVAL` (see [Configuration & Paths](/docs/configuration)). +::: ## Troubleshooting diff --git a/docs/src/docs/operations.md b/docs/src/docs/operations.md index ae41a0db..fe8721e2 100644 --- a/docs/src/docs/operations.md +++ b/docs/src/docs/operations.md @@ -1,130 +1,104 @@ -# Operations Guide +# Operational Model -This guide details the core operations available in Backrest and how to configure them effectively. "Operations" in Backrest refer to any task that interacts with your repository, such as creating backups, managing retention, verifying integrity, or restoring files. +This page is a reference for how Backrest works internally: the orchestrator, its task queue, the operation lifecycle, and restic integration. This information is not required for everyday use, but it is helpful for interpreting the operation history and understanding unexpected behavior. + +For task-oriented guidance, see [Scheduling Backups](/guides/scheduling) and [Retention & Repo Health](/guides/repo-health). + +## The Orchestrator + +At Backrest's core is an orchestrator that executes tasks from a **time-ordered priority queue, one at a time**. Serial execution is deliberate: restic repositories use locking, and running operations sequentially avoids lock contention and keeps resource usage predictable. + +Each task carries a scheduled run time; the queue dequeues whatever is due next. When multiple tasks are due at the same moment, priority breaks the tie (highest first): + +| Priority | Task | Consequence | +| --- | --- | --- | +| Interactive | Anything you trigger from the UI | UI-triggered tasks run ahead of scheduled work | +| Prune | Scheduled prune | Runs before check when both are due | +| Check | Scheduled check | Verifies the post-prune state | +| Index snapshots | Snapshot indexing | | +| Forget | Post-backup retention | | +| Default | Backups and most tasks | | +| Stats | Repository statistics | Runs when nothing else is due | + +After a recurring task finishes, the orchestrator computes its next run time from its [schedule](/guides/scheduling) and re-enqueues it. One-off tasks (a manual backup, a restore) run once. + +On startup and after **every configuration change**, the queue is rebuilt from scratch: one backup task per plan, plus prune, check, and repo-level forget tasks per repository, plus a garbage-collection task. A watchdog also monitors for system clock jumps (sleep, hibernation, NTP corrections) and rebuilds schedules if the clock drifts more than ~30 seconds from expectations. + +## Task Types + +| Task | Trigger | What it runs | +| --- | --- | --- | +| `backup` | Plan schedule, or **Backup Now** | `restic backup` with the plan's paths, excludes, and flags | +| `forget` | Automatically after each successful backup (per-plan retention) | `restic forget` scoped to the plan's snapshots | +| `scheduled_forget` | Repo-level forget policy schedule | `restic forget` across the repository, grouped by tags | +| `prune` | Repo prune policy schedule, or UI button | `restic prune` | +| `check` | Repo check policy schedule, or UI button | `restic check` | +| `index_snapshots` | After backups; repo added; UI button | `restic snapshots` — syncs Backrest's view of the repo | +| `restore` | UI restore action | `restic restore` | +| `stats` | At most every 24h; after prune and scheduled forget | `restic stats` — feeds the storage graphs | +| `run_command` | UI **Run Command** | An arbitrary restic command you type | +| `hook` | Fired by conditions on other operations | Your hook's action (command, notification, …) | +| `collect_garbage` | Startup, then every 24h | Internal cleanup of Backrest's operation log (never restic data) | + +Repo-level tasks (`prune`, `check`, `scheduled_forget`, and friends) are displayed under the synthetic **`_system_`** plan, since they belong to the repository rather than to any plan you created. + +## Operation Lifecycle + +Every task run is recorded as an **operation** in Backrest's operation log (the *oplog*), which is what the UI's history tree renders. Operations move through: + +``` +PENDING ──► INPROGRESS ──► SUCCESS + ├──► WARNING (completed with caveats) + ├──► ERROR + └──► USER_CANCELLED / SYSTEM_CANCELLED +``` + +Notes on specific statuses: + +- **WARNING on backups indicates a partial backup**: the snapshot was created, but some files could not be read (typically due to permissions or file locks). Follow-up tasks (forget, indexing) still run. The operation log lists the affected files. +- **After a crash or hard reboot**, any operation that was INPROGRESS is marked ERROR at startup, since Backrest cannot determine how far it progressed. Stale PENDING entries are cleared and rescheduled. An error block that appears immediately after a reboot usually reflects this recovery behavior rather than a new failure. +- **Cancellation**: cancelling a queued operation removes it; cancelling a running one interrupts the underlying restic process. Repo-level scheduled forgets also cancel *themselves* (SYSTEM_CANCELLED) when there's nothing new to do. +- **Logs**: each operation stores a summary of restic's output inline (truncated for very large outputs) and full logs are viewable from the operation's detail view while retained. + +### Operation Log Retention + +The `collect_garbage` task keeps the oplog bounded so the UI stays fast. Old operation records age out on type-specific rules (long-lived history for prune/check/stats, shorter for routine forgets), and records tied to snapshots that have been forgotten are cleaned up. This is Backrest-side bookkeeping only; it never deletes backup data. + +## How Backups Are Tagged + +Every snapshot Backrest creates carries two restic tags: + +- `plan:` — which plan created it +- `created-by:` — which Backrest installation created it + +These tags are how retention stays scoped: a plan's forget only touches snapshots with its own plan/instance tags, so multiple plans, multiple machines, and even manual restic CLI snapshots coexist safely in one repository. Backups also run with `--exclude-caches` (skipping directories marked with `CACHEDIR.TAG`) and pass the previous snapshot as `--parent` for faster change detection. + +## Hook Execution + +Operations fire [hook](/docs/hooks) conditions at lifecycle points (start, success, error, …). For each event, Backrest evaluates repository hooks first, then plan hooks; each hook runs at most once per event (its first matching condition wins) and executes as its own `hook` operation, visible in the history under the operation that triggered it. + +A failing hook applies its **error policy**: ignore, cancel the parent operation, mark it fatal, or retry (fixed 1‑minute/10‑minute delays, or exponential backoff capped at an hour). Retrying hooks keep the parent operation pending until they resolve. ## Restic Integration -Backrest executes operations through the [restic](https://restic.net) backup tool. Each operation maps to specific restic commands with additional functionality provided by Backrest. - ### Binary Management -- **Location**: Backrest searches for the restic binary in the following order: - 1. Data directory (typically `~/.local/share/backrest`) - 2. `/bin/` directory - 3. The system `$PATH` -- **Version Requirement**: Backrest is only tested against the latest version of restic. It will selectively reject outdated versions. -- **Auto-download**: If no valid binary is found, Backrest downloads a verified version from [GitHub releases](https://github.com/restic/restic/releases). -- **Verification**: Downloads are verified using SHA256 checksums signed by restic maintainers. -- **Override**: Set `BACKREST_RESTIC_COMMAND` environment variable to use a custom restic binary. + +Backrest pins a specific restic version per release (currently 0.19.x) and resolves the binary in this order: + +1. The `--restic-cmd` flag, if set. +2. The `BACKREST_RESTIC_COMMAND` environment variable, if set. +3. A `restic` on the system `$PATH`, if its version is compatible. +4. Otherwise, Backrest downloads its pinned version from restic's GitHub releases into the data directory, verifying the SHA256 checksum against the restic maintainers' GPG-signed manifest, and keeps it updated across Backrest upgrades. ### Command Execution -- **Environment**: Repository-specific environment variables are injected -- **Flags**: Repository-configured flags are appended to commands -- **Logging**: - - Error logs: Last ~500 bytes (split between start/end if longer) - - Full logs: Available via **[View Logs]** in the UI, truncated to 32KB (split if longer) -::: info -If an operation fails, you can always find the full diagnostic logs in the Backrest UI by clicking on the specific operation block in the history tree. -::: +For every restic invocation, Backrest injects the repository's environment variables (credentials, etc.), appends the repository's extra flags, and applies the repository's command prefix settings (CPU/IO niceness on Unix systems, useful for keeping background backups from competing with foreground work). `${VAR}` references in the URI, env vars, and flags are expanded from Backrest's own process environment. -## Scheduling System +If an operation fails, click its block in the history tree to view the full diagnostic log. -Backrest provides flexible scheduling options for all operations through policies and clocks. +## See Also -### Schedule Policies - -| Policy | Description | Use Case | -| -------------- | ------------------------------- | --------------------------------------------------------- | -| Disabled | Operation will not run | Temporarily disable operations | -| Cron | Standard cron expression timing | Precise scheduling (e.g., `0 0 * * *` for daily midnight) | -| Interval Days | Run every N days | Regular daily+ intervals | -| Interval Hours | Run every N hours | Regular sub-daily intervals | - -### Schedule Clocks - -| Clock | Description | Best For | -| ------------- | ------------------------------ | --------------------------------------- | -| Local | Local timezone wall-clock | Frequent operations (hourly+) | -| UTC | UTC timezone wall-clock | Cross-timezone coordination | -| Last Run Time | Relative to previous execution | Infrequent operations, preventing skips | - -::: info -**Scheduling Best Practices** -- **Backup Operations** (Plan Settings): - - Hourly or more frequent: Use "Local" clock - - Daily or less frequent: Use "Last Run Time" clock -- **Prune/Check Operations** (Repo Settings): - - Run infrequently (e.g., monthly) - - Use "Last Run Time" clock to prevent skips -::: - -## Operation Types - -### 💾 Backup -[Restic Documentation](https://restic.readthedocs.io/en/latest/040_backup.html) - -Creates snapshots of your data using `restic backup`. - -**Process Flow:** -1. **Start** - - Triggers `CONDITION_SNAPSHOT_START` hooks - - Applies hook failure policies if needed -2. **Execution** - - Runs `restic backup` - - Tags snapshot with `plan:{PLAN_ID}` and `created-by:{INSTANCE_ID}` -3. **Completion** - - Records operation metadata (files, bytes, snapshot ID) - - Triggers appropriate hooks: - - Error: `CONDITION_SNAPSHOT_ERROR` - - Success: `CONDITION_SNAPSHOT_SUCCESS` - - In both cases: `CONDITION_SNAPSHOT_END` -4. **Post-processing** - - Runs forget operation if retention policy exists - -**Snapshot Tags:** -- `plan:{PLAN_ID}`: Groups snapshots by backup plan -- `created-by:{INSTANCE_ID}`: Identifies creating Backrest instance - -### 🕰️ Forget -[Restic Documentation](https://restic.readthedocs.io/en/latest/060_forget.html) - -Manages snapshot retention using `restic forget --tag plan:{PLAN_ID}`. - -**Retention Policies:** -- **By Count**: `--keep-last {COUNT}` -- **By Time Period**: `--keep-{hourly,daily,weekly,monthly,yearly} {COUNT}` - -### ✂️ Prune -[Restic Documentation](https://restic.readthedocs.io/en/latest/060_forget.html#removing-unreferenced-data) - -Removes unreferenced data using `restic prune`. Like Backup, Prune operations trigger their respective lifecycle hooks (e.g., `CONDITION_PRUNE_START`). - -**Configuration:** -- Scheduled in repo settings -- Appears under `_system_` plan -- **Parameters:** - - Schedule timing - - Max unused percent (controls repacking threshold) - -::: info -**Optimization Tips:** -- Run infrequently (monthly recommended) -- Use higher max unused percent (5-10%) to reduce repacking -- Consider storage costs vs. cleanup frequency -::: - -### 🔍 Check -[Restic Documentation](https://restic.readthedocs.io/en/latest/080_check.html) - -Verifies repository integrity using `restic check`. - -**Configuration:** -- Scheduled in repo settings -- Appears under `_system_` plan -- **Parameters:** - - Schedule timing - - Read data percentage - -::: warning -A value of 100% for *read data%* will read/download every pack file in your repository. This can be very slow and, if your provider bills for egress bandwidth, can be expensive. It is recommended to set this to 0% or a low value (e.g. 10%) for most use cases. -::: \ No newline at end of file +- [Scheduling Backups](/guides/scheduling) — schedule types and clock semantics +- [Retention & Repo Health](/guides/repo-health) — forget/prune/check policies and interactions +- [Configuration & Paths](/docs/configuration) — where the oplog, logs, and config live on disk +- [Hooks](/docs/hooks) — conditions, actions, and error policies in full diff --git a/docs/src/guides/notifications.md b/docs/src/guides/notifications.md new file mode 100644 index 00000000..fb284605 --- /dev/null +++ b/docs/src/guides/notifications.md @@ -0,0 +1,108 @@ +# Notifications + +Backrest can notify you when backups succeed, fail, or need attention. Notifications are built on the [hooks system](/docs/hooks): a hook pairs one or more trigger *conditions* (e.g. "snapshot succeeded") with an *action* (e.g. "post to Discord"). This guide walks through setting up the most common notification services; the [Hooks reference](/docs/hooks) documents every condition, error policy, and template variable exhaustively. + +## How Notifications Work + +Hooks can be attached to a **repository** (fires for every plan using that repo, plus repo-level operations like prune and check) or to an individual **plan** (fires only for that plan's operations). You configure them in the same modal used to edit the repo or plan: scroll to the **Hooks** section, add a hook, and pick an action type: + +- **Discord**, **Slack**, **Gotify**, **Telegram**, **Healthchecks** — first-class integrations, each needing only a URL/token. +- **Shoutrrr** — a multi-provider gateway covering dozens of services (ntfy, Pushover, email, Matrix, and more). +- **Webhook** and **Command** — generic options for services without a dedicated integration; see the [command hook cookbook](/cookbooks/command-hook-examples). + +Every notification body is rendered from a Go template. If you leave the template field empty, Backrest uses {{ .Summary }}, a sensible default that includes the task name, event, and (for backups) snapshot statistics. + +Each hook execution is recorded as an operation in the UI, so you can see when hooks ran and read their logs if delivery fails. + +## Choosing Conditions + +A good baseline for a notification hook is: + +- `CONDITION_ANY_ERROR` — any operation on this plan/repo failed. This is the most useful condition; if you configure only one hook, use this. +- `CONDITION_SNAPSHOT_SUCCESS` (or `CONDITION_SNAPSHOT_END` to also cover failures) — confirmation that backups are running as expected. + +::: tip Avoid notification fatigue +Skip `CONDITION_SNAPSHOT_START` for chat notifications; on an hourly schedule it doubles message volume without adding useful information. Start conditions are mainly useful for command hooks (e.g. mounting a drive before backup) and for Healthchecks-style dead man's switches, which measure the time between start and success pings. +::: + +`CONDITION_SNAPSHOT_WARNING` is also worth adding: it fires when a backup completes but some files could not be read (a *partial* backup), which `ANY_ERROR` does not cover. + +## Walkthrough: Discord + +1. In Discord, open the target channel's settings → **Integrations** → **Webhooks** → **New Webhook**, and copy the webhook URL. +2. In Backrest, edit the plan you want notifications for (or the repo, to cover all its plans) and scroll to the **Hooks** section. +3. Add a hook and choose the **Discord** action type. +4. Paste the webhook URL. +5. Select conditions: `CONDITION_ANY_ERROR` and `CONDITION_SNAPSHOT_SUCCESS`. +6. Leave the template empty to use the default summary, then submit the modal to save. + +Hooks section of the plan modal with a Discord hook configured + +Trigger a manual backup with **Backup Now** — you should see a message in your Discord channel when the snapshot completes. + +## Quick Recipes for Other Services + +### Gotify + +Choose the **Gotify** action and provide the base URL of your Gotify server (e.g. `https://gotify.example.com`) and an application token (create one under **Apps** in the Gotify UI). You can optionally set a title template and a Gotify priority level to control how intrusively the notification is delivered. + +### Telegram + +1. Create a bot by messaging [@BotFather](https://t.me/botfather) and copy the bot token. +2. Start a chat with your bot (or add it to a group), then find the chat ID. For a direct chat, messaging [@userinfobot](https://t.me/userinfobot) is a quick way to get your numeric ID. +3. Choose the **Telegram** action in Backrest and enter the bot token and chat ID. + +::: info +Telegram messages are sent with HTML parse mode, so literal `<` and `>` characters in a custom template must be escaped as `<` and `>`. +::: + +### ntfy, Pushover, Email, and More via Shoutrrr + +The **Shoutrrr** action delivers to any service supported by the [Shoutrrr](https://containrrr.dev/shoutrrr/) notification library, including ntfy, Pushover, SMTP email, Matrix, Pushbullet, and many others. Configuration is a single service URL such as `ntfy://...` or `smtp://...`; see the Shoutrrr documentation for the URL syntax of each service. + +### Healthchecks (Dead Man's Switch) + +Push notifications cannot alert you when backups stop happening entirely (host offline, Backrest not running). For that case, use a service that expects periodic pings and alerts on their absence. The **Healthchecks** action integrates natively with [Healthchecks.io](https://healthchecks.io/) or a self-hosted instance: + +1. Create a check in Healthchecks and copy its ping URL. +2. Add a **Healthchecks** hook with that URL and select `CONDITION_SNAPSHOT_START`, `CONDITION_SNAPSHOT_SUCCESS`, and `CONDITION_SNAPSHOT_ERROR`. + +Backrest automatically appends the right endpoint per event (`/start` for start events, `/fail` for errors, the base URL for success) and sends the rendered summary as the ping body, so error details are readable in the Healthchecks dashboard. See the [Hooks reference](/docs/hooks#healthchecks-io-integration) for details. + +::: info +The [command hook cookbook](/cookbooks/command-hook-examples) shows an equivalent `curl`-based approach; that is the manual alternative for services without a native hook type. +::: + +### Slack + +Choose the **Slack** action and paste an [incoming webhook URL](https://api.slack.com/messaging/webhooks). For richly formatted messages using Slack's Block Kit layout, see the [Slack hook cookbook](/cookbooks/slack-hook-build-kit-examples). + +## Testing Your Hooks + +The simplest test is to click **Backup Now** on a plan with a `CONDITION_SNAPSHOT_SUCCESS` (or `_END`) hook attached. Hook executions appear as operations in the operation list. If a notification does not arrive, open the hook operation there to read its error output; common causes are a mistyped webhook URL or a template syntax error. + +## Customizing Messages + +Templates use Go template syntax. Commonly used variables: + +| Variable | Meaning | +| --- | --- | +| {{ .Summary }} | The full default message — a good starting point | +| {{ .Task }} | Name of the task that fired the hook | +| {{ .Error }} | Error message (empty on success) | +| {{ .FormatDuration .Duration }} | How long the operation took | +| {{ .SnapshotStats }} | Backup statistics (files processed, data added, ...) | + +For example, a compact success/failure template: + +
+ +```text +{{ if .Error }}❗ Backrest: {{ .Task }} failed: {{ .Error }} +{{ else }}✅ Backrest: {{ .Task }} finished in {{ .FormatDuration .Duration }} +{{ if .SnapshotStats }}Added {{ .FormatSizeBytes .SnapshotStats.DataAdded }} in snapshot {{ .SnapshotId }}{{ end }} +{{ end }} +``` +
+ +See the [Hooks reference](/docs/hooks#template-system) for the complete list of variables and helper functions, and the [error handling policies](/docs/hooks#error-handling) that control what happens when a hook itself fails. diff --git a/docs/src/guides/repo-health.md b/docs/src/guides/repo-health.md new file mode 100644 index 00000000..557fe798 --- /dev/null +++ b/docs/src/guides/repo-health.md @@ -0,0 +1,92 @@ +# Retention & Repo Health + +Restic repositories need periodic maintenance: removing snapshots that are no longer wanted, reclaiming unused storage, and verifying data integrity. Backrest automates the three restic operations responsible for this. This guide explains what each one does and, in particular, how they interact. + +## The Three-Stage Pipeline + +| Stage | Operation | What it does | What it does *not* do | +| --- | --- | --- | --- | +| 1 | **Forget** | Applies your retention policy: removes aged-out *snapshot records* | Doesn't delete file data or free space | +| 2 | **Prune** | Deletes data chunks no remaining snapshot references, repacking as needed | Doesn't decide *which* snapshots to keep | +| 3 | **Check** | Verifies repository structure (and optionally re-reads data) | Doesn't fix or clean anything | + +Note that storage usage does not decrease when forget runs. Space is only reclaimed when prune runs. + +## Retention Policies + +Each plan has a retention policy, applied automatically by a **forget** operation after every successful backup that produces a snapshot: + +- **Keep last N** — the N most recent snapshots are kept. +- **Time-bucketed** — keep the latest snapshot in each of the last N hourly, daily, weekly, monthly, and yearly buckets (any subset; maps directly to restic's `--keep-hourly`, `--keep-daily`, etc., and can be combined with a keep-last-N floor). This keeps frequent recent snapshots and progressively fewer older ones. +- **Keep all** — no forget is ever scheduled; you manage snapshot lifecycle manually (or via the restic CLI). + +For example, a time-bucketed policy of *7 daily + 4 weekly + 12 monthly* lets you restore yesterday's version of a file, any day from the past week, any week from the past month, and any month from the past year. Because restic deduplicates data across snapshots, storage growth under a policy like this levels off over time. + +Retention policy form with count-based, time-bucketed, and none options + +### Retention Is Scoped Per Plan + +Backrest tags every snapshot with `plan:` and `created-by:`, and each plan's forget only considers snapshots carrying its own tags. Multiple plans, and multiple machines, can therefore share one repository without their retention policies interfering with each other's snapshots, or with snapshots you create manually with the restic CLI. + +### Repository-Level Forget (Override) + +Repositories can optionally define their own **forget policy with a schedule**. This is for setups where you want retention applied uniformly across everything in the repository on a fixed cadence, rather than plan-by-plan after each backup. + +::: warning A repo-level forget schedule replaces per-plan retention +If a repository has a scheduled forget policy, Backrest stops running per-plan forgets for that repository entirely; the repo-level policy is applied to all snapshots (grouped by tags) on its schedule instead. The two modes are mutually exclusive. +::: + +Repo-level forget runs appear under the `_system_` plan and skip themselves (recorded as cancelled) when no new backups have happened since the last run. + +## Prune + +Prune scans the repository for data no snapshot references and deletes it, repacking partially-used pack files where worthwhile. It's configured in **repository settings**: + +Repository scheduling section with prune and check policy configuration + +- **Schedule** — monthly is typical. Prune is I/O-intensive and, on cloud storage, bandwidth-intensive, so running it more frequently provides little benefit. +- **Max unused percent** (default 25%) — how much unreferenced data restic may leave behind to avoid expensive repacking. Higher values make prunes faster and cheaper but leave more unused storage; lower values reclaim storage more tightly at the cost of more repacking (and, on cloud providers, more download/upload). A max-unused-bytes form is also available if you prefer an absolute cap. + +::: tip Cost tuning for cloud storage +Repacking downloads data before re-uploading it. If your provider bills for egress, a higher max-unused setting (10–25%) usually costs less overall than aggressive pruning. +::: + +## Check + +Check verifies repository integrity so that corruption is detected before a restore is needed. It is configured in **repository settings**: + +- **Structure only** (default): verifies the repository's internal consistency (indexes, pack metadata) without downloading file data. This mode is inexpensive and suitable for monthly runs on any repository. +- **Read data subset (%)**: additionally downloads and cryptographically verifies that percentage of the actual data, sampling different packs each run. Over many runs, a small percentage accumulates into broad coverage. + +::: warning +Setting read data to 100% downloads the entire repository on every check. On egress-billed cloud storage this can be expensive, and on large repositories it is slow. For most setups, 0% (structure only) or a low single-digit percentage is sufficient. +::: + +When prune and check are both due, Backrest orders them so that check runs after prune and verifies the repository state that prune leaves behind. + +## Stats and Internal Housekeeping + +Two more operations appear in the history that require no configuration: + +- **Stats** — records repository size and snapshot counts (at most once per day, plus after prune and repo-level forget). This feeds the storage graphs in the repository view's Stats tab. +- **Collect garbage** — trims Backrest's own *operation history* (the oplog) so the UI stays fast: old operation records age out on type-specific retention rules, and records for forgotten snapshots are cleaned up. This never touches restic data; it is internal bookkeeping only. + +## A Recommended Baseline + +A reasonable starting point for a personal server or homelab: + +| Setting | Value | +| --- | --- | +| Plan retention | Time-bucketed: 7 daily, 4 weekly, 12 monthly | +| Prune schedule | Every 30 days, Last Run Time clock | +| Prune max unused | 10–25% (higher if egress is billed) | +| Check schedule | Every 30 days, Last Run Time clock | +| Check read data | 0% (structure only); a few % if you want deep verification | + +Consider also adding a [notification hook](/guides/notifications) on error conditions so that maintenance failures do not go unnoticed. + +## See Also + +- [Scheduling Backups](/guides/scheduling) — schedule types and clock semantics used by all of the above +- [Operational Model](/docs/operations) — how these tasks queue, prioritize, and record their results +- Restic's own docs on [forget & prune](https://restic.readthedocs.io/en/latest/060_forget.html) and [check](https://restic.readthedocs.io/en/latest/080_check.html) diff --git a/docs/src/guides/scheduling.md b/docs/src/guides/scheduling.md new file mode 100644 index 00000000..36746a20 --- /dev/null +++ b/docs/src/guides/scheduling.md @@ -0,0 +1,91 @@ +# Scheduling Backups + +Every recurring activity in Backrest (plan backups, and per-repository prune, check, and forget) is driven by the same schedule system. This guide explains the schedule types, the clock options, and how Backrest behaves when a machine is off or asleep. + +## Schedule Types + +| Type | Meaning | Example | +| --- | --- | --- | +| **Cron** | Fire at wall-clock times matching a cron expression | `0 2 * * *` — daily at 2:00 AM | +| **Interval (hours)** | Fire every N hours (N ≥ 1) | every 4 hours | +| **Interval (days)** | Fire every N days (N ≥ 1) | every 2 days | +| **Disabled** | Never fire | pause a plan without deleting it | + +Cron expressions use the standard five fields (minute, hour, day-of-month, month, day-of-week). [crontab.guru](https://crontab.guru/) is useful for validating expressions. + +Schedule form showing the schedule type options and reference clock selection + +``` +0 2 * * * daily at 02:00 +*/30 * * * * every 30 minutes +0 3 * * 1 Mondays at 03:00 +0 1 1 * * first of the month at 01:00 +``` + +## Choosing a Clock + +Each schedule also has a **clock**, which determines the reference point for computing the next run: + +| Clock | Next run is computed from | Behavior | +| --- | --- | --- | +| **Local** (default) | Current time, local timezone | Cron fires at local wall-clock times. Intervals fire at *now + interval*, re-evaluated after each run. | +| **UTC** | Current time, UTC | Same as Local but cron times are interpreted in UTC. | +| **Last Run Time** | When the operation last ran | Intervals fire at *last run + interval* — a fixed cadence that "catches up" after downtime. | + +The clocks differ in how they handle interval schedules: + +- With **Local/UTC**, "every 24 hours" means 24 hours after Backrest last *evaluated* the schedule. If the machine was asleep at the scheduled moment, the interval effectively restarts when it wakes. +- With **Last Run Time**, "every 24 hours" means 24 hours after the operation last *completed*. If the machine wakes up late, the run is already overdue and fires promptly. + +::: tip Recommendations +- **Always-on machines** (servers, NAS): cron with the **Local** clock — predictable wall-clock timing, easy to place backups in quiet hours. +- **Sometimes-off machines** (laptops, desktops): interval with the **Last Run Time** clock — an overdue backup runs soon after the machine wakes, regardless of when it was last on. +- **Infrequent maintenance** (prune/check, monthly): **Last Run Time**, so a missed window does not delay the operation by a full additional period. +::: + +::: info First run +A newly created interval schedule with the Last Run Time clock has no "last run" yet, so Backrest uses the creation time as the reference: the first backup fires one interval after the plan is created. Use **Backup Now** to run one immediately. +::: + +## When the Machine Is Off or Asleep + +Backrest maintains a queue of upcoming tasks, each with a scheduled time. The following behaviors apply: + +- **Missed cron fires are not replayed.** If the machine is off at 2 AM, a `0 2 * * *` backup simply waits for the next 2 AM. For machines with unpredictable uptime, prefer interval + Last Run Time. +- **Clock jumps are handled.** Backrest watches for the system clock drifting from its expected timeline (sleep/hibernate, NTP corrections) and recomputes all scheduled tasks when it detects a jump of more than ~30 seconds. +- **Config changes reschedule everything.** Editing any plan or repository resets the queue and recomputes every schedule. + +## Scheduling Health Operations + +Prune, check, and repository-level forget schedules live in **repository settings** (not on plans), because they maintain the repository as a whole. Their operations appear in the UI under the synthetic `_system_` plan. + +Two orchestration behaviors are relevant here: + +- Operations on a repository run one at a time; a long backup and a prune queue behind each other rather than conflicting over repository locks. +- When prune and check are due together, prune runs first, so check verifies the repository's post-cleanup state. + +See [Retention & Repo Health](/guides/repo-health) for what these operations do and how to pick their policies. + +## Skip If Unchanged + +Plans have a **Skip if unchanged** option: when enabled, a scheduled backup that finds no file changes produces no new snapshot. The run is recorded as *skipped* (firing the `CONDITION_SNAPSHOT_SKIPPED` hook rather than success/error hooks), and follow-up forget and indexing work is skipped as well. This prevents frequent schedules on mostly-idle machines from accumulating identical snapshots. + +## Recipes + +**Nightly backup at 2 AM, server:** +- Schedule: cron `0 2 * * *`, clock **Local** + +**Laptop that should back up roughly hourly whenever it's awake:** +- Schedule: interval **1 hour**, clock **Last Run Time**, with **Skip if unchanged** enabled + +**Weekend-only backups:** +- Schedule: cron `0 3 * * 6,0` (Sat & Sun at 3 AM), clock **Local** + +**Monthly maintenance that never silently skips a month:** +- Prune: interval **30 days**, clock **Last Run Time** +- Check: interval **30 days**, clock **Last Run Time** (queued after prune automatically when both are due) + +## See Also + +- [Retention & Repo Health](/guides/repo-health) — what forget/prune/check actually do +- [Operational Model](/docs/operations) — the task queue, priorities, and lifecycle behind the schedules diff --git a/docs/src/guides/security.md b/docs/src/guides/security.md new file mode 100644 index 00000000..73eb0e53 --- /dev/null +++ b/docs/src/guides/security.md @@ -0,0 +1,74 @@ +# Authentication & Security + +This page describes Backrest's security model: how authentication works, which addresses Backrest listens on, and which files on disk are sensitive. + +## Authentication Model + +Backrest ships with **no default credentials**. On first launch, the web UI prompts you to create a username and password before anything else can be configured. + +Under the hood: + +- **Password storage** — passwords are hashed with bcrypt and stored in the `users` section of the [config file](/docs/configuration). Plaintext passwords are never written to disk. +- **Sessions** — a successful login issues a JWT (signed with HS256) that expires after **7 days**, after which you log in again. The signing secret is 64 random bytes generated on first startup. +- **HTTP Basic auth** — every request also accepts HTTP Basic authentication with the same username and password. This is convenient for scripting against the [API](/docs/api) or for tools that can't handle a login flow. + +Multiple users can be defined in the config file. All users have full access; there are no roles or per-resource permissions. + +## Disabling Authentication + +Authentication can be turned off entirely (via the checkbox in the UI's Settings screen, or by setting `"auth": {"disabled": true}` in the config file). When disabled, all requests are served without any credential checks. + +::: warning +Only disable authentication if the interface is genuinely unreachable by anyone but you: bound to `127.0.0.1` on a single-user machine, or sitting behind a reverse proxy that performs its own authentication. Anyone who can reach an unauthenticated Backrest UI can read your repository credentials and delete your snapshots. +::: + +## Network Exposure + +By default, Backrest binds to `127.0.0.1:9898`, which is reachable only from the local machine. The listen address is controlled by the `BACKREST_PORT` environment variable or the `--bind-address` flag (the flag takes precedence if both are set). A value like `:9898` or `0.0.0.0:9898` listens on all interfaces. + +Defaults vary by install method: + +| Install method | Default bind | +| --- | --- | +| Linux/macOS `install.sh` | `127.0.0.1:9898` (localhost only); pass `--allow-remote-access` to bind `0.0.0.0:9898` | +| Docker image | `0.0.0.0:9898` inside the container — exposure is governed by your port mapping | +| Running the binary directly | `127.0.0.1:9898` | + +::: tip Docker port mapping +With Docker, publish the port as `127.0.0.1:9898:9898` (rather than `9898:9898`) if you only want local access. A bare mapping exposes the UI on every host interface and, with default Docker firewall rules, potentially to your whole network. +::: + +For remote access, prefer either a VPN/overlay network (WireGuard, Tailscale) or a TLS-terminating reverse proxy over exposing the port directly. Backrest serves plain HTTP, so credentials sent to a remotely exposed instance without TLS travel unencrypted. + +## Secrets on Disk + +Two locations contain sensitive data (see [Configuration & Paths](/docs/configuration) for where these live on each OS): + +- **`config.json`** — contains your repository passwords and any credentials you entered as repo environment variables (S3 keys, B2 keys, etc.) **in plaintext**. Backrest sets its permissions to `0600` (owner read/write only) whenever it writes the file; keep it that way, and make sure the Backrest user account itself is protected. +- **`/jwt-secret`** — the session-signing key, written with `0600` permissions. Deleting this file and restarting Backrest invalidates all existing login sessions. + +To keep cloud credentials out of the config file, note that repo environment variables, flags, and URIs support `${VAR}` expansion from Backrest's process environment. You can set `AWS_SECRET_ACCESS_KEY` in the service environment (e.g. a systemd override or Docker secrets) and reference `${AWS_SECRET_ACCESS_KEY}` in the repo config. + +::: info Back up your config +`config.json` is also what you need to rebuild your setup after a disaster. Most importantly, it holds your repository passwords, without which your backups cannot be decrypted; restic provides no recovery mechanism. Keep a copy somewhere safe and treat it with the same sensitivity as a password manager export. +::: + +## Resetting a Lost Password + +If you're locked out of the UI: + +1. Stop Backrest. +2. Open `config.json` (Linux/macOS: `~/.config/backrest/config.json`, Windows: `%appdata%\backrest\config.json`, Docker: `/config/config.json` in the container). +3. Delete the `"users"` key from the `"auth"` section. +4. Start Backrest again. + +On the next visit, the UI runs first-time setup again and asks you to create a new username and password. Your repos, plans, and operation history are untouched. + +## Reverse Proxies & TLS + +Backrest has no built-in TLS support; it serves HTTP (with h2c, i.e. cleartext HTTP/2, for its streaming APIs). To get HTTPS, put a reverse proxy such as Caddy, nginx, or Traefik in front of it and terminate TLS there. Two proxy-specific considerations: + +- The Web UI uses long-lived streaming requests, so the proxy needs generous read timeouts. +- [Multihost sync](/docs/multihost) between Backrest instances requires the proxy to support h2c or gRPC-style HTTP/2 forwarding. + +Working configurations for common proxies are collected in the [reverse proxy cookbook](/cookbooks/reverse-proxy-examples). If your proxy layer already authenticates users (e.g. Authelia, Authentik, or proxy-level basic auth), you can [disable Backrest's built-in authentication](#disabling-authentication) behind it, provided the proxy is the only route to the Backrest port. diff --git a/docs/src/guides/sftp.md b/docs/src/guides/sftp.md new file mode 100644 index 00000000..d3a59053 --- /dev/null +++ b/docs/src/guides/sftp.md @@ -0,0 +1,126 @@ +# SFTP & SSH Remotes + +Backrest can back up to any machine reachable over SSH, such as a NAS or a VPS, using restic's SFTP backend. A built-in setup flow generates an SSH key pair and records the server's host key, so most of the setup can be done from the web UI. + +Under the hood, restic's SFTP backend shells out to the `ssh` binary, so an OpenSSH client must be installed on the machine (or in the container) running Backrest. + +## Repository URI Formats + +restic accepts two URI shapes for SFTP: + +```text +sftp:user@host:/path/to/repo # scp-style (cannot carry a port) +sftp://user@host:2222/path/to/repo # URL-style (port allowed) +``` + +If your server listens on a non-standard port, either use the URL-style form or set the **SFTP Port** field described below. The path should be absolute, or relative to the SSH user's home directory. + +## Built-In Setup Flow + +When you enter an `sftp:` URI in the Add Repository form, an SFTP configuration section appears with three fields (**SFTP Identity File**, **SFTP Port**, and **SFTP Known Hosts**) plus an optional **Setup SSH Key** helper. + +Add Repository modal with an SFTP URI and the Setup SSH Key section expanded + +### Generate a Key + +Expand **Setup SSH Key** and click **Generate Key**. Backrest will: + +1. **Generate an Ed25519 key pair** dedicated to this host, stored as `id_ed25519_` (plus `.pub`) in a Backrest-managed SSH directory: `.backrest-ssh/` next to your config file (in Docker: `/config/.backrest-ssh/`). If a key for this host already exists there, it is reused rather than replaced. +2. **Scan the server's host key** with `ssh-keyscan` and append it to `.backrest-ssh/known_hosts`. If the host is already present in that file or in your `~/.ssh/known_hosts`, the scan is skipped. If the host is unreachable, key generation still succeeds and Backrest shows a warning so you can add the host key manually later. +3. **Fill in the fields** — the identity file and known hosts paths are populated automatically, and the public key is displayed for you to copy. + +### Authorize the Key on the Server + +Backrest does not install the key on the server for you. Append the displayed public key to the SSH user's `authorized_keys` on the remote machine: + +```bash +# on the remote server, as the backup user +mkdir -p ~/.ssh && chmod 700 ~/.ssh +echo 'ssh-ed25519 AAAA... backrest' >> ~/.ssh/authorized_keys +chmod 600 ~/.ssh/authorized_keys +``` + +Then submit the form. Backrest translates the three SFTP fields into a restic flag on the repository, so this is equivalent to configuring: + +```text +--option=sftp.args='-oBatchMode=yes -i "/path/to/.backrest-ssh/id_ed25519_host" -oUserKnownHostsFile="/path/to/.backrest-ssh/known_hosts"' +``` + +::: info Batch mode +Backrest always runs SSH with `-oBatchMode=yes` for SFTP repositories (it injects this flag automatically if you have not set `sftp.args` yourself). This makes SSH fail immediately with a clear error instead of hanging on an interactive password or host-key prompt, which a background service cannot answer. +::: + +::: warning Windows +The automated key setup flow is not available on Windows. Use an existing key configured through your SSH client instead, as described below. +::: + +## Using Your Own Keys + +The built-in flow is optional. If you already manage SSH keys, you have two equivalent options: + +**Option 1 — point the Identity File field at your key.** Enter the path to your private key (and optionally a known_hosts file and port) in the SFTP fields. The key must not have a passphrase, since restic runs non-interactively. + +**Option 2 — set the flag directly.** Add a repository flag yourself: + +```text +-o sftp.args="-i /path/to/private_key" +``` + +Any options you can pass to `ssh` work here (`-p`, `-oUserKnownHostsFile=...`, etc.). When you set `sftp.args` manually, Backrest does not inject `BatchMode=yes`, so include `-oBatchMode=yes` if you want fail-fast behavior. + +**Option 3 — use your SSH config.** If the host is defined in `~/.ssh/config` (with `IdentityFile`, `Port`, and so on), a bare `sftp:alias:/path` URI will pick that configuration up, since restic invokes the regular `ssh` binary. Note that a daemonized Backrest runs as its service user — it reads *that* user's SSH config, not your desktop user's. + +## SFTP in Docker + +The two published image variants differ here: + +| Image | OpenSSH client | SFTP support | +| --- | --- | --- | +| `ghcr.io/garethgeorge/backrest:latest` (alpine) | Yes (including `ssh-keyscan`) | Full, including the built-in setup flow | +| `ghcr.io/garethgeorge/backrest:scratch` | No (no shell either) | Not available | + +With the default alpine image and the standard `/config` volume mount, the built-in setup flow works out of the box: generated keys and the known_hosts file live in `/config/.backrest-ssh/`, so they survive container recreation along with the rest of your config. + +If you prefer to generate keys on the Docker host and mount them into the container yourself, that approach is written up in the [SSH remotes with Docker Compose cookbook](/cookbooks/ssh-remote). + +## Troubleshooting + +**"Host key verification failed" or "Remote host identification has changed"** + +The server's host key is not in the known_hosts file restic is using, or it no longer matches. A changed host key can mean the server was reinstalled or, rarely, that something is intercepting the connection. Re-run **Setup SSH Key** to scan the current host key, or update the file manually: + +```bash +ssh-keyscan -H your-server >> /path/to/.backrest-ssh/known_hosts +``` + +If the key genuinely changed, delete the stale entry first: `ssh-keygen -R your-server -f /path/to/known_hosts`. + +**"Permission denied (publickey)"** + +- The public key is not in the remote user's `~/.ssh/authorized_keys`, or the wrong user is in the URI. +- Permissions on the remote side are too loose — sshd refuses keys when `~/.ssh` is not `700` or `authorized_keys` is not `600`. +- The identity file path is wrong from Backrest's point of view (remember: in Docker it must be a container-side path). + +**Operations hang or fail immediately with a cryptic SSH error** + +With `BatchMode=yes` in place, anything that would normally prompt interactively (password auth, unknown host key, encrypted key passphrase) fails immediately instead. Read the error text; restic forwards SSH's stderr. Reproduce the connection outside Backrest to isolate the problem: + +```bash +ssh -oBatchMode=yes -i /path/to/key user@host true +``` + +If that command succeeds silently, the SSH layer is fine and the issue is elsewhere (path permissions on the repository directory, for example). + +**Repository directory not writable** + +restic needs to create the repository directory structure under the URI path. Ensure the SSH user owns the target path on the server. + +## Alternative: rest-server over SSH + +SFTP works with any SSH server, but restic's [rest-server](https://github.com/restic/rest-server) is faster and supports an append-only mode that protects existing backups even if the client machine is compromised. If you control the remote machine, consider running rest-server on it instead; see the `rest:` section of the [Storage Backends guide](/guides/storage-backends). + +## Next Steps + +- Browse all supported backends in the [Storage Backends guide](/guides/storage-backends) +- Mount-your-own-keys Docker setup: [SSH remotes with Docker Compose cookbook](/cookbooks/ssh-remote) +- Configure [scheduling](/guides/scheduling) and [retention & repo health](/guides/repo-health) for your new repository diff --git a/docs/src/guides/storage-backends.md b/docs/src/guides/storage-backends.md new file mode 100644 index 00000000..9d68d67c --- /dev/null +++ b/docs/src/guides/storage-backends.md @@ -0,0 +1,195 @@ +# Storage Backends + +Backrest stores your backups in standard [restic](https://restic.net) repositories, which means it supports every storage backend that restic supports: local disks, SFTP servers, S3-compatible object stores, Backblaze B2, Azure Blob Storage, Google Cloud Storage, restic's own rest-server, and (via rclone) dozens of additional providers. + +This guide covers how to configure each backend in Backrest: the repository URI syntax, the credentials each backend expects, and how Backrest passes environment variables and flags through to restic. For deep details on any individual backend, the [restic documentation](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html) is the authoritative reference. + +## How Repository Configuration Works + +When you add a repository in Backrest, three fields together determine how restic connects to your storage: + +- **Repository URI** — tells restic which backend to use and where the repository lives. The scheme prefix (`s3:`, `b2:`, `sftp:`, ...) selects the backend; a plain filesystem path selects local storage. +- **Environment variables** — per-repository `KEY=VALUE` pairs, passed to every restic command Backrest runs against this repository. This is where backend credentials go. +- **Flags** — extra command-line flags appended to every restic invocation for this repository (for example `-o sftp.args=...` or `--limit-upload`). + +::: info Repositories stay portable +Backrest does not wrap or alter the repository format. Any repository you create through Backrest is a plain restic repository, and you can operate on it directly with the restic CLI using the same URI, password, and environment variables. +::: + +### Password Precedence + +The repository password entered in Backrest takes precedence over any environment-provided password. When a password is set in the repository config, Backrest exports it as `RESTIC_PASSWORD` *after* importing the system environment, and explicitly clears `RESTIC_PASSWORD_FILE` and `RESTIC_PASSWORD_COMMAND` so that values in the host or container environment cannot override it. + +If you prefer to manage the password outside of Backrest's config file, leave the password field empty and instead provide one of the following as a per-repository environment variable: + +| Variable | Meaning | +| --- | --- | +| `RESTIC_PASSWORD` | The password itself. | +| `RESTIC_PASSWORD_FILE` | Path to a file containing the password. | +| `RESTIC_PASSWORD_COMMAND` | Command that prints the password to stdout. | + +The Add Repository form requires a password by one of these mechanisms unless you pass the `--insecure-no-password` flag, which creates an unencrypted repository. + +::: warning +Restic cannot decrypt data without the repository password, so losing the password means losing access to your backups. Store it somewhere safe outside the machine being backed up. +::: + +### Variable Expansion + +The repository URI, environment variables, and flags all support `${VAR}` expansion from the environment of the Backrest process itself. For example, you can keep secrets out of `config.json` by referencing variables set in your systemd unit or docker-compose file: + +```json +{ + "id": "s3-backups", + "uri": "s3:s3.amazonaws.com/${BUCKET_NAME}", + "env": [ + "AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}", + "AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}" + ] +} +``` + +Only the `${VAR}` form is expanded (not `$VAR`), and variables that are unset in Backrest's environment expand to an empty string. + +## Local Paths + +For local storage, use an absolute filesystem path as the URI, with no scheme prefix. + +```text +/mnt/backup-drive/backrest-repo +``` + +No environment variables are required. A few things to keep in mind: + +- **Docker**: the path is interpreted inside the container. Mount your backup volume into the container and use the container-side path as the URI. +- **Removable media**: if the drive may be unplugged mid-operation, enable the repository's **auto unlock** option so Backrest automatically removes stale locks left behind by interrupted operations. Only enable this when no other host writes to the same repository, since it will also remove locks legitimately held by another instance. +- Backing up to the same disk you are backing up *from* protects against accidental deletion, but not against disk failure. Pair a local repository with a remote one for important data. + +## S3-Compatible Storage + +Works with AWS S3 and any S3-compatible service: MinIO, Wasabi, Backblaze B2's S3 endpoint, Cloudflare R2, Garage, and others. + +```text +s3:s3.amazonaws.com/bucket-name +``` + +For non-AWS services, embed the endpoint URL in the URI: + +```text +s3:https://minio.example.com/bucket-name +s3:https://s3.us-west-1.wasabisys.com/bucket-name +``` + +| Environment variable | Purpose | +| --- | --- | +| `AWS_ACCESS_KEY_ID` | Access key ID. | +| `AWS_SECRET_ACCESS_KEY` | Secret access key. | +| `AWS_SHARED_CREDENTIALS_FILE` | Alternative: path to a shared AWS credentials file instead of the two keys above. | + +Provide either both key variables or the credentials file; the Add Repository form validates that one of these combinations is present. Region selection and other S3 options are covered in the [restic S3 documentation](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#amazon-s3). + +::: tip +Create a dedicated bucket and an access key scoped to just that bucket for Backrest, rather than reusing account-wide credentials. +::: + +## Backblaze B2 + +```text +b2:bucketname:path/to/repo +``` + +| Environment variable | Purpose | +| --- | --- | +| `B2_ACCOUNT_ID` | Application key ID. | +| `B2_ACCOUNT_KEY` | Application key. | + +B2 buckets can also be accessed through their S3-compatible endpoint using the `s3:` scheme above; see the [restic B2 documentation](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#backblaze-b2) for the tradeoffs. + +## Azure Blob Storage + +```text +azure:container-name:/ +``` + +| Environment variable | Purpose | +| --- | --- | +| `AZURE_ACCOUNT_NAME` | Storage account name (always required). | +| `AZURE_ACCOUNT_KEY` | Account key, **or** | +| `AZURE_ACCOUNT_SAS` | Shared access signature token as an alternative to the account key. | + +Set `AZURE_ACCOUNT_NAME` plus either `AZURE_ACCOUNT_KEY` or `AZURE_ACCOUNT_SAS`. Further options are in the [restic Azure documentation](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#microsoft-azure-blob-storage). + +## Google Cloud Storage + +```text +gs:bucket-name:/ +``` + +| Environment variable | Purpose | +| --- | --- | +| `GOOGLE_APPLICATION_CREDENTIALS` | Path to a service account credentials JSON file. | +| `GOOGLE_PROJECT_ID` | The GCP project ID (used together with the credentials file). | +| `GOOGLE_ACCESS_TOKEN` | Alternative: a raw OAuth2 access token instead of the two above. | + +::: warning Docker note +`GOOGLE_APPLICATION_CREDENTIALS` is a *path*, and restic runs inside the container. Mount the credentials JSON into the container (for example to `/config/gcs-key.json`) and point the variable at the container-side path. +::: + +See the [restic GCS documentation](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#google-cloud-storage) for details on creating a service account. + +## SFTP + +```text +sftp:user@host:/path/to/repo +``` + +Backrest has first-class SFTP support, including built-in SSH key generation and host key management from the WebUI. Because there is enough to cover, SFTP has its own page: see the [SFTP & SSH Remotes guide](/guides/sftp). + +## rclone + +The `rclone:` scheme lets restic reach any of the many providers rclone supports (Google Drive, OneDrive, Dropbox, Proton Drive, and more): + +```text +rclone:myremote:path/to/repo +``` + +where `myremote` is a remote you have configured with `rclone config`. Requirements: + +- The `rclone` binary must be installed and on the `PATH` of the machine (or container) running Backrest. +- Your rclone config file must be readable by the Backrest process. + +In Docker, the `ghcr.io/garethgeorge/backrest:latest` (alpine-based) image ships with rclone preinstalled. Backrest runs as root in the container, so mount your rclone config to root's default location: + +```yaml +services: + backrest: + image: ghcr.io/garethgeorge/backrest:latest + volumes: + - ./rclone-config:/root/.config/rclone + # ... other volumes +``` + +The `:scratch` image variant does not include rclone (or a shell), so the rclone backend is unavailable there. See the [restic rclone documentation](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#other-services-via-rclone) for advanced options such as tuning rclone flags. + +## rest-server + +restic's [rest-server](https://github.com/restic/rest-server) is a lightweight HTTP server purpose-built for hosting restic repositories. It is a good option for backing up to a machine you control, and is generally faster than SFTP. + +```text +rest:https://user:pass@host:8000/repo-name +``` + +Credentials are carried in the URI rather than environment variables. Run rest-server with `--append-only` on the receiving machine for protection against a compromised client deleting its own backups. See the [restic REST server documentation](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#rest-server) for setup details. + +## Troubleshooting + +- **"Missing env vars ... for scheme ..."** — the Add Repository form checks that the expected credential variables for your URI scheme are present (the combinations in the tables above) and lists exactly which are missing. +- **Credentials look right but restic fails to connect** — remember that `${VAR}` references are expanded from *Backrest's* process environment. If the variable is not set for the Backrest service (systemd unit, docker-compose `environment:` block), it silently expands to an empty string. +- **Works with restic CLI but not in Backrest** — compare environments: Backrest passes only the system environment plus the per-repository variables. Shell-only configuration (e.g. variables exported in your `~/.bashrc`) is not visible to a Backrest daemon started by systemd or Docker. +- Test any repository outside Backrest by running the restic CLI with the same URI and environment variables — the repository formats are identical. + +## Next Steps + +- Set up [SFTP & SSH remotes](/guides/sftp) +- Configure [backup scheduling](/guides/scheduling) and [retention & repo health](/guides/repo-health) +- Review [where Backrest stores its own configuration](/docs/configuration) diff --git a/docs/src/index.md b/docs/src/index.md index 6234c70e..efd59b4d 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -4,11 +4,14 @@ layout: home hero: name: "Backrest" text: "Web UI and orchestrator for Restic backup." - tagline: "Backrest is a web-accessible backup solution built on top of restic and providing a WebUI which wraps the restic CLI and makes it easy to create repos, browse snapshots, and restore files. Additionally, Backrest can run in the background and take an opinionated approach to scheduling snapshots and orchestrating repo health operations." + tagline: "Backrest is a web-accessible backup solution built on top of restic. It wraps the restic CLI in a WebUI that makes it easy to create repos, browse snapshots, and restore files — and it runs in the background to schedule backups and keep your repositories healthy." actions: - theme: brand - text: Get started + text: Get Started link: /introduction/getting-started + - theme: alt + text: Install + link: /introduction/installation - theme: alt text: Open on GitHub link: https://github.com/garethgeorge/backrest @@ -28,50 +31,13 @@ features: details: Backup to any restic supported storage (e.g. S3, B2, Azure, GCS, local, SFTP, and all rclone remotes). Cross-platform support. --- -## Installation +## Quick Install + +Run Backrest with Docker Compose or install it natively with one command — see the [full installation guide](/introduction/installation) for Windows, Homebrew, service configuration, and image variants. ::: code-group -```bash [Linux (Script)] -# Download the latest release from https://github.com/garethgeorge/backrest/releases -curl -sLO https://github.com/garethgeorge/backrest/releases/latest/download/backrest_Linux_x86_64.tar.gz -mkdir backrest && tar -xzvf backrest_Linux_x86_64.tar.gz -C backrest -cd backrest && ./install.sh -``` -```bash [Linux (systemd)] -sudo mv backrest /usr/local/bin/backrest -sudo tee /etc/systemd/system/backrest.service > /dev/null < + +Every snapshot Backrest creates is tagged `created-by:`, which is how Backrest tells its snapshots apart from those created by other machines sharing the same repository. + +::: warning +The instance ID cannot be changed from the UI later, because renaming it would break the association with existing snapshots. Choose a value you do not expect to change. +::: + +## Step 2: Add a Repository + +A repository is where restic stores your encrypted backup data. Click **Add Repo**. + +Add repository view + +For a first backup to local storage, you only need three fields: + +1. **Repository name** — a human-readable ID, e.g. `mydrive`. Immutable after creation. +2. **Repository URI** — where the data lives. For local storage this is a filesystem path, e.g. `/mnt/backupdisk/backrest-repo` (in Docker, a path inside the container such as `/repos/backrest-repo`). Cloud URIs like `s3:...` or `b2:...` are also accepted; each provider's URI format and credentials are covered in [Storage Backends](/guides/storage-backends). +3. **Password** — the encryption key for the repository. Backrest can generate a strong one for you. + +::: danger Save your password +The repository password encrypts all backup data. If you lose it, your backups cannot be recovered; there is no reset mechanism. Store it in a password manager along with a copy of your Backrest config file. +::: + +The remaining settings (environment variables, flags, prune/check policies, hooks) have reasonable defaults and can be changed later. + +Click **Submit**. If the URI points at an empty location, Backrest initializes a brand-new restic repository there. + +::: info Importing an existing restic repository +The same flow works for existing repositories: enter the URI and password, then open the repository view and click **Index Snapshots** to import the snapshot history. Backrest can be used alongside the restic CLI; repositories remain standard restic repositories. +::: + +## Step 3: Create a Backup Plan + +A plan defines *what* to back up, *when*, and *how long to keep it*. Click **Add Plan**. + +Add plan view + +1. **Plan name** — descriptive and immutable, e.g. `mydrive-documents` (a `[storage]-[content]` naming convention works well). +2. **Repository** — select the repo you just created. Immutable after creation. +3. **Paths** — the directories or files to back up. +4. **Excludes** (optional) — glob patterns to skip, e.g. `*node_modules*` or `.cache`. `iexcludes` are the same but case-insensitive. +5. **Schedule** — when backups run. The default is fine for now; the [Scheduling guide](/guides/scheduling) explains cron expressions, intervals, and clocks in depth. +6. **Retention policy** — how long snapshots are kept. The default time-bucketed policy (a mix of daily/weekly/monthly snapshots) suits most users; see [Retention & Repo Health](/guides/repo-health) for how retention actually works. + +Click **Submit**. The plan now appears in the sidebar and will run on its schedule. + +## Step 4: Run a Backup Now + +You do not need to wait for the schedule. Open your plan and click **Backup Now**. + +The operation appears in the plan's history: it starts **pending** (queued), moves to **in progress** with live progress and log output, and finishes as **success**. Click the operation row to see details: files added, bytes processed, and the new snapshot ID. The dashboard reflects the result too: + +Summary dashboard after a successful backup + +If the backup finishes with a **warning** status instead, the snapshot was still created. A warning means some files could not be read; locked or permission-denied files are common on first runs. The operation log lists which paths failed. + +## Step 5: Verify Your Snapshot + +After the backup completes, confirm the snapshot contains what you expect: + +1. In the plan or repository view, find your new snapshot in the tree. +2. Expand it to browse the files it contains, and confirm the paths you configured are present. + +Snapshot operation expanded, showing details and the snapshot browser with backed-up files + +For a complete end-to-end test, restore a file or two by following the [Restoring Files](/introduction/restore-files) guide. A test restore is the most reliable way to confirm a backup is usable. + +## What Happens After a Backup + +Additional operations appear in the history after a successful backup. These are follow-up tasks that Backrest schedules automatically: + +- A **forget** operation applies your plan's retention policy, removing snapshots that have aged out of it. Forget does not delete the underlying data; the repository's scheduled **prune** operation reclaims that space later. +- An **index snapshots** operation keeps Backrest's local view of the repository in sync. + +The [Retention & Repo Health guide](/guides/repo-health) explains this pipeline, and the [Operational Model reference](/docs/operations) covers how the orchestrator schedules and prioritizes everything. + +## Next Steps + +- [Tune your schedule](/guides/scheduling) — cron vs. intervals, and behavior when a machine is asleep at the scheduled time. +- [Set up retention, prune, and check](/guides/repo-health) — keep the repository healthy and storage bounded. +- [Set up notifications](/guides/notifications) — Discord, Slack, Gotify, Telegram, Healthchecks, and more. +- [Restore files](/introduction/restore-files) — walk through a test restore. + +::: warning Back up your Backrest config +Your config file (typically `~/.config/backrest/config.json`) contains your repository definitions, credentials, and plans. Keep a copy somewhere safe; see [Configuration & Paths](/docs/configuration). +::: diff --git a/docs/src/introduction/getting-started.md b/docs/src/introduction/getting-started.md index 8c68e999..d7153d8e 100644 --- a/docs/src/introduction/getting-started.md +++ b/docs/src/introduction/getting-started.md @@ -1,142 +1,89 @@ -# Getting Started +# Introduction & Concepts -This guide will walk you through the basic steps to setup a new [Backrest](https://github.com/garethgeorge/backrest) instance. +[Backrest](https://github.com/garethgeorge/backrest) is a web UI and orchestrator for [restic](https://restic.net), the fast, secure, deduplicating backup tool. Backrest wraps the restic CLI with a browser-based interface for creating repositories, scheduling backups, browsing snapshots, and restoring files. It also runs in the background to schedule backups and repository maintenance. -## Prerequisites +This page explains the concepts the rest of the documentation builds on. If you'd rather learn by doing, jump straight to [Installation](/introduction/installation) and [Your First Backup](/introduction/first-backup). -Before diving into configuration, you should have: -- Backrest installed and running on your system. -- Your storage provider credentials ready (if using remote storage). -- Access to Backrest via your browser (typically `http://localhost:9898`). +## How Backrest Relates to Restic -## Installation +Backrest executes every backup, prune, and restore by running the restic binary. Backrest downloads and verifies a copy of restic automatically, or you can [configure it to use your own](/docs/configuration). -Please refer to the GitHub README for platform-specific installation instructions. +Because restic performs the actual storage operations, repositories created by Backrest are standard restic repositories. You can browse them with the restic CLI, restore from them on a machine that has never run Backrest, and use Backrest alongside your own restic scripts. Backrest adds orchestration, history tracking, and a UI; the underlying data format is unchanged. ## Core Concepts -Let's understand some key terminology used within Backrest: +### Instance -- **Restic Repository**: The underlying storage location where your backup data is kept. While Backrest manages this for you, understanding this concept allows you to interact directly with your backups using the restic CLI if needed. +One installation of Backrest, identified by an **instance ID** you choose at first launch (e.g. `home-server`). Snapshots are tagged with the instance that created them, so multiple machines can safely share one repository. The instance ID cannot be changed later without orphaning the association with existing snapshots. -- **Backrest Repository**: A configuration set in Backrest that defines: - - Where your backup data is stored - - Encryption credentials - - Backup orchestration settings - - Associated hooks and options +### Repository -- **Backup Plan**: A configuration that specifies: - - What local data to backup - - When to create snapshots - - How long to retain backups - - When to run maintenance operations +Where your encrypted backup data lives. The term is used at two levels: -- **Key Operations**: - - **Backup**: Creates a new snapshot of your data - - **Forget**: Marks old snapshots for deletion (without removing data) - - **Prune**: Removes unreferenced data to free up storage space - - **Restore**: Retrieves files from a snapshot to your local system +- A **restic repository** is the on-disk/remote storage format: a content-addressed, encrypted, deduplicated store that any restic client can read. +- A **Backrest repository** is that restic repository *plus* Backrest's configuration for it: the URI, credentials, environment variables and flags, maintenance policies (prune/check), and repository-level hooks. -## Initial Setup +One repository can serve many plans, and deduplication works across all of them. -::: info -After installation, access Backrest at `http://localhost:9898` (or your configured port). You'll need to complete the initial setup process below. -::: +### Plan -### 1. Instance Configuration +A plan defines what to back up (paths and exclude patterns), when to back it up (a schedule), and how long to keep the results (a retention policy). Plans belong to exactly one repository and can carry their own hooks (e.g. notify on failure). A machine typically has a small number of plans, such as one for documents backed up hourly and one for photos backed up daily, writing to one or more repositories. -Settings View +### Operations -#### Instance ID -- A unique identifier for your Backrest installation. -- Used to distinguish snapshots from different Backrest instances. -- **Important**: Cannot be changed via the UI after initial setup. +Everything Backrest does to a repository is an **operation**: backup, forget, prune, check, restore, and a few housekeeping tasks. Operations are the unit you see in the UI's history tree, each with a status (pending → in progress → success, warning, or error) and full logs. -#### Authentication -- Set your username and password during first launch. -- To reset credentials, delete the `"users"` key from your configuration file and **restart the Backrest service**: - - Linux/macOS: `~/.config/backrest/config.json` - - Windows: `%appdata%\backrest\config.json` -- Authentication can be disabled for local installations or when using an authenticating reverse proxy. +The four you'll interact with most: -### 2. Repository Setup +| Operation | What it does | +| --- | --- | +| **Backup** | Creates a new snapshot of your plan's paths | +| **Forget** | Applies retention policy, *marking* aged-out snapshots (no data deleted yet) | +| **Prune** | Reclaims storage by deleting data no snapshot references | +| **Restore** | Copies files from a snapshot back to disk | -Click **"Add Repo"** to configure your backup storage location. You can either create a new repository or connect to an existing one. +### The Operation Log -Add Repository View +Backrest records every operation in a local database (the *oplog*), which powers the history tree, statistics graphs, and multihost monitoring. The operation log is stored separately from the backup data, which lives in the repository. -#### Essential Repository Settings +### The `_system_` Plan -1. **Repository Name** - - A human-readable identifier. - - Cannot be changed after creation. +Repository-level maintenance (prune, check, repository-wide forget) is not tied to any of your plans, so it appears in the UI under a synthetic plan named `_system_`. Operations listed there are repository maintenance run by Backrest itself. -2. **Repository URI** - - Specifies the backup storage location. - - Common formats: - - Backblaze B2: `b2:bucket` or `b2:bucket/prefix` - - AWS S3: `s3:bucket` or `s3:bucket/prefix` - - Google Cloud: `gs:bucket:/` or `gs:bucket:/prefix` - - SFTP: `sftp:user@host:/path/to/repo` - - Local: `/mnt/backupdisk/repo1` - - Rclone: `rclone:remote:path` (requires rclone installation. See the [Rclone documentation](https://rclone.org/) to configure remote backends). +## How the Pieces Fit Together -3. **Environment Variables** - - Storage provider credentials: - - S3: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` - - B2: `B2_ACCOUNT_ID`, `B2_ACCOUNT_KEY` - - Google Cloud: `GOOGLE_PROJECT_ID`, `GOOGLE_APPLICATION_CREDENTIALS` +``` + Plan "mydrive-documents" Plan "mydrive-photos" + what: /home/me/Documents what: /home/me/Photos + when: hourly when: daily + keep: 30 daily, 12 monthly keep: 12 monthly + │ │ + └────────────┬────────────────────┘ + ▼ + Repository "mydrive" (s3:... + credentials) + maintenance: prune weekly, check monthly (_system_) + ▼ + restic repository (encrypted, deduplicated) +``` -4. **Optional Flags** - - Common examples: - - SFTP key: `-o sftp.args="-i /path/to/key"` - - Disable locking: `--no-lock` - - Bandwidth limits: `--limit-upload 1000`, `--limit-download 1000` +Backrest's orchestrator runs one operation at a time per repository, queued by time and priority, so that backups, follow-up forgets, and scheduled maintenance do not conflict over repository locks. The [Operational Model reference](/docs/operations) covers this in detail. -5. **Maintenance Policies** - - **Prune Policy**: Schedule for cleaning unreferenced data. - - **Check Policy**: Schedule for backup integrity verification. +## Where to Go Next -::: info -Once you've saved the repository, navigate to the Repository View and click **"Index Snapshots"** to import any previous backups. Backrest will also automatically index snapshots the first time a backup plan runs successfully. -::: +| I want to… | Read | +| --- | --- | +| Install Backrest | [Installation](/introduction/installation) | +| Take my first backup | [Your First Backup](/introduction/first-backup) | +| Get files back | [Restoring Files](/introduction/restore-files) | +| Tune when backups run | [Scheduling Backups](/guides/scheduling) | +| Manage retention and repo health | [Retention & Repo Health](/guides/repo-health) | +| Configure S3/B2/Azure/GCS/rclone | [Storage Backends](/guides/storage-backends) | +| Back up over SSH | [SFTP & SSH Remotes](/guides/sftp) | +| Get notified about failures | [Notifications](/guides/notifications) | +| Secure or expose my instance | [Authentication & Security](/guides/security) | +| Understand the internals | [Operational Model](/docs/operations) | +| Look up paths, env vars, config fields | [Configuration & Paths](/docs/configuration) | -### 3. Backup Plan Configuration - -Create a backup plan by clicking **"Add Plan"** and configuring these settings: - -Add Plan View - -#### Plan Settings - -1. **Plan Name** - - Choose a descriptive, immutable name. - - Recommended format: `[storage]-[content]` (e.g., `b2-documents`). - -2. **Repository** - - Select your target repository. - - Cannot be changed after creation. - -3. **Backup Configuration** - - **Paths**: Directories/files to backup. - - **Excludes**: Patterns or paths to skip (e.g., `*node_modules*`). - -4. **Schedule** - - Choose one: - - Hourly/daily intervals. - - Cron expression (e.g., `0 0 * * *` for daily midnight backups). We highly recommend using [crontab.guru](https://crontab.guru/) to help format your cron schedules correctly. - - Clock options: - - UTC/Local: Wall-clock time. - - Last Run Time: Relative to previous execution. - -5. **Retention Policy** - - Controls snapshot lifecycle: - - **Count-based**: Keep N most recent snapshots. - - **Time-based**: Keep snapshots by age (e.g., daily for 7 days, weekly for 4 weeks). - - **None**: Manual retention management. - -Success! Now that Backrest is configured, you can sit back and let it manage your backups. Monitor the status of your backups in the UI and restore files from snapshots as needed. - -::: warning -Make sure to save a copy of your repository credentials and encryption keys (e.g., password) in a safe place. Losing these will prevent you from restoring your data. Consider storing your entire Backrest configuration (typically `~/.config/backrest/config.json`) in a secure location, such as a password manager or encrypted storage. +::: warning Protect your credentials +Your repository password is required to decrypt your backups, and restic provides no way to reset it. Store the password (ideally your whole `config.json`) in a password manager or other secure location. See [Authentication & Security](/guides/security). ::: diff --git a/docs/src/introduction/installation.md b/docs/src/introduction/installation.md new file mode 100644 index 00000000..893dce49 --- /dev/null +++ b/docs/src/introduction/installation.md @@ -0,0 +1,149 @@ +# Installation + +Backrest ships as a single executable for Linux, macOS, and Windows, plus official Docker images. On first run it automatically downloads a verified copy of [restic](https://restic.net) if a compatible version is not already installed on your system. + +Once installed, Backrest is available at `http://localhost:9898`. On first launch it will prompt you to create a username and password. + +## Choosing an Install Method + +| Platform | Recommended method | Alternatives | +| --- | --- | --- | +| Linux server | [Install script](#linux-and-macos-install-script) (systemd/OpenRC) | [Docker](#docker), [AUR](#arch-linux-aur) | +| NAS / homelab with containers | [Docker Compose](#docker) | Install script | +| macOS | [Homebrew](#macos-homebrew) | Install script (launchd) | +| Windows | [Installer](#windows) | — | + +## Linux and macOS (Install Script) + +The install script downloads the latest release, installs the binary to `/usr/local/bin`, and sets up auto-start using your platform's service manager (systemd or OpenRC on Linux, launchd on macOS): + +```bash +curl -fsSL https://raw.githubusercontent.com/garethgeorge/backrest/main/install.sh | bash +``` + +Flags go after `--`: + +```bash +# Bind to all interfaces instead of the default 127.0.0.1:9898 +curl -fsSL https://raw.githubusercontent.com/garethgeorge/backrest/main/install.sh | bash -s -- --allow-remote-access + +# Uninstall (removes the service, autostart entry, and /usr/local/bin/backrest) +curl -fsSL https://raw.githubusercontent.com/garethgeorge/backrest/main/install.sh | bash -s -- --uninstall +``` + +The service runs as your user by default, so configuration and data live under your `$HOME` (see [Configuration & Paths](/docs/configuration)). To install as `root` instead, pass `--root`; backups will then run with root privileges and can read files your user cannot. + +::: tip +Review [install.sh](https://github.com/garethgeorge/backrest/blob/main/install.sh) before piping it into a shell. You can also clone the repository and run `./install.sh` locally; it accepts the same flags. +::: + +::: warning Binding to all interfaces +Only use `--allow-remote-access` on trusted networks, and make sure authentication is enabled. See [Authentication & Security](/guides/security) for guidance on exposing Backrest safely. +::: + +### macOS (Homebrew) + +Install from the [Homebrew tap](https://github.com/garethgeorge/homebrew-backrest-tap): + +```bash +brew tap garethgeorge/homebrew-backrest-tap +brew install backrest +brew services start backrest +``` + +::: info Full Disk Access +macOS restricts access to many directories by default. If backups fail with permission errors, grant Full Disk Access to Backrest under `System Preferences > Security & Privacy > Privacy > Full Disk Access` by adding `/usr/local/bin/backrest`. +::: + +### Arch Linux (AUR) + +The [AUR package](https://aur.archlinux.org/packages/backrest) is third-party (not maintained by the Backrest project) and uses its own systemd unit: + +```bash +paru -Sy backrest # or: yay -Sy backrest +sudo systemctl enable --now backrest@$USER.service +``` + +## Docker + +The canonical image is `ghcr.io/garethgeorge/backrest` (also mirrored to [Docker Hub](https://hub.docker.com/r/garethgeorge/backrest)). Two variants are published: + +| Tag | Base | Contents | +| --- | --- | --- | +| `latest` | Alpine | restic, rclone, openssh, bash, curl, docker CLI, timezone data | +| `scratch` | scratch | restic and Backrest only — no shell or extra tools | + +::: warning Choosing scratch +The `scratch` image does not include a shell, ssh, or rclone. Command hooks, the guided SFTP setup, and rclone remotes will not work in it. Use `latest` unless you are sure you do not need those features. +::: + +### Docker Compose + +```yaml +services: + backrest: + image: ghcr.io/garethgeorge/backrest:latest + container_name: backrest + hostname: backrest + volumes: + - ./backrest/data:/data + - ./backrest/config:/config + - ./backrest/cache:/cache + - ./backrest/tmp:/tmp + - ./backrest/rclone:/root/.config/rclone # rclone config (only needed for rclone remotes) + - /path/to/backup/data:/userdata # mount the local paths you want to back up + - /path/to/local/repos:/repos # mount local repo storage (optional if using remote storage) + environment: + - BACKREST_DATA=/data + - BACKREST_CONFIG=/config/config.json + - XDG_CACHE_HOME=/cache + - TMPDIR=/tmp + - TZ=America/Los_Angeles + ports: + - "9898:9898" + restart: unless-stopped +``` + +A few things to know about the container: + +- Inside the container Backrest binds `0.0.0.0:9898` and defaults its paths to `/config/config.json`, `/data`, and `/cache`. The compose file above mounts each of these so your configuration and operation history survive container recreation. +- Set `hostname:` (or the instance ID at first launch) to a stable value, since it identifies this instance's snapshots. +- Set `TZ` so cron schedules run in your local timezone rather than UTC. +- Backrest can only back up paths that are mounted into the container, and restores also write to container paths, so plan your mounts accordingly. + +## Windows + +Download `Backrest-setup-[arch].exe` from the [releases page](https://github.com/garethgeorge/backrest/releases). The installer places Backrest and a tray application in `%localappdata%\Programs\Backrest\`. The tray app starts on login, runs Backrest in the background, and shows its status. + +::: tip Changing the port on Windows +Set a user environment variable named `BACKREST_PORT` (Settings > About > Advanced system settings > Environment Variables) with a value like `127.0.0.1:8080`. If you change it after installation, re-run the installer so shortcuts pick up the new port. +::: + +## Verifying the Install + +Open `http://localhost:9898` in your browser. You should see the Backrest UI and a prompt to create your first user. From there, continue to [Your First Backup](/introduction/first-backup). + +If nothing loads: + +- **Script/service installs**: check the service status (`systemctl status backrest`, `rc-service backrest status`, or `brew services info backrest`). +- **Docker**: check `docker logs backrest`. +- Confirm nothing else is bound to port 9898, or change the port via `BACKREST_PORT`. + +## Upgrading + +Backrest is safe to upgrade in place. Configuration and operation history are stored separately from the binary (see [Configuration & Paths](/docs/configuration)) and are migrated automatically when the format changes. + +- **Install script**: re-run the script; it replaces the binary with the latest release and restarts the service. +- **Homebrew**: `brew upgrade backrest && brew services restart backrest`. +- **Docker**: pull the new image and recreate the container. Your state lives in the mounted `/config` and `/data` volumes. +- **Windows**: run the new installer over the existing installation. + +## Uninstalling + +- **Install script**: run it with `--uninstall` (see above). This removes the service and binary but leaves your configuration (`~/.config/backrest`) and data (`~/.local/share/backrest`) in place; delete those directories manually to remove all Backrest state. +- **Docker**: remove the container and delete the mounted config/data directories. +- **Windows**: uninstall via Settings > Apps, then optionally delete `%appdata%\backrest`. + +::: warning +Uninstalling Backrest does not touch your restic repositories; your backup data remains wherever it is stored. Keep a copy of your repository passwords, since without them the backups cannot be decrypted. +::: diff --git a/docs/src/introduction/restore-files.md b/docs/src/introduction/restore-files.md index 52d178ab..9dfdf2ea 100644 --- a/docs/src/introduction/restore-files.md +++ b/docs/src/introduction/restore-files.md @@ -1,44 +1,77 @@ -# Restore Files +# Restoring Files -This guide will walk you through the basic steps of using Backrest to restore files. +This guide covers finding the right snapshot, restoring files to disk, downloading files through the browser, and recovering data on a different machine. -## Prerequisites - -- A running Backrest instance -- A repo configured in Backrest - -## Indexing Snapshots - -A snapshot is a point-in-time backup of your files. This is interchangeable with the term "backup". To restore files, you first need to index the snapshots in your repository. This is done automatically by Backrest when you - - 1. first add a repository - 2. run a backup - -if you have recently added your repository or are using backrest to regularly create backups there is nothing to do here. If you've created your backups some other way, you may need to index them before they will show up in the UI. To do this, click the "Index Snapshots" button in the repository view. - -Index Snapshots Button - -## Restoring a Snapshot - -Once your snapshots are indexed, they are visible in backrest in a tree view ordered by their creation timestamp. To view details about a snapshot, click on it in the tree view. This will open a side panel with the history of operations that created the snapshot (if it was created by this backrest install) as well as the snapshot operation itself which includes: - - * Metadata about the snapshot - * Snapshot browser which can be used to browse and restore files in the snapshot - -Tree View for Restore Article - -To restore a snapshot start by browsing for the files you'd like to restore. Click on the "Snapshot Browser" shown in the red box in the image above. This view will expand to show the files in your snapshot. - -::: warning -If your repo is using remote storage browsing can be very slow as restic fetches pack files to index the directory structure of your snapshot. +::: tip +Perform a small test restore after setting up your first plan. It verifies repository access, the repository password, and data integrity before you need them in a real recovery. ::: -Once you have found the directory you'd like to restore, hover over the directory and click the restore icon and select "Restore to path". The restore location options are +## Finding the Right Snapshot - 1. Restore to a specific location, the default populated location will be the folder name + the first 8 digets of the snapshot's ID. - 2. If the location is left empty, Backrest will attempt to locate and restore to your Downloads directory. +Snapshots appear in Backrest's tree view, ordered by creation time, grouped under the plan (or repository) that created them. Click a snapshot to open a side panel showing its metadata, the operation history that produced it, and a **Snapshot Browser** for exploring its contents. -Once you have selected a location, click "Restore". Backrest will begin the restore process as a new operation visible at the top of the operation tree. You can monitor the progress of the restore in the operation tree. +Operation tree view with a snapshot selected -Restore Progress +### If Snapshots Are Missing: Index Them +Backrest indexes snapshots automatically when you add a repository and after each backup it runs. If snapshots were created outside Backrest (by the restic CLI, or by another machine writing to the same repository), click **Index Snapshots** in the repository view to import them. + +Index Snapshots button in the repository view + +## Browsing a Snapshot + +Open the **Snapshot Browser** from the snapshot's panel and navigate to the files or directories you want back. + +Snapshot browser expanded showing directories and files inside a snapshot + +::: warning +With remote storage, browsing can be slow because restic fetches pack files on demand to reconstruct the snapshot's directory structure. Expect a delay on the first expansion of large directories. +::: + +## Restoring to a Path + +Hover over a file or directory in the browser, click the restore icon, and choose **Restore to path**: + +Restore dialog with target path options + +- **Specific location**: the dialog pre-fills a path based on the folder name plus the first 8 characters of the snapshot ID. You can change it to any path. Restoring to a fresh directory and moving files into place afterwards is safer than restoring over live data. +- **Left empty**: Backrest restores to a timestamped folder in your Downloads directory (e.g. `~/Downloads/restic-restore-2026-07-12T10-30-00`). + +The target directory must not already exist; Backrest will not overwrite an existing directory. + +Click **Restore**. The restore runs as a tracked operation at the top of the operation tree, with live progress (bytes and files restored): + +Restore operation showing progress + +::: info Restoring in Docker +Restore paths are **inside the container**. To get files onto the host, restore to a directory under one of your bind mounts (e.g. `/userdata/restored`), or use the download option below. +::: + +## Downloading Files Directly + +Files can also be retrieved through the browser instead of being restored to the server's filesystem: + +- After a restore completes, the operation offers a **download** link that packages the restored files as a `.tar.gz` archive through your browser. +- Individual files can be downloaded straight from the snapshot browser. Directories arrive as `.tar` archives (Backrest streams them via `restic dump` under the hood). + +This is useful when Backrest runs on a NAS or remote server and you need a few files on the machine you are browsing from. + +## Restoring on a Different Machine + +Backrest repositories are standard restic repositories, so there are two recovery paths that do not depend on the original machine: + +1. **Another Backrest instance**: install Backrest anywhere, add the repository with the same URI and password, click **Index Snapshots**, and restore from the UI. +2. **The restic CLI**: point restic at the repository directly, e.g.: + +```bash +export RESTIC_REPOSITORY=s3:s3.amazonaws.com/my-bucket/backrest-repo +export RESTIC_PASSWORD='your-repo-password' +restic snapshots +restic restore latest --target /tmp/restored +``` + +Recovery requires only the repository location, its password, and any storage credentials. All three are recorded in your [Backrest config](/docs/configuration), which is a good reason to keep a copy of it somewhere safe. + +## Verifying Restored Data + +After a restore, spot-check the results: open a few files, compare directory sizes against the source, and check file counts in the restore operation's summary. For ongoing verification of repository integrity, schedule **check** operations; see [Retention & Repo Health](/guides/repo-health). diff --git a/docs/src/public/logo.svg b/docs/src/public/logo.svg new file mode 100644 index 00000000..70991046 --- /dev/null +++ b/docs/src/public/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/src/public/screenshots/add-plan-view.png b/docs/src/public/screenshots/add-plan-view.png index 7f0d34c2..9dbdb31d 100644 --- a/docs/src/public/screenshots/add-plan-view.png +++ b/docs/src/public/screenshots/add-plan-view.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:61c887f52049232be557822eb38eff8d0670e033af03af57895e1595697b47e0 -size 171653 +oid sha256:f62a502938aaff921b9cad3048bcda5aab928c90a6e6d35cf69ae93930db1088 +size 135226 diff --git a/docs/src/public/screenshots/add-repo-view.png b/docs/src/public/screenshots/add-repo-view.png index b52a065d..bcc42f10 100644 --- a/docs/src/public/screenshots/add-repo-view.png +++ b/docs/src/public/screenshots/add-repo-view.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bbf9898e9e894e6fbe18ae4f73ad7ad95e208114fc7da59b727d8c4ebfa7bd98 -size 211442 +oid sha256:f472cb371b46a1a9f82e90455d6a753cfd21094b6c8e9432ff0d6f762ec81cce +size 228067 diff --git a/docs/src/public/screenshots/discord-hook.png b/docs/src/public/screenshots/discord-hook.png new file mode 100644 index 00000000..bffcd045 --- /dev/null +++ b/docs/src/public/screenshots/discord-hook.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1ecce51671b7b1ae0d0fcec57eba166865551724f21022861a1636ac02784d36 +size 48379 diff --git a/docs/src/public/screenshots/index-snapshots-btn.png b/docs/src/public/screenshots/index-snapshots-btn.png index f4f027ab..9df4e6f5 100644 --- a/docs/src/public/screenshots/index-snapshots-btn.png +++ b/docs/src/public/screenshots/index-snapshots-btn.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:151d4d13af376f58ba20ae5405bc8fe319c790d4bba52c2eb33febb59d156911 -size 243083 +oid sha256:7a325ebbfc802fc771ca95ea2cf06fdc6e0e79f8f8d014beca17fd187abdf1fc +size 138110 diff --git a/docs/src/public/screenshots/repo-policies.png b/docs/src/public/screenshots/repo-policies.png new file mode 100644 index 00000000..c91330e7 --- /dev/null +++ b/docs/src/public/screenshots/repo-policies.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f97c30dc42852afd7f72d9aa12507c59702c4db6f0cb7c1bdc37f354aebfd02 +size 161673 diff --git a/docs/src/public/screenshots/restore-dialog.png b/docs/src/public/screenshots/restore-dialog.png index 673565be..424389ae 100644 --- a/docs/src/public/screenshots/restore-dialog.png +++ b/docs/src/public/screenshots/restore-dialog.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a03b669b06cf8816987d13960801b715982349643d2c6e4aafc3700daf1b28f6 -size 72138 +oid sha256:1c31cc38e4e8ef665a933b0291d524b463b2bc59e4fa4f5b12ccebf879f43085 +size 77952 diff --git a/docs/src/public/screenshots/restore-progress.png b/docs/src/public/screenshots/restore-progress.png index 2c4b6fa1..05f59f32 100644 --- a/docs/src/public/screenshots/restore-progress.png +++ b/docs/src/public/screenshots/restore-progress.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6954a6f1f8aae635552fdada63b97205a433fe006a0abf8a1329364f4ddf6a91 -size 327095 +oid sha256:5438f4388f172b8d50f10a830fac62aecdb48033b96b5e847d38ed2dcbe7ea6d +size 74616 diff --git a/docs/src/public/screenshots/retention-policy.png b/docs/src/public/screenshots/retention-policy.png new file mode 100644 index 00000000..5cf95c29 --- /dev/null +++ b/docs/src/public/screenshots/retention-policy.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e206867131e1f058b38fb748b7aa602a1c795cb0270d82904128d06978a1fc2a +size 101267 diff --git a/docs/src/public/screenshots/schedule-form.png b/docs/src/public/screenshots/schedule-form.png new file mode 100644 index 00000000..089fa671 --- /dev/null +++ b/docs/src/public/screenshots/schedule-form.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dec12fe205fdfee6b63acdbc4d9e49c06bd8af6c65e34deaf26637941256f87a +size 101288 diff --git a/docs/src/public/screenshots/settings-view.png b/docs/src/public/screenshots/settings-view.png index 62ae0bdf..b270f516 100644 --- a/docs/src/public/screenshots/settings-view.png +++ b/docs/src/public/screenshots/settings-view.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:915d6571839e93221809b0e9b02debb885b7fc61ed7a9f174ad91ec7c93c256b -size 42234 +oid sha256:3521c53b023473efa95d0121c8a392f4a6d8c5f509465be3cf3796e7987ddf91 +size 138511 diff --git a/docs/src/public/screenshots/sftp-repo-setup.png b/docs/src/public/screenshots/sftp-repo-setup.png new file mode 100644 index 00000000..fd8287b5 --- /dev/null +++ b/docs/src/public/screenshots/sftp-repo-setup.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee37e7326d81a90754f4c985ac52ff33280f83c33fceb9f25024a9707247ca25 +size 192827 diff --git a/docs/src/public/screenshots/snapshot-browser.png b/docs/src/public/screenshots/snapshot-browser.png new file mode 100644 index 00000000..dd522c1d --- /dev/null +++ b/docs/src/public/screenshots/snapshot-browser.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de8c59dac91b11dd4b25b8875a0853a3f1833159345bdc0d3a4b745f19703b86 +size 135825 diff --git a/docs/src/public/screenshots/summary-dashboard.png b/docs/src/public/screenshots/summary-dashboard.png new file mode 100644 index 00000000..032ea5d1 --- /dev/null +++ b/docs/src/public/screenshots/summary-dashboard.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a22a01f2bc5437fa4cedb5a26b2b239ec8fba506b881de44f7b24f95412382a1 +size 203583 diff --git a/docs/src/public/screenshots/tree-view-for-restore-article.png b/docs/src/public/screenshots/tree-view-for-restore-article.png index 05e544c8..7c7a6689 100644 --- a/docs/src/public/screenshots/tree-view-for-restore-article.png +++ b/docs/src/public/screenshots/tree-view-for-restore-article.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e51bb7e336d8216856858644d9f9542077e4acb446996969d7eec2fe68310f62 -size 346262 +oid sha256:dde030a52214a4f32bf5b921b6c214ce28f968e855e87a8a7ed944766ef04e0c +size 272465 diff --git a/webui/e2e/specs/docs-screenshots.spec.ts b/webui/e2e/specs/docs-screenshots.spec.ts new file mode 100644 index 00000000..15f0d4db --- /dev/null +++ b/webui/e2e/specs/docs-screenshots.spec.ts @@ -0,0 +1,413 @@ +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; +import { create } from '@bufbuild/protobuf'; +import type { Locator, Page } from '@playwright/test'; +import { test, expect } from '../harness/fixtures'; +import { backrestClient, seedInstance, seedRepo, seedPlan } from '../harness/seed'; +import type { BackrestInstance } from '../harness/backrest'; +import { BackupRequestSchema, GetOperationsRequestSchema } from '../../gen/ts/v1/service_pb'; +import { OperationStatus } from '../../gen/ts/v1/operations_pb'; + +/** + * Captures the screenshots embedded in the docs site (docs/src/public/screenshots). + * + * Not part of the regular e2e suite: it only runs when DOCS_SCREENSHOTS=1 is + * set, because it produces image artifacts rather than assertions. To + * regenerate the docs screenshots: + * + * cd webui + * DOCS_SCREENSHOTS=1 pnpm exec playwright test docs-screenshots + * + * Output lands in DOCS_SCREENSHOTS_DIR (default: e2e/.cache/docs-screenshots); + * review the images, then copy them into docs/src/public/screenshots/. + */ + +const OUT_DIR = + process.env.DOCS_SCREENSHOTS_DIR ?? path.join(__dirname, '..', '.cache', 'docs-screenshots'); + +test.skip(!process.env.DOCS_SCREENSHOTS, 'set DOCS_SCREENSHOTS=1 to capture docs screenshots'); + +test.use({ + viewport: { width: 1440, height: 900 }, + deviceScaleFactor: 2, + colorScheme: 'light', +}); + +async function save(target: Page | Locator, name: string, clip?: Clip): Promise { + await fs.mkdir(OUT_DIR, { recursive: true }); + const file = path.join(OUT_DIR, name); + if ('screenshot' in target && clip && isPage(target)) { + await target.screenshot({ path: file, clip }); + } else { + await (target as Locator).screenshot({ path: file }); + } +} + +interface Clip { + x: number; + y: number; + width: number; + height: number; +} + +function isPage(t: Page | Locator): t is Page { + return typeof (t as Page).goto === 'function'; +} + +/** + * Close-up of one section of a modal that has a section nav on the left: + * clicks the nav entry (which scrolls that section into view), then clips the + * modal's content pane (right of the nav column, between header and footer). + */ +async function scrollDialogToSection( + page: Page, + dialog: Locator, + navText: string, +): Promise { + const nav = dialog.getByText(navText, { exact: true }).first(); + await nav.click({ timeout: 5_000 }); + await page.waitForTimeout(600); // allow the section scroll to settle + return nav; +} + +async function saveDialogSection( + page: Page, + dialog: Locator, + navText: string, + name: string, +): Promise { + const nav = await scrollDialogToSection(page, dialog, navText); + const dlgBox = await dialog.boundingBox(); + const navBox = await nav.boundingBox(); + const cancelBox = await dialog + .getByRole('button', { name: 'Cancel' }) + .first() + .boundingBox(); + if (!dlgBox || !navBox || !cancelBox) { + await save(dialog, name); // fallback: the whole dialog + return; + } + const left = navBox.x + navBox.width + 12; + const top = dlgBox.y + 64; // below the modal header bar + const bottom = cancelBox.y - 16; // above the footer buttons + await save(page, name, { + x: left, + y: top, + width: dlgBox.x + dlgBox.width - left, + height: bottom - top, + }); +} + +/** Runs a real backup via RPC and waits for the indexed snapshot (mirrors restore.spec). */ +async function runBackupViaApi( + inst: BackrestInstance, + planId: string, + timeoutMs = 90_000, +): Promise { + const client = backrestClient(inst); + await client.backup(create(BackupRequestSchema, { value: planId })); + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + const resp = await client.getOperations( + create(GetOperationsRequestSchema, { selector: { planId }, lastN: 100n }), + ); + let backupOk = false; + let indexed = false; + for (const op of resp.operations) { + if (op.op.case === 'operationBackup' && op.status === OperationStatus.STATUS_SUCCESS) + backupOk = true; + if (op.op.case === 'operationIndexSnapshot') indexed = true; + if (op.op.case === 'operationBackup' && op.status === OperationStatus.STATUS_ERROR) + throw new Error(`backup for ${planId} failed: ${op.displayMessage}`); + } + if (backupOk && indexed) return; + await new Promise((r) => setTimeout(r, 500)); + } + throw new Error(`backup for ${planId} did not complete within ${timeoutMs}ms`); +} + +/** Demo files that read naturally in a snapshot-browser screenshot. */ +const DEMO_FILES: Record = { + 'notes/meeting-notes.md': '# Meeting notes\n\n- ship the docs\n', + 'notes/ideas.md': '# Ideas\n', + 'projects/report-2026.txt': 'Quarterly report draft.\n', + 'projects/budget.csv': 'item,cost\nbackups,0\n', + 'recipes.txt': 'pancakes: flour, eggs, milk\n', +}; + +test.describe('docs screenshots', () => { + test('first-run settings modal', async ({ page, backrest }) => { + // No seeding: the initial-setup Settings modal opens on first load. + await page.goto(backrest.url); + const dialog = page.getByRole('dialog'); + await expect(dialog).toBeVisible(); + await dialog.getByTestId('settings-instance-id').fill('my-backrest'); + await save(dialog, 'settings-view.png'); + }); + + test('add repo, sftp setup, add plan, discord hook modals', async ({ page, backrest }) => { + // Tall viewport so full-height modals (and the hook card at the bottom of + // the Add Plan dialog) fit without clipping. + await page.setViewportSize({ width: 1440, height: 1400 }); + await seedInstance(backrest, 'my-backrest'); + await page.goto(backrest.url); + + // --- Add Repo modal, filled out for a local repository. ---------------- + // (The 'mydrive' repo is seeded via the API only *after* this shot, so the + // name typed here does not trigger the duplicate-name validation error.) + await page.getByTestId('sidebar-add-repo').click(); + let dialog = page.getByRole('dialog'); + await expect(dialog).toBeVisible(); + await dialog.getByTestId('add-repo-name').fill('mydrive'); + await dialog.getByTestId('add-repo-uri').fill('/mnt/backup-drive/backrest-repo'); + await dialog.getByTestId('add-repo-name').click(); // dismiss URI autocomplete + await dialog.getByTestId('add-repo-password').fill('correct-horse-battery-staple'); + await save(dialog, 'add-repo-view.png'); + + // Close-up: the repo Scheduling section (prune + check policies). + try { + await saveDialogSection(page, dialog, 'Scheduling', 'repo-policies.png'); + } catch { + /* layout drift: skip the close-up, the full modal shot still exists */ + } + + // --- Same modal with an SFTP URI: the SFTP config + key helper. -------- + await scrollDialogToSection(page, dialog, 'Connection').catch(() => {}); + await dialog.getByTestId('add-repo-uri').fill('sftp://backup@nas.local:22/srv/backrest-repo'); + await dialog.getByTestId('add-repo-name').click(); + await dialog.getByText('Setup SSH Key (Optional)').click(); + await save(dialog, 'sftp-repo-setup.png'); + await page.keyboard.press('Escape'); + await expect(page.getByRole('dialog')).toHaveCount(0); + + // --- Add Plan modal, filled out, plus a Discord notification hook. ----- + await seedRepo(backrest, 'mydrive'); + await page.reload(); + await page.getByTestId('sidebar-add-plan').click(); + dialog = page.getByRole('dialog'); + await expect(dialog).toBeVisible(); + await dialog.getByTestId('add-plan-name').fill('mydrive-documents'); + await dialog.getByTestId('add-plan-repo-select').click(); + await page.getByRole('option', { name: 'mydrive', exact: true }).click(); + await dialog.getByTestId('add-plan-path-add').click(); + await dialog.getByTestId('add-plan-path-input').last().fill('/home/alice/Documents'); + await dialog.getByTestId('add-plan-name').click(); // dismiss path autocomplete + await save(dialog, 'add-plan-view.png'); + + // Close-ups: the schedule and retention sections. + try { + await saveDialogSection(page, dialog, 'Schedule', 'schedule-form.png'); + await saveDialogSection(page, dialog, 'Retention', 'retention-policy.png'); + } catch { + /* skip close-ups */ + } + + // Discord hook: add it, pick conditions, fill a placeholder webhook URL. + await dialog.getByTestId('hooks-add').click(); + await page.getByRole('menuitem', { name: 'Discord', exact: true }).click(); + await dialog.getByText('Runs when...').click(); + await page.getByRole('option', { name: /CONDITION_ANY_ERROR/ }).click(); + await page.getByRole('option', { name: /CONDITION_SNAPSHOT_SUCCESS/ }).click(); + await dialog.getByTestId('add-plan-name').click(); // dismiss the multi-select + // Fill the webhook URL field: the first text input below the conditions box. + const hookCondBox = await dialog.getByTestId('hook-conditions').boundingBox(); + if (hookCondBox) { + const hookInputs = dialog.locator('input'); + const count = await hookInputs.count(); + for (let i = 0; i < count; i++) { + const box = await hookInputs.nth(i).boundingBox(); + if (box && box.y > hookCondBox.y) { + await hookInputs + .nth(i) + .fill('https://discord.com/api/webhooks/1234567890/example-token'); + break; + } + } + } + // Close-up of the configured Discord hook card. The Add Plan modal has no + // "Hooks" nav entry (hooks live under "Advanced"), so frame the card + // itself: content pane right of the nav column, vertically around the + // hook's conditions select. + try { + const navBox = await dialog.getByText('Details', { exact: true }).first().boundingBox(); + const dlgBox = await dialog.boundingBox(); + const cond = dialog.getByTestId('hook-conditions'); + // Scroll to the hook card's LAST field (the template textarea) so the + // whole card is inside the dialog's visible scroll area, then measure. + const lastField = dialog.locator('textarea').last(); + await lastField.scrollIntoViewIfNeeded().catch(() => cond.scrollIntoViewIfNeeded()); + await page.waitForTimeout(300); + const condBox = await cond.boundingBox(); + const lastBox = await lastField.boundingBox().catch(() => null); + const cancelBox = await dialog + .getByRole('button', { name: 'Cancel' }) + .first() + .boundingBox(); + if (!navBox || !dlgBox || !condBox || !cancelBox) throw new Error('no boxes'); + const left = navBox.x + navBox.width + 12; + const top = Math.max(dlgBox.y + 64, condBox.y - 120); + const cardBottom = lastBox ? lastBox.y + lastBox.height + 24 : condBox.y + 380; + const bottom = Math.min(cancelBox.y - 16, cardBottom); + await save(page, 'discord-hook.png', { + x: left, + y: top, + width: dlgBox.x + dlgBox.width - left, + height: bottom - top, + }); + } catch { + await save(dialog, 'discord-hook.png'); + } + await page.keyboard.press('Escape'); + }); + + test('dashboard, operations, snapshot browser, restore', async ({ page, backrest }) => { + test.setTimeout(300_000); + + // --- Seed: instance, repo, plan over demo files; run two real backups. - + await seedInstance(backrest, 'my-backrest'); + await seedRepo(backrest, 'mydrive'); + // Prefer a reader-friendly path over the harness tmp dir — it shows up in + // the snapshot browser, restore dialog, and plan config screenshots. + let dataPath = '/tmp/demo/home/alice/Documents'; + try { + for (const [rel, content] of Object.entries(DEMO_FILES)) { + const abs = path.join(dataPath, rel); + await fs.mkdir(path.dirname(abs), { recursive: true }); + await fs.writeFile(abs, content); + } + } catch { + dataPath = await backrest.makeTestData(DEMO_FILES); + } + await seedPlan(backrest, 'mydrive-documents', 'mydrive', [dataPath]); + await runBackupViaApi(backrest, 'mydrive-documents'); + await fs.writeFile(path.join(dataPath, 'notes', 'todo.md'), '# Todo\n- test restores\n'); + await runBackupViaApi(backrest, 'mydrive-documents'); + + // --- Summary dashboard. ------------------------------------------------- + await page.goto(backrest.url); + await expect(page.getByTestId('sidebar-item-plan-mydrive-documents')).toBeVisible(); + await page.waitForTimeout(2_000); // let dashboard cards/charts settle + await save(page, 'summary-dashboard.png'); + + // --- List view (fresh load, so no hidden tree-tab panels linger). -------- + await page.goto(`${backrest.url}/#/plan/mydrive-documents`); + await page.waitForTimeout(1_500); + await page.getByRole('tab', { name: 'List View' }).click(); + const rows = page.locator('[data-testid="operation-row"]'); + await expect(rows.first()).toBeVisible({ timeout: 30_000 }); + + // Diagnostic: record what rows exist and their geometry, so a failure of + // any selector below is explainable from OUT_DIR/debug-rows.json. + const rowDebug = await page.evaluate(() => + Array.from(document.querySelectorAll('[data-testid="operation-row"]')).map((el) => ({ + opType: el.getAttribute('data-op-type'), + status: el.getAttribute('data-status'), + rect: el.getBoundingClientRect().toJSON(), + })), + ); + await fs.mkdir(OUT_DIR, { recursive: true }); + await fs.writeFile(path.join(OUT_DIR, 'debug-rows.json'), JSON.stringify(rowDebug, null, 2)); + + const backupRow = rows.filter({ hasText: '- Backup' }).first(); + try { + await save(backupRow, 'backup-operation.png'); + } catch { + /* no standalone backup row in this layout: skip */ + } + + // --- Snapshot browser: expand down to the demo files. -------------------- + // Click the accordion trigger by its accessible role; if Playwright's + // actionability check stalls, dispatch the click directly. + const browserTrigger = page.getByRole('button', { name: 'Snapshot Browser' }).first(); + await expect(browserTrigger).toBeAttached({ timeout: 15_000 }); + try { + await browserTrigger.click({ timeout: 5_000 }); + } catch { + await browserTrigger.dispatchEvent('click'); + } + const snapshotRow = rows.filter({ hasText: 'Snapshot Browser' }).first(); + const fileLoc = page.getByTestId('snapshot-browser-entry').filter({ hasText: 'recipes.txt' }); + for (const seg of dataPath.split('/').filter(Boolean)) { + if ((await fileLoc.count()) > 0) break; + const dir = page.getByTestId('snapshot-browser-entry').filter({ hasText: seg }).first(); + await dir.waitFor({ state: 'visible', timeout: 20_000 }); + await dir.click(); + await page.waitForTimeout(750); + } + await expect(fileLoc.first()).toBeVisible({ timeout: 20_000 }); + // Also expand a subdirectory so the shot shows files at two levels. + const notesDir = page.getByTestId('snapshot-browser-entry').filter({ hasText: 'notes' }); + if ((await notesDir.count()) > 0) { + await notesDir.first().click(); + await page.waitForTimeout(750); + } + try { + await save(snapshotRow, 'snapshot-browser.png'); + } catch { + await save(page, 'snapshot-browser.png'); + } + + // --- Restore dialog (not submitted with these values). ------------------- + await fileLoc.first().getByRole('button').click(); + await page.getByTestId('snapshot-restore').click(); + const restoreDialog = page.getByRole('dialog'); + await expect(restoreDialog).toBeVisible(); + await save(restoreDialog, 'restore-dialog.png'); + + // --- Run the restore for real; capture the completed operation. ---------- + // Prefer a reader-friendly destination (it is displayed in the operation + // details); clear leftovers from prior runs since restore refuses to + // overwrite an existing directory. + let restoreTarget = '/tmp/demo/home/alice/restored-files'; + try { + await fs.rm(restoreTarget, { recursive: true, force: true }); + } catch { + restoreTarget = path.join(backrest.dataDir, 'restore-target'); + } + const targetInput = restoreDialog.locator('input').first(); + await targetInput.click(); + await targetInput.fill(restoreTarget); + await targetInput.blur(); + await restoreDialog.getByRole('button', { name: 'Restore' }).click(); + await restoreDialog.getByRole('button', { name: 'Confirm Restore?' }).click(); + const restoreRow = page.locator( + '[data-testid="operation-row"][data-op-type="Restore"][data-status="success"]', + ); + await expect(restoreRow).toBeVisible({ timeout: 60_000 }); + await save(restoreRow, 'restore-progress.png'); + + // --- Plan view: operation tree (fresh load; default tab). ---------------- + await page.reload(); + await page.waitForTimeout(1_500); + try { + await page + .getByRole('treeitem', { name: /Backup .*ID:/ }) + .first() + .click({ timeout: 5_000 }); + await page.waitForTimeout(1_000); + } catch { + /* tree layout drift: capture unselected state */ + } + await save(page, 'tree-view-for-restore-article.png'); + + // --- Repo view: header with Index Snapshots + the maintenance menu open. - + await page.goto(`${backrest.url}/#/repo/mydrive`); + await expect(page.getByRole('heading', { name: 'mydrive' })).toBeVisible(); + await page.getByRole('button', { name: 'More actions' }).click(); + await page.waitForTimeout(500); + await save(page, 'index-snapshots-btn.png', { x: 0, y: 0, width: 1440, height: 420 }); + await page.keyboard.press('Escape'); + + // --- Repo stats panel (best effort; needs a stats op). ------------------- + try { + await page.getByRole('button', { name: 'More actions' }).click({ timeout: 5_000 }); + await page.getByRole('menuitem', { name: /Stats/ }).click({ timeout: 5_000 }); + await page.waitForTimeout(8_000); // small repo: stats completes quickly + await page.getByTestId('view-tab-stats').click(); + await page.waitForTimeout(2_000); + await save(page, 'stats-panel.png'); + } catch { + /* stats view unavailable: skip */ + } + }); +}); diff --git a/webui/pnpm-workspace.yaml b/webui/pnpm-workspace.yaml new file mode 100644 index 00000000..24a72cab --- /dev/null +++ b/webui/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +allowBuilds: + '@parcel/watcher': true + esbuild: true diff --git a/webui/src/api/streams/sharedStream.ts b/webui/src/api/streams/sharedStream.ts index 9ce5ab3c..aa86b5d6 100644 --- a/webui/src/api/streams/sharedStream.ts +++ b/webui/src/api/streams/sharedStream.ts @@ -143,9 +143,11 @@ class SharedStreamImpl implements SharedStream { this.leaderStreamLoop(signal), ); } catch (err) { - // AbortError just means we left the queue; otherwise log and re-contend. + // AbortError just means we left the queue; otherwise log and re-contend + // after a backoff so a persistently-rejecting lock API can't busy-loop. if ((err as Error)?.name !== "AbortError") { console.warn(`[sharedStream:${this.opts.name}] lock error`, err); + await abortableDelay(this.backoffMs, runSignal); } } } @@ -162,7 +164,8 @@ class SharedStreamImpl implements SharedStream { this.goLive(); } this.deliverMessage(msg); - this.post({ t: "m", d: this.opts.encode(msg) }); + // Encode only to feed the bus; the fallback path has no channel. + if (this.bc) this.post({ t: "m", d: this.opts.encode(msg) }); } } catch (err) { if (!signal.aborted) {