Disclaimer
This package is not related to or developed by Porkbun. No relationship between the developer of this package and Porkbun exists.
All trademarks, logos and brand names are the property of their respective owners. All company, product and service names used in this package are for identification purposes only. Use of these names,trademarks and brands does not imply endorsement.
Porkbun DDNS
porkbun-ddns is a unofficial DDNS-Client for Porkbun Domains.
This library will only update the records if the IP(s) have changed or the dns entry didn't exist before, it will also set/update A (IPv4) and AAAA (IPv6) records.
⚠️ v2.0.0 breaking changes
Version 2.0.0 introduces breaking changes to the public
PorkbunDDNSAPI and the CLI:
- Constructor:
PorkbunDDNS(config, domain, ...)->PorkbunDDNS(credentials, retry, domain, ...).Configis nowAppConfig(credentials, retry, webhook)— build it withCredentials,RetryPolicyandWebhookConfig, or load it viaextract_config.- CLI: the
--fritzbox/-fflag is removed. Pipefritzbox-ipsinto--public-ipsinstead:porkbun-ddns domain.com --public-ips "$(fritzbox-ips <fritzbox-ip>)".- Endpoint:
Credentials.endpointnow defaults to the real Porkbun endpoint (previously it raised when unset).See the full breakdown in PR #163.
Since porkbun-dynamic-dns-python is deprecated I took it into my own hands to code a decent DDNS Client for Porkbun. Inspired by con-f-use pull request, I built a pip Package and a docker container.
As alternative to cert-bun use my lego-certbot image.
Setup on Porkbun
Make sure that any domain you use this client with has API access enabled. See the below picture for reference.
If this is not enabled, you'll see an error about your API keys being invalid, despite them being correct.
CLI
Minimum required python version: 3.10
Install via pip
pip install porkbun-ddns
Usage
usage: porkbun-ddns [-h] [-c CONFIG] [-e ENDPOINT] [-pk APIKEY] [-sk SECRETAPIKEY]
[--retry-count RETRY_COUNT] [--retry-delay RETRY_DELAY]
[--webhook-url WEBHOOK_URL]
[--webhook-template WEBHOOK_TEMPLATE]
[--webhook-template-file WEBHOOK_TEMPLATE_FILE]
[--log-level LOG_LEVEL] [-i [PUBLIC_IPS ...]] [-4 | -6] [-v]
[--env_only]
domain [subdomains ...]
positional arguments:
domain Domain to be updated
subdomains Subdomain(s)
options:
-h, --help show this help message and exit
-c CONFIG, --config CONFIG
Path to config file (default:
~/.config/porkbun-ddns-config.json)
-e ENDPOINT, --endpoint ENDPOINT
The endpoint
-pk APIKEY, --apikey APIKEY
The Porkbun-API-key
-sk SECRETAPIKEY, --secretapikey SECRETAPIKEY
The secret API-key
--retry-count RETRY_COUNT
Number of attempts for transient API failures
--retry-delay RETRY_DELAY
Seconds to wait between retry attempts
--webhook-url WEBHOOK_URL
Webhook URL to notify when IPs change
--webhook-template WEBHOOK_TEMPLATE
Jinja2 template for the webhook payload
--webhook-template-file WEBHOOK_TEMPLATE_FILE
Path to a file containing the Jinja2 webhook template
(takes precedence over --webhook-template)
--log-level LOG_LEVEL
Set log verbosity (DEBUG, INFO, WARNING, ERROR,
CRITICAL)
-i [PUBLIC_IPS ...], --public-ips [PUBLIC_IPS ...]
Public IPs (v4 and or v6)
-4, --ipv4-only Only set/update IPv4 A Records
-6, --ipv6-only Only set/update IPv6 AAAA Records
-v, --verbose Show Debug Output
--env_only Don't use any config, get all variables from the
environment
The parameter endpoint, apikey, secretapikey
These parameter are required for each run of the program. The program will take the values for these (in this order) from:
- The command-line-arguments (
-pk pk1_xxx) - The environment-variables (
export PORKBUN_APIKEY='pk1_xxx') - The config-file (
apikey="pk_xxx")
In Docker use the API_ENDPOINT environment-variable instead.
So if a value is set through the CLI and in the file, the CLI-value will be used. This allows for a default-configuration in the config-file, whose settings can be selectively overridden through enviromnment-variables or CLI-arguments.
The parameter retry_count, retry_delay
Transient API failures (unreachable endpoint, timeouts, HTTP 5xx) are retried automatically, HTTP 4xx errors (e.g. invalid API keys) fail immediately. Default is 3 attempts with a 5 seconds delay between them.
The program will take the values for these (in this order) from:
- The command-line-arguments (
--retry-count 3) - The environment-variables (
export PORKBUN_RETRY_COUNT='3') - The config-file (
retry_count="3")
The parameter webhook_url, webhook_template, webhook_template_file
When the IP(s) of your records change, an aggregated webhook-notification can be POSTed to a URL of your choice. This works out of the box with Slack, MS Teams, Mattermost and Google Chat.
The program will take the values for these (in this order) from:
- The command-line-arguments (
--webhook-url 'https://...') - The environment-variables (
export PORKBUN_WEBHOOK_URL='https://...') - The config-file (
webhook_url="https://...")
In Docker use the WEBHOOK_URL, WEBHOOK_TEMPLATE and WEBHOOK_TEMPLATE_FILE environment-variables instead.
The payload can be customized with an inline Jinja2-template (--webhook-template) or a template-file (--webhook-template-file), where the file takes precedence over the inline one. If neither is set, the following Slack-compatible default is used:
{"text": "IP changed: {{ old_ips | join(', ') }} -> {{ new_ips | join(', ') }} ({{ domain }})"}
The following context-variables are available in templates: changes (list of changes, each {record_type, fqdn, old_ip|None, new_ip}), old_ips (previous IPs), new_ips (new IPs), domain (the updated domain) and timestamp (ISO-8601 UTC timestamp of the notification).
One notification is sent per run, after all records have been updated. Notifications are fire-and-forget: a failure to deliver never crashes the update-loop.
The parameter log_level
Controls the verbosity of the logs. Accepts standard logging level names, case-insensitively: DEBUG, INFO, WARNING, ERROR, CRITICAL (default INFO). Set it via --log-level WARNING on the CLI or LOG_LEVEL=WARNING in Docker.
When both the legacy --verbose/DEBUG and LOG_LEVEL are set, LOG_LEVEL wins. An invalid value logs a warning and falls back to INFO, it never crashes.
Examples
# using the default config-file in ~/.config/porkbun-ddns-config.json:
$ porkbun-ddns domain.com my_subdomain
# Using only environment variables:
# PORKBUN_APIKEY
# PORKBUN_SECRETAPIKEY
# PORKBUN_ENDPOINT (Optional)
$ porkbun-ddns domain.com my_subdomain --env_only
# Specific config-file:
$ porkbun-ddns domain.com my_subdomain -c "./config.json"
# Multiple subdomains:
$ porkbun-ddns domain.com my_subdomain_1 my_subdomain_2 my_subdomain_3
# Set root and subdomains:
$ porkbun-ddns domain.com @ my_subdomain_1 my_subdomain_2 my_subdomain_3
# Set wildcard domain:
$ porkbun-ddns domain.com '*'
# Set IP's explicit
$ porkbun-ddns domain.com my_subdomain -i '1.2.3.4' '1234:abcd:0:4567::8900'
# Use Fritz!Box to obtain IP's (via fritzbox-ips sidecar) and set IPv4 A Record only
$ porkbun-ddns domain.com my_subdomain --public-ips "$(fritzbox-ips fritz.box)" -4
You can set up a cron job get the full path to porkbun-ddns with which porkbun-ddns, then execute crontab -e and add the following line:
*/30 * * * * <PORKBUN-DDNS-PATH>/porkbun-ddns "<YOUR-PATH>/config.json" domain.com my.subdomain >/dev/null 2>&1
config.json example:
{
"endpoint":"https://api.porkbun.com/api/json/v3",
"apikey": "pk1_xxx",
"secretapikey": "sk1_xxx",
"retry_count": "3",
"retry_delay": "5",
"webhook_url": "https://hooks.slack.com/services/..."
}
Docker compose
services:
porkbun-ddns:
image: "mietzen/porkbun-ddns:latest"
container_name: porkbun-ddns
environment:
DOMAIN: "domain.com" # Your Porkbun domain
SUBDOMAINS: "my_subdomain,my_other_subdomain,my_subsubdomain.my_subdomain" # Subdomains comma spreaded
SECRETAPIKEY: "<YOUR-SECRETAPIKEY>" # Your Porkbun Secret-API-Key
APIKEY: "<YOUR-APIKEY>" # Your Porkbun API-Key
# API_ENDPOINT: "https://api.porkbun.com/api/json/v3" # Override the Porkbun API endpoint (e.g. a mirror/proxy)
# PUBLIC_IPS: "1.2.3.4,2001:043e::1" # Set if you got static IP's (wins over FRITZBOX)
# FRITZBOX: "192.168.178.1" # Use Fritz!BOX to obtain Public IP's (queried directly by the entrypoint)
# SLEEP: "300" # Seconds to sleep between DynDNS runs
# IPV4: "TRUE" # Set IPv4 address
# IPV6: "TRUE" # Set IPv6 address
# DEBUG: "FALSE" # DEBUG LOGGING
# LOG_LEVEL: "WARNING" # Set log verbosity (DEBUG, INFO, WARNING, ERROR, CRITICAL)
# RETRY_COUNT: "3" # Number of attempts for transient API failures
# RETRY_DELAY: "5" # Seconds to wait between retry attempts
# WEBHOOK_URL: "https://hooks.slack.com/services/..." # POST an IP-change notification to this URL (Slack, MS Teams, Mattermost, Google Chat compatible by default)
# WEBHOOK_TEMPLATE: '{"text": "IP changed: {{ old_ips | join(", ") }} -> {{ new_ips | join(", ") }} ({{ domain }})"}' # Optional custom Jinja2 template
# WEBHOOK_TEMPLATE_FILE: "/path/to/template.j2" # Optional Jinja2 template file (takes precedence over WEBHOOK_TEMPLATE)
restart: unless-stopped
# # Uncomment below to let it detect ipv6 address:
# networks:
# - ipv6_enabled
# networks:
# ipv6_enabled:
# enable_ipv6: true
Docker run
docker run -d \
-e DOMAIN="domain.com" \
-e SUBDOMAINS="my_subdomain,my_other_subdomain,my_subsubdomain.my_subdomain" \
-e SECRETAPIKEY="<YOUR-SECRETAPIKEY>" \
-e APIKEY="<YOUR-APIKEY>" \
-e LOG_LEVEL="WARNING" \
-e WEBHOOK_URL="https://hooks.slack.com/services/..." \
--name porkbun-ddns \
--restart unless-stopped \
mietzen/porkbun-ddns:latest
Python
Minimum required python version: 3.10
from pathlib import Path
from porkbun_ddns import PorkbunDDNS
from porkbun_ddns.config import AppConfig, Credentials, RetryPolicy, WebhookConfig, DEFAULT_ENDPOINT, extract_config
# Build config directly:
app = AppConfig(
credentials=Credentials(apikey="YOUR-APIKEY", secretapikey="YOUR-SECRETAPIKEY", endpoint=DEFAULT_ENDPOINT),
retry=RetryPolicy(), # defaults: retry_count=3, retry_delay=5
webhook=WebhookConfig(),
)
porkbun_ddns = PorkbunDDNS(app.credentials, app.retry, 'domain.com')
# Or load from config file:
# app = extract_config(Path("./config.json"))
# porkbun_ddns = PorkbunDDNS(app.credentials, app.retry, 'domain.com')
# With static IPs:
# porkbun_ddns = PorkbunDDNS(app.credentials, app.retry, 'domain.com', public_ips=['1.2.3.4', '1234:abcd:0:4567::8900'])
# With Fritz!Box (via fritzbox-ips sidecar CLI):
# porkbun-ddns domain.com --public-ips "$(fritzbox-ips fritz.box)"
porkbun_ddns.set_subdomain('my_subdomain')
porkbun_ddns.update_records()
Release files for porkbun-ddns 2.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| porkbun_ddns-2.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Release files / porkbun_ddns-2.0.0-py3-none-any.whl
| Download URL | porkbun_ddns-2.0.0-py3-none-any.whl |
|---|---|
| Size | 21.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0915baff092b9bf323e900c60a077ff64db1c08a6604688f89b09cd8898a898a
|
|
BLAKE2b-256 checksum How to use checksums |
772cbddfec2cf54ec45486468f798940e01ba56bb7906f56a09eabea65db1d25
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 3, 2026.
Transparency log