A lightweight, self-hosted DynDNS (Dynamic DNS) service with pluggable DNS provider support. Currently supports Netcup and AWS Route53. Perfect for home labs, homeservers, and Home Assistant users. Deploy with Docker Compose, Kubernetes, or as a Home Assistant addon.
- Multi-Provider Plugin System: Pluggable DNS provider architecture
- Netcup CCP API: Updates DNS records via Netcup's Customer Control Panel API
- AWS Route53: Updates DNS records via AWS Route53 API
- Session Management: Automatic session handling (Netcup: 15-minute timeout with proactive refresh)
- Multiple Formats: Supports both standard DynDNS format and UniFi custom provider format
- Wildcard DNS: Full support for wildcard DNS records (e.g.,
*.example.com) - Basic Authentication: Simple and secure HTTP Basic Auth
- Flexible Deployment: Run with Docker Compose, Kubernetes, or Home Assistant addon
- Health Checks: Built-in health endpoint for Kubernetes probes
- Minimal Attack Surface: Uses distroless base image and runs as non-root
- IAM Role Support: AWS Route53 provider supports IRSA (IAM Roles for Service Accounts)
Most modern routers already support the DynDNS (inadyn) API, and homelabbers typically own one or more domains. Instead of relying on third-party DynDNS services, you can reuse an existing domain and keep full ownership of your update pipeline. Self-hosting brings:
- Control: You choose the auth rules, uptime strategy, and see every update request.
- Privacy: Your Dynamic DNS credentials and logs stay within your own network or trusted infrastructure.
- Reliability: No upstream DynDNS provider to throttle or sunset a free tier.
- Integration: Works with any standard router or device that speaks DynDNS/HTTP update protocols, so you can keep your existing-compatible clients.
Mentions of Netcup, AWS, and Route53 are strictly descriptive: they explain which external services the project integrates with. This project is independently developed and is not affiliated with, endorsed by, or sponsored by those companies or their products.
┌─────────────┐
│ Router │ (UniFi, Fritz!Box, etc.)
│ /inadyn │
└──────┬──────┘
│ HTTP GET with Basic Auth
▼
┌─────────────────────────────┐
│ HomeDDNS Service │
│ (Docker/K8s/Home Assistant)│
│ ┌───────────────────────┐ │
│ │ HTTP Server │ │
│ │ - Auth Middleware │ │
│ │ - DynDNS Handler │ │
│ └───────┬───────────────┘ │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ Netcup CCP API Client │ │
│ │ - Session Management │ │
│ │ - DNS Record Updates │ │
│ └───────┬───────────────┘ │
└──────────┼──────────────────┘
│ HTTPS
▼
┌───────────────┐
│ Netcup CCP │
│ API Endpoint │
└───────────────┘
- Deployment Platform (choose one):
- Docker / Docker Compose
- Kubernetes cluster
- Home Assistant (addon coming soon)
- DNS Provider (choose one):
- Netcup: Account with API credentials, domain managed by Netcup
- Route53: AWS account with Route53 hosted zone
- Create
docker-compose.yml:
version: '3.8'
services:
homeddns:
image: ghcr.io/markussiebert/homeddns:latest
container_name: homeddns
restart: unless-stopped
ports:
- "8053:8053"
environment:
- AUTH_USERNAME=dyndns
- AUTH_PASSWORD=your-secure-password
- DNS_PROVIDER=netcup_ccp
- DOMAIN=example.com
- DNS_TTL=60
# Netcup credentials
- NETCUP_CUSTOMER_NUMBER=123456
- NETCUP_API_KEY=your-api-key
- NETCUP_API_PASSWORD=your-api-password- Start the service:
docker-compose up -dCreate a secret with your credentials:
kubectl create secret generic homeddns-secret \
--from-literal=auth-username=dyndns \
--from-literal=auth-password='your-secure-password' \
--from-literal=netcup-customer-number=123456 \
--from-literal=netcup-api-key=YOUR_API_KEY \
--from-literal=netcup-api-password=YOUR_API_PASSWORDNetcup API Credentials:
- Login to Netcup Customer Control Panel (CCP)
- Navigate to: Stammdaten → API
- Generate API Key and API Password
- Customer Number is your Netcup customer number
# Apply manifests
kubectl apply -f k8s/deployment.yaml
kubectl apply -f k8s/service.yamlOption A: Ingress (Recommended)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: homeddns
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
tls:
- hosts:
- dyndns.example.com
secretName: dyndns-tls
rules:
- host: dyndns.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: homeddns
port:
number: 80Option B: LoadBalancer
kubectl patch service homeddns -p '{"spec":{"type":"LoadBalancer"}}'This service uses a plugin system that supports multiple DNS providers. Choose the provider that matches your DNS hosting.
The Netcup provider uses the CCP (Customer Control Panel) API to update DNS records.
Environment Variables:
DNS_PROVIDER=netcup_ccp(default)NETCUP_CUSTOMER_NUMBER- Your Netcup customer number (required)NETCUP_API_KEY- API key from CCP (required)NETCUP_API_PASSWORD- API password from CCP (required)
Example Deployment:
ko apply -f k8s/deployment.yamlFeatures:
- Automatic session management (15-min timeout, 10-min refresh)
- Support for all DNS record types (A, AAAA, CNAME, etc.)
- Wildcard DNS support
The Route53 provider uses the AWS SDK to update DNS records in Route53 hosted zones.
Environment Variables:
DNS_PROVIDER=route53- AWS credentials via one of:
- IRSA (Recommended for EKS): IAM Role for Service Accounts
- IAM Instance Profile: Attached to EC2 instances
- Environment Variables:
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY - Shared Credentials:
~/.aws/credentials
Example Deployment:
ko apply -f k8s/deployment-route53.yamlRequired IAM Permissions:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"route53:ListHostedZones",
"route53:ListResourceRecordSets",
"route53:ChangeResourceRecordSets"
],
"Resource": "*"
}
]
}IRSA Setup (EKS):
- Create IAM role with the above policy
- Associate role with ServiceAccount:
apiVersion: v1 kind: ServiceAccount metadata: name: dyndns-route53 annotations: eks.amazonaws.com/role-arn: arn:aws:iam::ACCOUNT_ID:role/dyndns-route53
- Deploy using
k8s/deployment-route53.yaml
Features:
- Automatic hosted zone discovery
- Hosted zone caching for performance
- Support for all DNS record types
- Wildcard DNS support
- Configurable TTL
The plugin system makes it easy to add new DNS providers:
- Implement the
provider.Providerinterface ininternal/provider/ - Add provider initialization in
cmd/server/main.go - Update documentation
Provider Interface:
type Provider interface {
Name() string
GetRecord(ctx context.Context, domain, hostname, recordType string) (*DNSRecord, error)
UpdateRecord(ctx context.Context, domain string, record *DNSRecord) error
Close(ctx context.Context) error
}curl -u "dyndns:your-password" \
"https://dyndns.example.com/nic/update?hostname=home.example.com&myip=1.2.3.4"Response: good 1.2.3.4 or nochg 1.2.3.4
curl -u "dyndns:your-password" \
"https://dyndns.example.com/home.example.com"Response: good or nochg
Omit the myip parameter to automatically use the request source IP:
curl -u "dyndns:your-password" \
"https://dyndns.example.com/nic/update?hostname=home.example.com"curl -u "dyndns:your-password" \
"https://dyndns.example.com/nic/update?hostname=*.example.com&myip=1.2.3.4"| Variable | Required | Default | Description |
|---|---|---|---|
PORT |
No | 8053 |
HTTP server port |
AUTH_USERNAME |
Yes | - | Basic auth username |
AUTH_PASSWORD |
Yes | - | Basic auth password |
NETCUP_CUSTOMER_NUMBER |
Yes | - | Netcup customer number |
NETCUP_API_KEY |
Yes | - | Netcup API key |
NETCUP_API_PASSWORD |
Yes | - | Netcup API password |
DNS_TTL |
No | 60 |
DNS record TTL in seconds |
| Code | Description |
|---|---|
good |
DNS record updated successfully |
nochg |
IP address unchanged, no update needed |
notfqdn |
Invalid hostname format |
911 |
Server error or invalid IP address |
- Navigate to: Settings → Internet → WAN
- Scroll to Dynamic DNS
- Click Create New Dynamic DNS
- Configure:
- Service:
custom - Hostname:
home.example.com(or*.example.comfor wildcard) - Username:
dyndns(or your configured username) - Password: Your DynDNS password
- Server:
dyndns.example.com/%h(replace with your domain)
- Service:
Note: UniFi uses inadyn internally. You need to edit /etc/inadyn.conf to add ddns-path = "/":
# SSH into UniFi device
ssh admin@192.168.1.1
# Edit inadyn config
vi /etc/inadyn.conf
# Add this line in the custom provider section:
ddns-path = "/"
# Restart inadyn
killall inadyn- Navigate to: Internet → Freigaben → DynDNS
- Configure:
- DynDNS-Anbieter:
Benutzerdefiniert - Update-URL:
https://dyndns.example.com/nic/update?hostname=<domain>&myip=<ipaddr> - Domainname:
home.example.com - Benutzername:
dyndns - Kennwort: Your DynDNS password
- DynDNS-Anbieter:
- mise - Development environment manager (recommended)
- Go 1.25.4 (managed by mise)
- Ko - Container image builder (optional)
All common tasks are available via mise:
# List all available tasks
mise tasks
# Build binary for current platform
mise run build
# Build for Linux (deployment)
mise run build:linux
# Build container image with Ko (no Dockerfile needed!)
mise run build:ko
# Run server locally
mise run run
# Run tests
mise run test
# Clean build artifacts
mise run clean| Task | Description |
|---|---|
build |
Build homeddns binary for current platform |
build:linux |
Build for Linux AMD64 |
build:linux-arm |
Build for Linux ARM64 (Raspberry Pi) |
build:ko |
Build container image with Ko (local) |
build:ko-push |
Build and push container to registry |
build:ko-multiplatform |
Build multi-platform container image |
run |
Run homeddns server |
run:update |
Update DNS record (pass hostname as arg) |
test |
Run all tests |
test:provider |
Test provider package only |
fmt |
Format Go code |
clean |
Remove build artifacts |
# Copy the example environment file
cp .env.local.example .env.local
# Edit .env.local with your credentials
# Then run with mise (automatically loads .env.local)
mise run run:dev
# Or export environment variables manually
export AUTH_USERNAME=dyndns
export AUTH_PASSWORD='$2a$10$...'
export DNS_PROVIDER=netcup_ccp
export DOMAIN=example.com
export NETCUP_CUSTOMER_NUMBER=123456
export NETCUP_API_KEY=your-api-key
export NETCUP_API_PASSWORD=your-api-password
# Run server
mise run run
# Test update command
mise run run:update -- test.example.com# Test health endpoint (no auth required)
curl http://localhost:8053/health
# Test DNS update
curl -u "dyndns:your-password" \
"http://localhost:8053/nic/update?hostname=test.example.com&myip=1.2.3.4"Ko builds optimized container images directly from Go code:
- ✅ No Dockerfile needed - builds directly from Go source
- ✅ Smaller images - optimized layers, distroless base (~15MB total)
- ✅ SBOM included - automatic software bill of materials
- ✅ Multi-platform - easy cross-compilation
- ✅ Fast builds - aggressive optimization flags built-in
Our build is optimized for minimal size:
| Build Type | Size | Optimizations |
|---|---|---|
| Standard | ~19MB | Default Go build |
| Optimized | ~14MB | Aggressive optimization flags (26% smaller) |
Optimization flags explained:
-trimpath- Remove file system paths from binary-s -w- Strip debug info and symbol table-extldflags=-static- Create fully static binary (no external dependencies)-tags netgo- Use pure Go networking (no CGO required)
Result: 14MB static binary that runs anywhere with no dependencies!
- Endpoint:
https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON - Documentation: https://www.netcup-wiki.de/wiki/CCP_API
- Session Timeout: 15 minutes
- Supported Operations:
login- Authenticate and obtain session IDlogout- Invalidate sessioninfoDnsRecords- Retrieve DNS records for a domainupdateDnsRecords- Update DNS records
- HTTP Basic Authentication for DynDNS endpoint
- Container runs as non-root user (UID 65532)
- Read-only root filesystem
- No privilege escalation
- Minimal container image (distroless)
- All secrets stored in Kubernetes Secrets or environment variables
Docker Compose:
docker logs -f homeddnsKubernetes:
kubectl logs -l app=homeddns --tail=100 -f401 Unauthorized
- Check that Basic Auth credentials are correct
- Verify password hash was generated correctly
"notfqdn" Response
- Hostname must be a valid FQDN
- Ensure domain is managed by Netcup
"911" Response
- Check Netcup API credentials
- Verify domain exists in Netcup account
- Check service logs for detailed error messages
Session Timeout
- Sessions are automatically refreshed every 10 minutes
- If you see frequent re-logins, check network connectivity to Netcup API
This project inherits the license from the parent repository.
Contributions are welcome! Please ensure:
- Code follows Go best practices
- Tests are added for new features
- Documentation is updated