From e7b9b118a87c3f8dba30ba2e9598e6b180d78e05 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Mon, 4 May 2026 08:12:24 +0200 Subject: [PATCH] docs: backup webhooks --- README.md | 1 + apps/docs/content/docs/concepts/backups.mdx | 8 + apps/docs/content/docs/configuration.mdx | 1 + .../content/docs/guides/backup-webhooks.mdx | 305 ++++++++++++++++++ apps/docs/content/docs/guides/meta.json | 1 + apps/docs/content/docs/installation.mdx | 1 + 6 files changed, 317 insertions(+) create mode 100644 apps/docs/content/docs/guides/backup-webhooks.mdx diff --git a/README.md b/README.md index 88aecf0b..37999202 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,7 @@ Zerobyte can be customized using environment variables. Below are the available | `TRUST_PROXY` | When `true`, trust an existing `X-Forwarded-For` header from your reverse proxy. Leave `false` for direct deployments. | `false` | | `TRUSTED_ORIGINS` | Comma-separated list of extra trusted origins for CORS (e.g., `http://localhost:3000,http://example.com`). | (none) | | `WEBHOOK_ALLOWED_ORIGINS` | Comma-separated list of HTTP origins allowed for backup webhooks and outbound HTTP notification destinations. | (none) | +| `WEBHOOK_TIMEOUT` | Timeout for backup webhook requests in seconds. | `60` | | `LOG_LEVEL` | Logging verbosity. Options: `debug`, `info`, `warn`, `error`. | `info` | | `SERVER_IDLE_TIMEOUT` | Idle timeout for the server in seconds. | `60` | | `RCLONE_CONFIG_DIR` | Path to the directory containing `rclone.conf` inside the container. Change this if running as a non-root user. | `/root/.config/rclone` | diff --git a/apps/docs/content/docs/concepts/backups.mdx b/apps/docs/content/docs/concepts/backups.mdx index 5ac023dd..f629eecc 100644 --- a/apps/docs/content/docs/concepts/backups.mdx +++ b/apps/docs/content/docs/concepts/backups.mdx @@ -114,6 +114,14 @@ Both methods produce identical snapshots. Manual runs are useful for verifying a You can monitor backup progress in real time through the web interface. Zerobyte streams file counts, data processed, and upload progress as the backup runs. +## Backup webhooks + +Backup jobs can run optional HTTP webhooks immediately before and after Restic. Pre-backup webhooks are useful for preparing the source, such as pausing a service or creating a dump. Post-backup webhooks are useful for cleanup, such as resuming a service after the snapshot. + +Pre-backup hook failures stop the backup before Restic runs. Post-backup hook failures are recorded with the run result; a clean backup becomes a warning if the post hook fails. + +For setup details and a container stop/start example, see [Backup Webhooks](/docs/guides/backup-webhooks). + ## Backup status The UI exposes two related status views: diff --git a/apps/docs/content/docs/configuration.mdx b/apps/docs/content/docs/configuration.mdx index 13204f34..eaacdb0b 100644 --- a/apps/docs/content/docs/configuration.mdx +++ b/apps/docs/content/docs/configuration.mdx @@ -38,6 +38,7 @@ Zerobyte is configured through environment variables and Docker Compose settings | `TRUST_PROXY` | Set to `true` to trust `X-Forwarded-For` headers from a reverse proxy. | `false` | | `TRUSTED_ORIGINS` | Comma-separated list of additional trusted origins for CORS. | (none) | | `WEBHOOK_ALLOWED_ORIGINS` | Comma-separated list of HTTP origins allowed for backup webhooks and outbound HTTP notification destinations. | (none) | +| `WEBHOOK_TIMEOUT` | Timeout for backup webhook requests in seconds. | `60` | | `LOG_LEVEL` | Logging verbosity: `debug`, `info`, `warn`, `error`. | `info` | | `SERVER_IDLE_TIMEOUT` | Server idle timeout in seconds. | `60` | | `RCLONE_CONFIG_DIR` | Path to the rclone config directory inside the container. | `/root/.config/rclone` | diff --git a/apps/docs/content/docs/guides/backup-webhooks.mdx b/apps/docs/content/docs/guides/backup-webhooks.mdx new file mode 100644 index 00000000..535b4202 --- /dev/null +++ b/apps/docs/content/docs/guides/backup-webhooks.mdx @@ -0,0 +1,305 @@ +--- +title: Backup Webhooks +description: Run HTTP hooks before and after a backup job +--- + +Backup webhooks let a backup job call an HTTP endpoint immediately before Restic starts and immediately after Restic finishes. Use them when the source needs a short runtime action around the backup, such as pausing a service, creating a database dump, flushing a cache, or resuming a container after the snapshot. + +Backup webhooks are configured per backup job in the **Advanced** section. They are different from [notifications](/docs/guides/notifications): notifications report backup events to people or systems, while backup webhooks are part of the backup execution lifecycle. + +## How backup webhooks work + +Zerobyte supports two lifecycle hooks: + +| Hook | When it runs | Failure behavior | +| --- | --- | --- | +| **Pre-backup webhook** | Before Restic starts reading the volume | A failed request stops the backup before Restic runs | +| **Post-backup webhook** | After Restic finishes, fails, or is cancelled | A failed request is recorded with the final result; a clean backup becomes a warning | + +Each hook sends a `POST` request. A response with a `2xx` status code is treated as success. Redirects are not followed. Webhook requests time out after `WEBHOOK_TIMEOUT` seconds, which defaults to 60 seconds. + + + Every backup webhook URL must use an origin listed in `WEBHOOK_ALLOWED_ORIGINS`. The origin is the scheme, hostname, and port, such as `http://host.docker.internal:9000`. + + +## Request body + +If the hook body field is empty, Zerobyte sends a JSON backup context body and sets `Content-Type: application/json`. + +Pre-backup webhook example: + +```json +{ + "phase": "pre", + "event": "backup.pre", + "jobId": "job_...", + "scheduleId": "sched_...", + "organizationId": "org_...", + "sourcePath": "/data" +} +``` + +Post-backup webhook example: + +```json +{ + "phase": "post", + "event": "backup.post", + "jobId": "job_...", + "scheduleId": "sched_...", + "organizationId": "org_...", + "sourcePath": "/data", + "status": "success" +} +``` + +`status` is only sent to the post-backup webhook. It can be `success`, `warning`, `error`, or `cancelled`. `error` is included on the post-backup webhook when Zerobyte has warning, failure, or cancellation details to report. + +If you enter a custom body, Zerobyte sends that exact body instead of the default JSON context. Add a `Content-Type` header yourself if the receiver expects one. + +## Headers + +Headers are optional and are entered one per line: + +```text +X-Zerobyte-Hook-Secret: replace-with-a-long-random-secret +Content-Type: application/json +``` + +Header values are stored as plain text. Use a scoped webhook secret rather than a reusable account password or long-lived infrastructure token. + +## Configure a backup hook + +1. Add the webhook origin to `WEBHOOK_ALLOWED_ORIGINS` in the Zerobyte environment. +2. Restart Zerobyte so the environment change is loaded. +3. Open **Backups** and select the backup job. +4. Edit the job and expand **Advanced**. +5. Fill **Pre-backup webhook** or **Post-backup webhook**. +6. Add any required headers. +7. Leave the body empty unless the receiving service requires a custom payload. +8. Save the backup job and run **Backup now** to test the lifecycle. + +For Docker Compose on Linux, `host.docker.internal` usually needs an explicit host gateway entry: + +```yaml docker-compose.yml +services: + zerobyte: + extra_hosts: + - "host.docker.internal:host-gateway" + environment: + - WEBHOOK_ALLOWED_ORIGINS=http://host.docker.internal:9000 +``` + +## How-to: stop and start a Postgres container with adnanh/webhook + +This example runs [`adnanh/webhook`](https://github.com/adnanh/webhook) on the Docker host. Zerobyte calls it before and after the backup: + +- Pre-backup hook stops the `postgres` container. +- Restic backs up the mounted data. +- Post-backup hook starts the `postgres` container again. + + + Stopping a database container is a blunt consistency strategy. Use it only when a short outage is acceptable. For larger databases, prefer native database dumps, replication snapshots, or storage-level snapshots. + + +### 1. Install webhook on the Docker host + +On Debian or Ubuntu: + +```bash +sudo apt-get update +sudo apt-get install webhook +``` + +`webhook` serves configured hooks at `/hooks/`. The default port is `9000`, and the `-hooks` flag points to the JSON or YAML hook file. + +### 2. Create hook scripts + +Create a directory for the scripts: + +```bash +sudo mkdir -p /opt/zerobyte-hooks +``` + +Create `/opt/zerobyte-hooks/stop-postgres.sh`: + +```sh +#!/bin/sh +set -eu + +CONTAINER=postgres + +STATE=$(docker inspect -f '{{.State.Running}}' "$CONTAINER") + +if [ "$STATE" = "true" ]; then + docker stop "$CONTAINER" +fi +``` + +Create `/opt/zerobyte-hooks/start-postgres.sh`: + +```sh +#!/bin/sh +set -eu + +CONTAINER=postgres + +STATE=$(docker inspect -f '{{.State.Running}}' "$CONTAINER") + +if [ "$STATE" != "true" ]; then + docker start "$CONTAINER" +fi +``` + +Make both scripts executable: + +```bash +sudo chmod +x /opt/zerobyte-hooks/stop-postgres.sh /opt/zerobyte-hooks/start-postgres.sh +``` + +If your container has a different name, change `CONTAINER=postgres` in both scripts. + +### 3. Create the webhook config + +Create `/opt/zerobyte-hooks/hooks.json`: + +```json +[ + { + "id": "stop-postgres", + "execute-command": "/opt/zerobyte-hooks/stop-postgres.sh", + "command-working-directory": "/opt/zerobyte-hooks", + "http-methods": ["POST"], + "include-command-output-in-response": true, + "trigger-rule": { + "match": { + "type": "value", + "value": "replace-with-a-long-random-secret", + "parameter": { + "source": "header", + "name": "X-Zerobyte-Hook-Secret" + } + } + } + }, + { + "id": "start-postgres", + "execute-command": "/opt/zerobyte-hooks/start-postgres.sh", + "command-working-directory": "/opt/zerobyte-hooks", + "http-methods": ["POST"], + "include-command-output-in-response": true, + "trigger-rule": { + "match": { + "type": "value", + "value": "replace-with-a-long-random-secret", + "parameter": { + "source": "header", + "name": "X-Zerobyte-Hook-Secret" + } + } + } + } +] +``` + +Use the same secret in both hook definitions. `include-command-output-in-response` makes `webhook` wait for the script and return an error response if the command fails, which lets Zerobyte stop the backup when the pre-backup hook cannot stop Postgres. + +### 4. Start webhook + +Run it in the foreground first: + +```bash +sudo webhook -hooks /opt/zerobyte-hooks/hooks.json -port 9000 -verbose -http-methods POST +``` + +In another shell, test both hooks: + +```bash +curl -X POST \ + -H "X-Zerobyte-Hook-Secret: replace-with-a-long-random-secret" \ + http://localhost:9000/hooks/stop-postgres + +curl -X POST \ + -H "X-Zerobyte-Hook-Secret: replace-with-a-long-random-secret" \ + http://localhost:9000/hooks/start-postgres +``` + +Once the test works, run `webhook` under your normal process manager. + +### 5. Allow Zerobyte to call the webhook server + +Add the webhook server origin to Zerobyte: + +```yaml docker-compose.yml +services: + zerobyte: + extra_hosts: + - "host.docker.internal:host-gateway" + environment: + - WEBHOOK_ALLOWED_ORIGINS=http://host.docker.internal:9000 +``` + +Restart Zerobyte: + +```bash +docker compose up -d +``` + +### 6. Add the hooks to the backup job + +Open the backup job in Zerobyte, edit it, and expand **Advanced**. + +Use these values: + +```text +Pre-backup webhook: http://host.docker.internal:9000/hooks/stop-postgres +Pre-backup webhook headers: +X-Zerobyte-Hook-Secret: replace-with-a-long-random-secret + +Post-backup webhook: http://host.docker.internal:9000/hooks/start-postgres +Post-backup webhook headers: +X-Zerobyte-Hook-Secret: replace-with-a-long-random-secret +``` + +Leave both body fields empty. Zerobyte will send the default JSON context body. + +Run **Backup now**. If the stop hook fails or returns a non-`2xx` response, Zerobyte fails the backup before Restic starts. If the start hook fails after Restic finishes, Zerobyte records the problem in the run details so you can restart the container manually. + +### 7. Run webhook as a service + +After the foreground test works, create a small systemd unit so `webhook` starts on boot. + +Create `/etc/systemd/system/zerobyte-webhook.service`: + +```ini +[Unit] +Description=Zerobyte backup webhook runner +After=network-online.target docker.service +Wants=network-online.target +Requires=docker.service + +[Service] +Type=simple +ExecStart=/usr/bin/webhook -hooks /opt/zerobyte-hooks/hooks.json -port 9000 -http-methods POST -verbose +Restart=on-failure +RestartSec=5s + +[Install] +WantedBy=multi-user.target +``` + +Enable and start it: + +```bash +sudo systemctl daemon-reload +sudo systemctl enable --now zerobyte-webhook.service +sudo systemctl status zerobyte-webhook.service +``` + +Check logs with: + +```bash +sudo journalctl -u zerobyte-webhook.service -f +``` + +import { Callout } from "fumadocs-ui/components/callout"; diff --git a/apps/docs/content/docs/guides/meta.json b/apps/docs/content/docs/guides/meta.json index 1f6a6767..7709de28 100644 --- a/apps/docs/content/docs/guides/meta.json +++ b/apps/docs/content/docs/guides/meta.json @@ -3,6 +3,7 @@ "pages": [ "3-2-1-backup-strategy", "restoring", + "backup-webhooks", "notifications", "recovery-key-and-repository-passwords", "repository-maintenance", diff --git a/apps/docs/content/docs/installation.mdx b/apps/docs/content/docs/installation.mdx index 2621de98..0068f235 100644 --- a/apps/docs/content/docs/installation.mdx +++ b/apps/docs/content/docs/installation.mdx @@ -115,6 +115,7 @@ The `BASE_URL` determines cookie security behavior: | `PORT` | Port the web interface listens on inside the container | `4096` | | `RESTIC_HOSTNAME` | Hostname used by Restic in snapshots | `zerobyte` | | `TRUSTED_ORIGINS` | Comma-separated list of additional trusted CORS origins | (none) | +| `WEBHOOK_TIMEOUT` | Timeout for backup webhook requests in seconds | `60` | | `LOG_LEVEL` | Logging verbosity: `debug`, `info`, `warn`, `error` | `info` | | `SERVER_IDLE_TIMEOUT` | Server idle timeout in seconds | `60` | | `RCLONE_CONFIG_DIR` | Path to rclone config directory inside container | `/root/.config/rclone` |