Skip to content


Captain hook is a Python WSGI application that handles webhooks from gitea and github.

Forked from carlos-jenkins/python-github-webhooks.

To install captain hook:

git clone
cd b-captain-hook
pip install -r requirements.txt
cp config.example.json config.json
# edit config.json

Run it standalone:


Run it with docker:

docker-compose build
docker-compose up -d
docker-compose down

What is Captain Hook?

Captain Hook is a flask webhook server that runs via docker.

See What is Captain Hook? for an explanation.

Configuring Captain Hook

Captain Hook requires a config file. See config.example.json in this directory, and the Captain Hook Config page for more info.

Starting Captain Hook

See Starting Captain Hook for how to set up the various startup services and docker pods that are required to run Captain Hook.

Adding Hooks

Captain Hook accepts incoming webhooks and checks for scripts in hooks/ that match the action performed on the given repository.

A webhook script is anything that is executable.

To add a new script to be triggered on a particular webhook, put it in the hooks/ directory and follow the naming convention:


For example, suppose I performed a push action to a repository named happy-giraffe, and I was pushing commits to the gh-pages branch. The webhook sent to Captain Hook would contain this information, and Captain Hook would look for any webhooks with the following names, and run them if present:


The application passes the path to a JSON file, while holding the payload for the request as the first argument.

The application will pass in two pieces of information:

  • Path to JSON file holding payload - the first part of the request is a JSON file that holds the payload for the request.

  • The other piece of information passed is the name of the action (e.g., push)

The application will pass to the hooks the path to a JSON file holding the payload for the request as first argument. The event type will be passed as second argument. For example:

    hooks/push-happy-giraffe-master /tmp/sXFHji push

Hooks can be written in any scripting language as long as the file is executable and has a shebang. A simple example in Python could be:

    #!/usr/bin/env python
    # Python Example for Python GitHub Webhooks
    # File: push-happy-giraffe-master

    import sys
    import json

    with open(sys.argv[1], 'r') as jsf:
      payload = json.loads(

    ### Do something with the payload
    name = payload['repository']['name']
    outfile = '/tmp/hook-{}.log'.format(name)

    with open(outfile, 'w') as f:

Not all events have an associated branch, so a branch-specific hook cannot fire for such events. For events that contain a pull_request object, the base branch (target for the pull request) is used, not the head branch.

The payload structure depends on the event type. Please review:

Docker Deployment

Dockerfile defines the image, but use the docker-compose.yml file instead.

To build, start, and stop:

docker-compose build --no-cache
docker-compose up
docker-compose down

Base Docker Image

Captain Hook uses the python3-alpine-flask container, which is a very lightweight container with python 3 available and with flask installed.

See nikos/python3-alpine-flask-docker on Github.


Captain Hook binds to external port 5000 on whatever host it runs on.

Implementing a secret key is critical to keep captain hook from deploying webhook requests from random strangers.

Recommendations: You can implement validation of the IP address sending the webhook, but more important than validating the incoming IP address (which can always be spoofed) is to validate the secret.


THe docker container mounts the hooks/ directory in this repository to /app/hooks in the container.


NOTE: These scripts must be made executable with chmod +x or the webhook server will not do anything and be totally silent.

The docker container will also mount /www/ into the container, so all the static web content on the host (blackbeard) is available to the webhooks to perform updates and etc.

/www is mounted to the same place on the host and in the container, as indicated by this bind mount lin from the docker-compose file:



To test Captain Hook, install a webhook in a given repository and use the server running Captain Hook as the endpoint. Set up the secret.

You should be able to trigger a webhook test from the repository's webhooks control panel. This should trigger Captain Hook, and if you've added a script for the corresponding repository, it should be triggered by the test.

Keep in mind that Gitea/Github will only fire triggers on the master branch of a repository, so you can't test arbitrary branches using this method.


To get into test/debug mode:

  • Set up a "dummy" hook (the Python hook shown above) that will just log the webhook to a log file in /tmp
  • Run the server in one window
  • In a second window, open a shell in the container and monitor /tmp/*.log
  • In a third window, open a shell in the container and monitor /www/*

To open a shell inside the container, run this command from the host machine:

docker exec -it <name-of-container> /bin/sh

Remember that when you are inside this container, you will only have /bin/sh available - no bash.

To check logs of this container, run this from the host machine:

docker logs -f <container-name>

You can also run the container without sending it to the background,

docker-compose up

and this will show exceptions on the screen (but it won't show anything else useful...)


Original license (Carlos Jenkins) and forked license (Charles Reid) given in


This project is just the reinterpretation and merge of two approaches:



It is implemented with the help of python 3 alpine: