Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Vercel Dynamic DNS

Simple script for exposing a local server with [Vercel DNS](https://vercel.com/docs/custom-domains).
It runs on CRON, checking the current IP address and updating DNS records for your domain.
It runs on CRON, checking the current IP address and updating DNS records for your domains.

## Installation

Expand All @@ -12,19 +12,30 @@ It runs on CRON, checking the current IP address and updating DNS records for yo
5. Open the cron settings using the command `crontab -e`
6. Add the following line to the cron job: `*/15 * * * * /path-to/vercel-ddns/dns-sync.sh`

### Configuration

Set these values inside `dns.config`:

- `VERCEL_TOKEN`: Personal token from https://vercel.com/account/tokens
- `DOMAINS`: Comma-separated list of fully qualified domains you want to keep in sync. Examples: `example.com`, `api.example.com, www.example.com`, `foo.bar.example.com, example.com`. The script treats the last two labels as the apex domain (e.g. `foo.bar.example.com` → apex `example.com`, subdomain `foo.bar`).

## Usage example

```sh
# Creating
# Creating (multiple domains supported)
➜ ./dns-sync.sh
Updating IP: x.x.x.x
Record for SUBDOMAIN does not exist. Creating...
Processing api.example.com
Record for api.example.com does not exist. Creating...
Processing example.com
Record for example.com already exists (id: rec_xxxxxxxxxxxxxxxxxxxxxxxx). Updating...
🎉 Done!

# Updating
➜ ./dns-sync.sh
Updating IP: x.x.x.x
Record for SUBDOMAIN already exists (id: rec_xxxxxxxxxxxxxxxxxxxxxxxx). Updating...
Processing api.example.com
Record for api.example.com already exists (id: rec_xxxxxxxxxxxxxxxxxxxxxxxx). Updating...
🎉 Done!
```

Expand Down
123 changes: 99 additions & 24 deletions dns-sync.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,21 @@ get_current_ip() {
echo $ip
}

# Function to check if subdomain exists
# Function to check if subdomain exists for a given domain
check_subdomain_exists() {
local subdomain="$1"
local domain_name="$1"
local subdomain="$2"
local response
response=$(curl -sX GET "https://api.vercel.com/v4/domains/$DOMAIN_NAME/records" \
response=$(curl -sX GET "https://api.vercel.com/v4/domains/$domain_name/records" \
-H "Authorization: Bearer $VERCEL_TOKEN" \
-H "Content-Type: application/json")

local record_id
record_id=$(echo "$response" | jq -r ".records[] | select(.name == \"$subdomain\") | .id")
if [[ -z "$subdomain" ]]; then
record_id=$(echo "$response" | jq -r '.records[] | select((.name == "") or (.name == null)) | .id')
else
record_id=$(echo "$response" | jq -r ".records[] | select(.name == \"$subdomain\") | .id")
fi
if [[ -n "$record_id" ]]; then
# Return record ID if exists
echo "$record_id"
Expand All @@ -39,15 +44,21 @@ check_subdomain_exists() {

# Updates dns record
update_dns_record() {
local ip="$1"
local record_id="$2"
local domain_name="$1"
local subdomain="$2"
local ip="$3"
local record_id="$4"
local host_display="$domain_name"
if [[ -n "$subdomain" ]]; then
host_display="$subdomain.$domain_name"
fi
local response
response=$(curl -sX PATCH "https://api.vercel.com/v1/domains/records/$record_id" \
-H "Authorization: Bearer $VERCEL_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"comment": "vercel-ddns",
"name": "'$SUBDOMAIN'",
"name": "'$subdomain'",
"type": "A",
"value": "'$ip'",
"ttl": 60
Expand All @@ -57,21 +68,27 @@ update_dns_record() {
if [[ "$response" == *"error"* ]]; then
local error_message
error_message=$(echo "$response" | jq -r '.error.message')
echo "⚠️ Error updating DNS record: $error_message"
exit 1
echo "⚠️ Error updating DNS record for $host_display: $error_message"
return 1
fi
}

# Creates dns record
create_dns_record() {
local ip="$1"
local domain_name="$1"
local subdomain="$2"
local ip="$3"
local host_display="$domain_name"
if [[ -n "$subdomain" ]]; then
host_display="$subdomain.$domain_name"
fi
local response
response=$(curl -sX POST "https://api.vercel.com/v4/domains/$DOMAIN_NAME/records" \
response=$(curl -sX POST "https://api.vercel.com/v4/domains/$domain_name/records" \
-H "Authorization: Bearer $VERCEL_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"comment": "vercel-ddns",
"name": "'$SUBDOMAIN'",
"name": "'$subdomain'",
"type": "A",
"value": "'$ip'",
"ttl": 60
Expand All @@ -81,23 +98,81 @@ create_dns_record() {
if [[ "$response" == *"error"* ]]; then
local error_message
error_message=$(echo "$response" | jq -r '.error.message')
echo "⚠️ Error creating DNS record: $error_message"
exit 1
echo "⚠️ Error creating DNS record for $host_display: $error_message"
return 1
fi
}

# Split a fully qualified domain into apex and subdomain parts
split_domain() {
local full_domain="$1"
local IFS='.'
read -r -a parts <<< "$full_domain"
local total_parts=${#parts[@]}
if (( total_parts < 2 )); then
echo "⚠️ Invalid domain: $full_domain" >&2
return 1
fi

local apex
apex="${parts[$((total_parts - 2))]}.${parts[$((total_parts - 1))]}"

local subdomain_parts=()
if (( total_parts > 2 )); then
subdomain_parts=("${parts[@]:0:$((total_parts - 2))}")
fi

local subdomain=""
if (( ${#subdomain_parts[@]} > 0 )); then
local IFS='.'
subdomain="${subdomain_parts[*]}"
fi

echo "$apex|$subdomain"
}

# Get current IP
# Clean and split the DOMAINS list into an array
IFS=',' read -r -a DOMAIN_ENTRIES <<< "$DOMAINS"
IFS=$' \t\n'

# Get current IP once for all domains
ip=$(get_current_ip)
echo "Updating IP: $ip"

# Check if subdomain exists
record_id=$(check_subdomain_exists "$SUBDOMAIN")
if [[ -n "$record_id" ]]; then
echo "Record for $SUBDOMAIN already exists (id: $record_id). Updating..."
update_dns_record "$ip" "$record_id"
else
echo "Record for $SUBDOMAIN does not exist. Creating..."
create_dns_record "$ip"
fi
for raw_domain in "${DOMAIN_ENTRIES[@]}"; do
domain_trimmed=$(echo "$raw_domain" | xargs)

if [[ -z "$domain_trimmed" ]]; then
continue
fi

split_result=$(split_domain "$domain_trimmed") || continue
domain_name=${split_result%%|*}
subdomain=${split_result#*|}

# Log which host we are processing
if [[ -z "$subdomain" ]]; then
echo "Processing apex $domain_name"
else
echo "Processing $subdomain.$domain_name"
fi

record_id=$(check_subdomain_exists "$domain_name" "$subdomain")
if [[ -n "$record_id" ]]; then
if [[ -z "$subdomain" ]]; then
echo "Record for $domain_name already exists (id: $record_id). Updating..."
else
echo "Record for $subdomain.$domain_name already exists (id: $record_id). Updating..."
fi
update_dns_record "$domain_name" "$subdomain" "$ip" "$record_id"
else
if [[ -z "$subdomain" ]]; then
echo "Record for $domain_name does not exist. Creating..."
else
echo "Record for $subdomain.$domain_name does not exist. Creating..."
fi
create_dns_record "$domain_name" "$subdomain" "$ip"
fi
done

echo "🎉 Done!"
11 changes: 6 additions & 5 deletions dns.config.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
# Get the token from https://vercel.com/account/tokens
VERCEL_TOKEN=""

# Domain name (e.g. example.com)
DOMAIN_NAME=""

# Subdomain to update (note: use an empty string for the root record)
SUBDOMAIN=""
# Comma-separated list of full domains (with or without subdomains)
# Examples:
# "example.com"
# "api.example.com, www.example.com"
# "foo.bar.example.com, example.com"
DOMAINS=""