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:
| 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:
{
"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/jsonHeader 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
- Add the webhook origin to
WEBHOOK_ALLOWED_ORIGINSin the Zerobyte environment. - Restart Zerobyte so the environment change is loaded.
- Open Backups and select the backup job.
- Edit the job and expand Advanced.
- Fill Pre-backup webhook or Post-backup webhook.
- Add any required headers.
- Leave the body empty unless the receiving service requires a custom payload.
- 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:9000How-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
postgrescontainer. - Restic backs up the mounted data.
- Post-backup hook starts the
postgrescontainer 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 webhookwebhook 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-hooksCreate /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"
fiCreate /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"
fiMake both scripts executable:
sudo chmod +x /opt/zerobyte-hooks/stop-postgres.sh /opt/zerobyte-hooks/start-postgres.shIf 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 POSTIn 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-postgresOnce 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:9000Restart Zerobyte:
docker compose up -d6. 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-secretLeave 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.targetEnable and start it:
sudo systemctl daemon-reload
sudo systemctl enable --now zerobyte-webhook.service
sudo systemctl status zerobyte-webhook.serviceCheck logs with:
sudo journalctl -u zerobyte-webhook.service -f