A lightweight Docker container that automatically monitors your external IP address and updates Pangolin firewall rules when changes are detected. Perfect for home servers, VPS instances, or any infrastructure that needs dynamic IP-based access rules.
- Automatic IP Monitoring — checks your external IP on a configurable interval
- Smart Updates — only calls the Pangolin API when the IP actually changes (local cache, no unnecessary reads)
- Rotating IP Services — round-robins across multiple IP-check endpoints to reduce fingerprinting
- Jittered Intervals — adds random ± seconds to each check to avoid predictable traffic patterns
- Exponential Backoff — backs off gracefully on transient errors instead of hammering the API
- Persistent HTTP Session — reuses the TCP connection to Pangolin for lower overhead
- Dynamic DNS Support — resolve a hostname instead of checking this machine's own IP
- Webhook / Trigger Mode — expose an HTTP endpoint so an external source (e.g. your browser, a cron, a reverse proxy) pushes its IP directly
- Environment-Based Config — everything managed through a
.envfile - Docker Compose Ready — single-command deployment
- Docker and Docker Compose
- Pangolin Integration API enabled: https://docs.digpangolin.com/manage/integration-api
- Valid Pangolin API access token with:
Resource Rule → List Resource RulesResource Rule → Update Resource Rule
- The Rule ID you want to keep updated
- Visit the Swagger UI at
https://<your-pangolin>/v1/docs, authorize with your token, and callGET /resource/{resourceId}/rulesto list rules and find your Rule ID
- Visit the Swagger UI at
-
Clone the repository
git clone https://github.com/olizimmermann/pangolin_rule_updater.git cd pangolin_rule_updater -
Create your environment file
cp example.env .env
-
Configure your settings (see Configuration below)
-
Build and start
docker compose up -d
Create a .env file in the project root:
# Pangolin credentials
API_KEY=YOUR_LONG_BEARER_TOKEN
RESOURCE_ID=1
RULE_ID=1
RULE_PRIORITY=1
RULE_ACTION=ACCEPT
RULE_MATCH=IP # IP, CIDR, PATH
RULE_ENABLED=True
TARGET_DOMAIN= # dynamic DNS hostname — leave empty to use this machine's IP
PANGOLIN_HOST=https://api.pangolin.example
# Runtime controls (optional)
IP_SERVICE_URL=https://wtfismyip.com/text,https://api.ipify.org,https://icanhazip.com
LOOP_SECONDS=60 # check interval in seconds
LOOP_JITTER=10 # ± random seconds added to each interval
# Webhook trigger (optional)
EXPOSE_TRIGGER_WEBSITE=False
TRIGGER_WEBSITE_DOMAIN=trigger.my.dyn.dns.com
TRIGGER_WEBSITE_PATH=/update
TRIGGER_WEBSITE_PORT=8080
TRIGGER_SECRET= # recommended when EXPOSE_TRIGGER_WEBSITE=True| Parameter | Required | Default | Description |
|---|---|---|---|
API_KEY |
✅ | — | Pangolin API Bearer token |
RESOURCE_ID |
✅ | — | Resource ID in Pangolin |
RULE_ID |
✅ | — | Rule ID to update |
PANGOLIN_HOST |
✅ | https://api.pangolin.example |
Pangolin API base URL |
RULE_PRIORITY |
❌ | 100 |
Rule priority |
RULE_ACTION |
❌ | ACCEPT |
ACCEPT or DROP |
RULE_MATCH |
❌ | IP |
IP, CIDR, or PATH |
RULE_ENABLED |
❌ | True |
Enable or disable the rule |
TARGET_DOMAIN |
❌ | — | Resolve this hostname instead of checking machine's external IP |
IP_SERVICE_URL |
❌ | three built-in services | Comma-separated list of plain-text IP services, rotated round-robin |
LOOP_SECONDS |
❌ | 60 |
Base check interval in seconds |
LOOP_JITTER |
❌ | 10 |
Random ± seconds added to each interval |
EXPOSE_TRIGGER_WEBSITE |
❌ | False |
Enable webhook trigger mode (disables automatic polling) |
TRIGGER_WEBSITE_DOMAIN |
❌ | trigger.my.dyn.dns.com |
Expected Host header for the trigger endpoint |
TRIGGER_WEBSITE_PATH |
❌ | /update |
Path for the trigger endpoint |
TRIGGER_WEBSITE_PORT |
❌ | 8080 |
Port the trigger server listens on |
TRIGGER_SECRET |
❌ | — | If set, requests must include ?token=<value>; missing/wrong token → HTTP 401 |
docker compose up -ddocker compose logs -fdocker compose downdocker compose build --no-cache && docker compose up -dSet TARGET_DOMAIN to your DynDNS hostname. The script will resolve its IP instead of detecting this machine's external IP.
TARGET_DOMAIN=my.dyn.dns.comWhen EXPOSE_TRIGGER_WEBSITE=True, automatic polling is disabled. Instead, a tiny HTTP server listens for incoming connections and uses the requester's IP to update the rule. This is handy when the device that needs access can initiate the request itself.
Enable the port in docker-compose.yml:
services:
ip-updater:
build: .
env_file: .env
restart: unless-stopped
ports:
- "${TRIGGER_WEBSITE_PORT}:${TRIGGER_WEBSITE_PORT}"Set in .env:
EXPOSE_TRIGGER_WEBSITE=True
TRIGGER_WEBSITE_DOMAIN=trigger.my.dyn.dns.com
TRIGGER_WEBSITE_PATH=/update
TRIGGER_WEBSITE_PORT=8080
⚠️ Security note: Anyone who can reach this endpoint can update your firewall rule. SetTRIGGER_SECRETand share the full URL including?token=…only with trusted users. See Securing the trigger endpoint below.
services:
pangolin-rule-updater:
container_name: pangolin-rule-updater
build:
context: https://github.com/olizimmermann/pangolin_rule_updater.git#main
dockerfile: Dockerfile
restart: unless-stopped
environment:
# --- Required ---
API_KEY: YOUR_API_TOKEN
RESOURCE_ID: "1"
RULE_ID: "1"
PANGOLIN_HOST: "https://api.example.com"
# --- Optional ---
RULE_PRIORITY: "1"
RULE_ACTION: "ACCEPT"
RULE_MATCH: "IP"
RULE_ENABLED: "True"
IP_SERVICE_URL: "https://wtfismyip.com/text,https://api.ipify.org"
LOOP_SECONDS: "60"
LOOP_JITTER: "10"pangolin-ip-updater/
├── Dockerfile # Container definition
├── docker-compose.yml # Service orchestration
├── update_ip.py # Main application logic
├── requirements.txt # Python dependencies
├── example.env # Template for environment variables
├── .env # Your actual config (create this, never commit it)
└── README.md
curl -X GET \
'https://api.pangolin.example/v1/resource/{RESOURCE_ID}/rules' \
-H 'Authorization: Bearer {API_KEY}'curl -X POST \
'https://api.pangolin.example/v1/resource/{RESOURCE_ID}/rule/{RULE_ID}' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {API_KEY}' \
-d '{"action":"ACCEPT","match":"IP","value":"1.2.3.4","priority":1,"enabled":true}'| Symptom | Check |
|---|---|
| Container exits immediately | .env exists and has all required variables; API key is valid |
401 / auth errors |
API_KEY correct and active; Bearer prefix is added automatically |
| Rule not updating | Correct RESOURCE_ID + RULE_ID; test with the curl commands above |
| Network errors | Container has internet access; try a different IP_SERVICE_URL |
View live logs:
docker compose logs -f- Never commit
.env— it contains your API credentials - Restrict file permissions:
chmod 600 .env - Use Docker secrets for production deployments
- Rotate API keys regularly
When EXPOSE_TRIGGER_WEBSITE=True, protect the endpoint with TRIGGER_SECRET:
TRIGGER_SECRET=replace-with-a-long-random-stringRequests without a matching ?token= query parameter are rejected with HTTP 401 and logged as a warning. The full trigger URL then becomes a magic link:
https://trigger.my.dyn.dns.com/update?token=replace-with-a-long-random-string
Bookmark this URL on your phone or laptop — one tap updates the rule, no app required.
Path hardening: The default path /update is predictable. If you do not set TRIGGER_SECRET, use a random webhook-style path instead — something like /trigger/a3f8c2e1b7d94f05 — so the endpoint is not trivially discoverable by scanners. If TRIGGER_SECRET is set, the path matters less, but there is no reason not to change the default anyway.
Port: Avoid well-known ports (80, 443, 8080, 8443) — they attract the most automated scanning traffic. Pick a random high port (e.g. 47823) to reduce noise. This is not a security control on its own, but it lowers the number of unsolicited probes you will see in the logs.
A note on subdomain privacy and CT logs: If you use HTTP-01 or TLS-ALPN-01 ACME validation to obtain a TLS certificate for the trigger subdomain, the subdomain will appear in public Certificate Transparency logs (searchable at crt.sh). If you want the subdomain to stay private, use a wildcard certificate obtained via DNS-01 challenge instead. Regardless of subdomain visibility, TRIGGER_SECRET is the primary access control.
Worst-case abuse: An attacker who successfully calls the endpoint can whitelist their own IP address, which grants them direct access to your protected resources — no credentials required. That is the entire point of the rule. Keep the token secret and rotate it if compromised.
LOOP_SECONDS=300 # check every 5 minutes
LOOP_JITTER=30 # ± 30 s randomisationIP_SERVICE_URL=https://api6.ipify.orgDeploy separate containers with different .env files:
docker compose -f docker-compose.rule1.yml up -d
docker compose -f docker-compose.rule2.yml up -dIf this saved you time, please consider giving it a star on GitHub — it helps others find the project and motivates further development!
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
MIT — see the LICENSE file for details.
- Pangolin for the great self-hosted tunneling platform
- ipify for a reliable IP detection API
- Docker community for containerisation best practices
Found a bug or have a question? Open an issue.