4.4 KiB
nas-webhook
Lightweight webhook container for NAS auto-deploy. Alpine-based with docker-cli and docker-cli-compose included so hooks can run docker compose pull && docker compose up -d.
Image
registry.gitlab.com/bgrolleman/nas-webhook:latest
Built automatically by GitLab CI on every push to main. Image is public — no registry login needed on the NAS to pull it.
How it works
- A GitLab CI pipeline builds and pushes a new image (e.g.
hugo_basg.nl:latest) - The CI pipeline sends a POST to
http://webhook.nas.basg.nl/hooks/<hook-id>with a secret token in theX-Webhook-Tokenheader - The webhook container receives the request, validates the token, and runs
deploy.sh <compose-dir> deploy.shdoesdocker compose pull && docker compose up -din the project directory
Deployment on NAS
Place the following files in a directory on the NAS (e.g. /volume1/docker/webhook/):
webhook/
├── docker-compose.yml
├── hooks.json
└── scripts/
└── deploy.sh
Then bring it up via the UGREEN web interface or docker compose up -d.
The Docker socket (/var/run/docker.sock) must be mounted so the container can run Docker commands. The NAS docker directory (/volume1/docker) must also be mounted so deploy.sh can reach the project compose files.
docker-compose.yml
networks:
external:
external: true
services:
webhook:
image: registry.gitlab.com/bgrolleman/nas-webhook:latest
restart: always
command: ["-hooks", "/config/hooks.json", "-verbose", "-hotreload"]
volumes:
- ./hooks.json:/config/hooks.json:ro
- ./scripts:/scripts:ro
- /var/run/docker.sock:/var/run/docker.sock
- /volume1/docker:/volume1/docker
expose:
- 9000
environment:
- VIRTUAL_HOST=webhook.nas.basg.nl
- VIRTUAL_PORT=9000
networks:
- external
Note: No LETSENCRYPT_HOST — running HTTP only. The ACME challenge (Let's Encrypt) doesn't work for this container because it only exposes port 9000, not 80. Since it's only called by GitLab CI (not a browser), HTTP is fine.
Adding a new project
1. Add a hook to hooks.json
{
"id": "my-project",
"execute-command": "/scripts/deploy.sh",
"command-working-directory": "/scripts",
"pass-arguments-to-command": [
{
"source": "string",
"name": "/volume1/docker/my-project"
}
],
"trigger-rule": {
"match": {
"type": "value",
"value": "YOUR_SECRET_TOKEN_HERE",
"parameter": {
"source": "header",
"name": "X-Webhook-Token"
}
}
}
}
Generate a token with: openssl rand -hex 32
The webhook container uses -hotreload so it picks up hooks.json changes without a restart.
2. Add deploy stage to the project's .gitlab-ci.yml
Store the token as a masked CI variable (e.g. DEPLOY_WEBHOOK_TOKEN) in the GitLab project settings.
deploy:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache curl
script:
- |
curl -f -X POST \
-H "X-Webhook-Token: $DEPLOY_WEBHOOK_TOKEN" \
http://webhook.nas.basg.nl/hooks/my-project
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
needs: [build]
3. Make sure the project image is public
The NAS pulls images without authentication. Either make the GitLab registry public (project → Settings → General → Visibility → Public) or set up a deploy token and run sudo docker login registry.gitlab.com on the NAS.
Testing a hook manually
curl -X POST \
-H "X-Webhook-Token: YOUR_TOKEN" \
http://webhook.nas.basg.nl/hooks/your-hook-id
Current hooks
| Hook ID | Project | Compose dir |
|---|---|---|
hugo-basg |
bgrolleman/hugo_basg.nl | /volume1/docker/basg-hugo |
Known issues / pitfalls
- No HTTPS: Let's Encrypt won't issue a cert because the container doesn't serve HTTP on port 80. Since only GitLab CI calls this endpoint, HTTP is acceptable. If HTTPS is needed in the future, put a separate nginx container in front that handles port 80/443 and proxies to port 9000.
- Docker socket permissions: The webhook runs as root inside the container (required to use the socket). This is standard for Docker-in-Docker patterns on a home NAS.
- Image must be public or NAS must be logged in: The deploy script runs
docker compose pullwhich needs registry access. Easiest solution is a public GitLab project.