The delivery page shows a timeout and Jenkins logs nothing, which is the signature of a connection that never arrived. GitHub sends a webhook as an outbound HTTP POST from its own infrastructure to the payload URL you configured. A VPN protects Jenkins by refusing exactly that kind of connection. Nothing is broken on either side. The direction of the request is the problem.

What the delivery actually attempts

GitHub resolves the hostname in the payload URL and connects to it from the public internet. If that URL points at a private address, or at a name that only resolves inside your network, the attempt fails before any request is made. If it points at a public name your edge drops, the attempt times out instead.

The distinction matters for diagnosis. A DNS failure and a filtered port both look like nothing happened on the Jenkins side, and both appear on GitHub as a failed delivery with a different reason attached. Read that reason before you change the URL.

A tunnel that dials outward does not create a listener. That is the part people expect to work differently.

Three ways to fix the direction of the connection

Every fix changes who initiates the connection, and there are only three shapes.

ApproachWho initiatesWhat you give up
Relay serviceAn outbound client inside the networkA third party sees every payload
Reverse tunnel or ingressThe public edge, into a controlled listenerAn inbound surface you must secure
Runner or pollingThe agent, outbound to GitHubInstant delivery, replaced by a schedule

Polling is the least elegant and the easiest to operate. A relay keeps near-instant feedback without opening a port. A runner is usually the right answer when payload confidentiality rules out a third party, because the job can run on a machine that already sits inside the network.

Pick by what you can operate, not by what sounds modern.

Relay a delivery outward with smee

Create a channel on the relay service and keep its URL. Inside the network, install the client and forward deliveries to the path Jenkins expects. The payload URL in the repository settings becomes the channel URL, with the content type set to application/json.

sudo npm install --global smee-client
smee -u CHANNEL_URL --path /github-webhook/ --port 8080

Three details decide whether it works. The path must match what Jenkins expects for GitHub events, which is /github-webhook/ by default. Jenkins must be listening on the port you handed the client. And the Jenkins URL configured in the job has to agree with that path, or the event arrives and is then discarded.

A trailing slash mismatch produces a 404 that reads like a delivery failure and is not one.

Running it so it survives the week

A foreground client dies with the terminal, and the webhooks work for a day and then stop. Give it a unit with Restart=always, run it as the Jenkins user, and read the journal after the first real delivery. That last step is what separates a working relay from a configured one.

[Unit]
Description=smee webhook relay
After=network.target

[Service]
Type=simple
Restart=always
RestartSec=2
User=jenkins
ExecStart=/usr/bin/smee -u CHANNEL_URL --path /github-webhook/ --port 8080

[Install]
WantedBy=multi-user.target

Test with a real push rather than a replay button. A replay exercises the relay's stored payload, not the path from GitHub to your channel, so it can pass while live delivery still fails.

Alert when the unit stops. A relay that is down is silent by construction, and silence is what you are already debugging.

What the relay can see, and when not to use one

A relay terminates the payload, so it sees repository names, commit data and whatever else the event carries. Treat the channel URL as a public endpoint. Keep the Jenkins secret token configured so forged deliveries are rejected, and scope the token Jenkins uses to the minimum it needs.

If payload confidentiality rules that out, a self-hosted runner or an authenticated reverse tunnel is the answer.

A relay is infrastructure now, and it deserves the same review as the build it triggers. Rotate the channel if it leaks, and note the date you last verified a live delivery. Put that date in the runbook next to the channel URL. A relay nobody has reviewed for a year is a public endpoint nobody owns.

འབྲེལ་བའི་རྩོམ་ཡིག