Zerobyte

Backup Webhooks

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: 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:

HookWhen it runsFailure behavior
Pre-backup webhookBefore Restic starts reading the volumeA failed request stops the backup before Restic runs
Post-backup webhookAfter Restic finishes, fails, or is cancelledA 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:

{
  "phase": "pre",
  "event": "backup.pre",
  "jobId": "job_...",
  "scheduleId": "sched_...",
  "organizationId": "org_...",
  "sourcePath": "/data"
}

Post-backup webhook example:

{
  "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:

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:

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 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:

sudo apt-get update
sudo apt-get install webhook

webhook serves configured hooks at /hooks/<hook-id>. 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:

sudo mkdir -p /opt/zerobyte-hooks

Create /opt/zerobyte-hooks/stop-postgres.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:

#!/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:

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:

[
  {
    "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:

sudo webhook -hooks /opt/zerobyte-hooks/hooks.json -port 9000 -verbose -http-methods POST

In another shell, test both hooks:

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:

services:
  zerobyte:
    extra_hosts:
      - "host.docker.internal:host-gateway"
    environment:
      - WEBHOOK_ALLOWED_ORIGINS=http://host.docker.internal:9000

Restart Zerobyte:

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:

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:

[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:

sudo systemctl daemon-reload
sudo systemctl enable --now zerobyte-webhook.service
sudo systemctl status zerobyte-webhook.service

Check logs with:

sudo journalctl -u zerobyte-webhook.service -f