HTTPS for Webhooks with nginx and Let's Encrypt

Put your self-hosted webhook receiver behind nginx with a free auto-renewing Let's Encrypt certificate. Copy-paste guide with verified configs.

Webhook providers require an HTTPS endpoint. This guide takes a webhook receiver listening on localhost port 8000, such as Core Webhook Module, and puts it behind nginx with a free auto-renewing Let's Encrypt certificate. Ten minutes, one server, copy-paste commands.

Prerequisites

  • A Linux server (Ubuntu/Debian assumed below) with ports 80 and 443 reachable from the internet
  • A DNS A record pointing your hostname (for example hooks.example.com) at the server's IP
  • Your webhook receiver running on the server, listening on 127.0.0.1:8000

Step 1: Install nginx

sudo apt update && sudo apt install -y nginx

If you use UFW, allow web traffic:

sudo ufw allow 'Nginx Full'

Step 2: Create the reverse proxy config

Create /etc/nginx/sites-available/hooks.example.com (replace the hostname with yours):

server {
    listen 80;
    server_name hooks.example.com;

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        client_max_body_size 25m;
        proxy_read_timeout 30s;
    }
}

Two parameters matter for webhooks specifically:

  • client_max_body_size 25m - nginx defaults to 1 MB and returns 413 Request Entity Too Large for anything bigger. GitHub caps webhook payloads at 25 MB, so match that. Without this line, large pushes silently fail.
  • X-Forwarded-For - if your receiver does IP whitelisting (Core Webhook Module supports it), it needs the real client IP, not nginx's.

Enable the site and check the syntax:

sudo ln -s /etc/nginx/sites-available/hooks.example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

nginx -t must print syntax is ok and test is successful before you reload.

Step 3: Get the certificate

Install certbot via snap (the method Let's Encrypt currently recommends):

sudo apt-get remove certbot
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/local/bin/certbot

Request the certificate. Certbot edits your nginx config in place, adds the listen 443 ssl block, and sets up the HTTP to HTTPS redirect:

sudo certbot --nginx -d hooks.example.com

Renewal is automatic (certbot installs a systemd timer or cron job). Verify it works before you forget about it:

sudo certbot renew --dry-run

Step 4: Verify end to end

curl -i https://hooks.example.com/webhook/your_webhook_id \
  -X POST -H "Content-Type: application/json" -d '{"test": true}'

You should reach your receiver and get its response over valid TLS. An authenticated webhook (HMAC, bearer token) will return 401 here, which proves the proxy path works and auth is enforced. Point your webhook provider at https://hooks.example.com/webhook/your_webhook_id and you are done.

Troubleshooting

  • 502 Bad Gateway: your receiver is not listening on 127.0.0.1:8000. Check curl -i http://127.0.0.1:8000/docs locally on the server.
  • 413 Request Entity Too Large: you skipped client_max_body_size.
  • Certificate errors from the provider's side: some providers verify SSL strictly (GitHub does by default, and you should keep it that way). curl -vI https://hooks.example.com shows what certificate is actually served.

If you would rather not open ports 80/443 at all, see our companion guide on receiving webhooks through Cloudflare, which uses an outbound-only tunnel.

Next step: a full provider walkthrough, for example receiving GitHub webhooks or receiving Stripe webhooks.

Subscribe to Free Webhook Tool

Sign up now to get access to the library of members-only issues.
Jamie Larson
Subscribe