บทความ › เซิร์ฟเวอร์และ DevOps
เซิร์ฟเวอร์ ที่บ้าน IP เปลี่ยนบ่อย ทำ DDNS เองด้วย API ของ DNS และ cron
ทำไม IP บ้านถึงเปลี่ยน และสคริปต์ Python สั้น ๆ ที่เช็ก IP ทุก 5 นาที แล้วแก้ A record ผ่าน API ของผู้ให้บริการ DNS ยกตัวอย่าง DigitalOcean พร้อมเรื่อง TTL และ token
- IP บ้านเปลี่ยนได้ทุกครั้งที่เราเตอร์รีบูตหรือ ISP สลับให้ ชื่อโดเมนต้องมีคนคอยแก้ให้ตาม
- DDNS ทำเองได้ด้วยสคริปต์ 60 บรรทัด ถาม IP สาธารณะ เทียบกับค่าที่จำไว้ ถ้าเปลี่ยนค่อยเรียก API
- ตั้ง TTL 300 วินาที เพื่อให้คนนอกเห็น IP ใหม่ภายใน 5 นาที
- token ของ DNS อยู่ในไฟล์ .env นอก repo และชื่อโดเมนไม่ใช่การเปิดระบบ ยังต้องมี firewall หรือ VPN
เซิร์ฟเวอร์เล็ก ๆ ที่บ้านทำงานได้ดีจนถึงคืนที่ไฟดับ เราเตอร์รีบูต ISP แจก IP ใหม่ แล้วชื่อ home.example.com ก็ชี้ไปที่บ้านคนอื่น ทางแก้คือ Dynamic DNS (DDNS) ให้เครื่องที่บ้านคอยบอก DNS เองว่าตอนนี้ IP เป็นอะไร บทนี้เล่าแบบที่ใช้จริงกับ 2 ชื่อ (home และ code) มาตั้งแต่ต้นปี สคริปต์ Python หนึ่งไฟล์ cron หนึ่งบรรทัด ไม่ต้องสมัครบริการ DDNS เพิ่ม
ทดสอบบน Ubuntu 24.04 กับ Python 3.12 และ DNS ที่ DigitalOcean แต่ผู้ให้บริการไหนที่มี API แก้ record ได้ก็ใช้โครงเดียวกัน
ทำไม IP บ้านถึงเปลี่ยน
เน็ตบ้านส่วนใหญ่ได้ IP สาธารณะ (public IP) จาก ISP แบบยืมใช้ คล้ายกับที่อุปกรณ์ในบ้านยืม IP จากเราเตอร์ (เรื่องนั้นอยู่ใน บทกล้องและอุปกรณ์ IoT เปลี่ยน IP) แต่ตัวนี้เป็นฝั่งนอกบ้าน เปลี่ยนได้เมื่อ
- เราเตอร์รีบูต หรือไฟดับ สาย PPPoE ต่อใหม่มักได้ IP ใหม่
- ISP บังคับต่อใหม่เป็นระยะ บางเจ้าทุก 24 ถึง 72 ชั่วโมง
- ISP ปรับโครงข่าย ย้ายลูกค้าไปอีกชุด IP
IP คงที่ซื้อได้ แต่แพงและบางแพ็กเกจไม่มีให้ DDNS จึงเป็นทางออกมาตรฐานของเซิร์ฟเวอร์บ้าน
แนวคิด: จำ IP ล่าสุดไว้ ถ้าเปลี่ยนค่อยแก้ DNS
ทุก 5 นาที cron รันสคริปต์ที่ทำ 4 ขั้น
- ถามบริการภายนอกว่า “IP ของฉันคืออะไร”
- เทียบกับ IP ล่าสุดที่จดไว้ในไฟล์ JSON เล็ก ๆ
- ถ้าเท่ากัน จบ ไม่แตะ API เลย
- ถ้าต่าง เรียก API ของ DNS แก้ A record แล้วจดค่าใหม่ลงไฟล์
ข้อ 3 สำคัญ IP เปลี่ยนเดือนละไม่กี่ครั้ง แต่ cron รันวันละ 288 ครั้ง ถ้าเรียก API ทุกรอบจะชน rate limit และเปลือง log โดยไม่จำเป็น การเทียบกับไฟล์ในเครื่องถูกกว่าเทียบกับ DNS จริงมาก
ทำไมถาม IP จาก 2 บริการ
บริการอย่าง api.ipify.org หรือ checkip.amazonaws.com ตอบ IP ของผู้เรียกกลับมาเป็นข้อความบรรทัดเดียว ฟรี แต่ไม่มีใครรับประกันว่าจะอยู่ตลอด วันที่บริการแรกล่ม สคริปต์ต้องไม่สรุปว่า “เน็ตพัง” แล้วหยุด จึงลองตัวที่สองก่อนยอมแพ้ และต้องตรวจว่าคำตอบเป็น IPv4 จริง ไม่ใช่หน้า HTML แจ้ง error
สคริปต์
ของจริงใช้ httpx ตัวนี้ตัดเหลือ urllib ใน stdlib เพื่อให้ไม่ต้อง pip install อะไร วางที่ ~/scripts/ddns-update.py
#!/usr/bin/env python3
"""ddns-update.py DOMAIN SUBDOMAIN — update one A record when the public IP changes."""
import json, os, sys, urllib.request
from datetime import datetime
from pathlib import Path
IP_SERVICES = ["https://api.ipify.org", "https://checkip.amazonaws.com"]
API = "https://api.digitalocean.com/v2" # change for another provider
TTL = 300
def http(url, method="GET", body=None, token=None):
req = urllib.request.Request(url, method=method,
data=json.dumps(body).encode() if body else None)
req.add_header("Content-Type", "application/json")
if token:
req.add_header("Authorization", f"Bearer {token}")
with urllib.request.urlopen(req, timeout=15) as r:
return r.read().decode().strip()
def public_ip():
for url in IP_SERVICES:
try:
ip = http(url)
parts = ip.split(".")
if len(parts) == 4 and all(p.isdigit() and int(p) < 256 for p in parts):
return ip
except Exception as e:
print(f"warn: {url}: {e}", file=sys.stderr)
sys.exit("could not detect public IP")
def update_record(domain, sub, ip, token):
url = f"{API}/domains/{domain}/records?type=A&name={sub}.{domain}"
records = json.loads(http(url, token=token))["domain_records"]
rec = next((r for r in records if r["name"] == sub), None)
if rec is None:
http(f"{API}/domains/{domain}/records", "POST",
{"type": "A", "name": sub, "data": ip, "ttl": TTL}, token)
elif rec["data"] != ip:
http(f"{API}/domains/{domain}/records/{rec['id']}", "PUT", {"data": ip}, token)
def main():
domain, sub = sys.argv[1], sys.argv[2]
token = os.environ["DO_API_TOKEN"]
cache = Path(__file__).with_name(f".ddns_{sub}.json")
ip = public_ip()
last = json.loads(cache.read_text())["ip"] if cache.exists() else ""
if ip == last:
return # the usual case: nothing to do
update_record(domain, sub, ip, token)
cache.write_text(json.dumps({"ip": ip, "updated": datetime.now().isoformat()}))
print(f"{datetime.now():%F %T} {sub}.{domain}: {last or '-'} -> {ip}")
if __name__ == "__main__":
main()
ส่วนที่ผูกกับ DigitalOcean มีแค่ฟังก์ชัน update_record ขั้นตอนคือ GET /v2/domains/{domain}/records เพื่อหา id ของ record แล้ว PUT /v2/domains/{domain}/records/{id} ส่ง {"data": "IP ใหม่"} ถ้ายังไม่มี record ก็ POST สร้างพร้อม ttl: 300 ผู้ให้บริการอื่น (Cloudflare, Gandi, Porkbun) มี API หน้าตาเดียวกัน ต่างแค่ URL กับชื่อฟิลด์
ไฟล์ cache ต่อหนึ่งชื่อ หน้าตาแบบนี้
{"ip": "203.0.113.42", "updated": "2026-10-21T06:05:02"}
wrapper ต่อ subdomain และ cron
token ไม่อยู่ในสคริปต์และไม่อยู่ใน repo แต่อยู่ใน ~/.env ที่ chmod 600 ไว้ wrapper มีหน้าที่โหลดไฟล์นั้นแล้วเรียกสคริปต์ด้วยชื่อที่ต้องการ หนึ่ง wrapper ต่อหนึ่งชื่อ จะได้เพิ่มหรือถอดชื่อโดยไม่แตะโค้ด
#!/bin/bash
# ~/scripts/ddns-home.sh
set -a; source ~/.env; set +a # DO_API_TOKEN=dop_v1_...
python3 ~/scripts/ddns-update.py example.com home
# crontab -e (รันบนเครื่องที่บ้าน)
*/5 * * * * ~/scripts/ddns-home.sh >> ~/scripts/ddns.log 2>&1
*/5 * * * * ~/scripts/ddns-code.sh >> ~/scripts/ddns.log 2>&1
log จะว่างเกือบตลอด เพราะรอบที่ IP ไม่เปลี่ยนไม่พิมพ์อะไร เวลาหาว่า IP เปลี่ยนเมื่อไหร่ก็ grep -- '->' ~/scripts/ddns.log ได้เลย
เลือก TTL เท่าไหร่
TTL คือเวลาที่ resolver ทั่วโลกจำคำตอบ DNS ไว้ก่อนถามใหม่ มีสองแรงดึงกัน
| TTL | เห็น IP ใหม่ช้าสุด | query ที่ DNS | เหมาะกับ |
|---|---|---|---|
| 30 วินาที (ต่ำสุดของ DigitalOcean) | 0.5 นาที + รอบ cron 5 นาที | มากที่สุด | ไม่คุ้ม เพราะ cron ช้ากว่าอยู่แล้ว |
| 300 วินาที | 5 + 5 = ประมาณ 10 นาที | พอดี | DDNS ทั่วไป |
| 3600 วินาที | ชั่วโมงกว่า | น้อย | record ที่ไม่เปลี่ยน |
คิดเป็นตัวเลขจริง เลวร้ายสุด IP เปลี่ยนตอนวินาทีถัดจาก cron เพิ่งรัน รออีก 5 นาทีสคริปต์ถึงเห็น แก้ DNS แล้ว resolver ที่เพิ่งจำค่าเก่าถือไว้ได้อีก 300 วินาที รวมประมาณ 10 นาทีที่ชื่ออาจชี้ผิด ยอมรับได้สำหรับบ้าน ถ้าอยากเร็วกว่านี้ให้ลดรอบ cron ก่อน ไม่ใช่ลด TTL อย่างเดียว
อะไรพังตอน IP เปลี่ยน อะไรไม่พัง
- TLS ไม่พัง ใบรับรองออกให้ “ชื่อ” ไม่ใช่ IP ตราบที่ชื่อชี้มาถูก certbot และเบราว์เซอร์ไม่สนว่า IP เปลี่ยนไปกี่รอบ
- SSH session ที่เปิดอยู่หลุด เพราะ TCP ผูกกับ IP คู่เดิม ต่อใหม่หลัง DNS อัปเดตได้ปกติ ใช้
tmuxฝั่งเซิร์ฟเวอร์งานจะไม่หาย - ช่วง 10 นาทีที่ชี้ผิด ใครที่เข้าชื่อนั้นอาจไปเจอบ้านคนอื่น ซึ่งมักไม่มีอะไรฟัง port นั้น จึงแค่ต่อไม่ติด
- Webhook จากข้างนอก ที่ยิงเข้าบ้านช่วงนั้นจะหาย ระบบที่รับ webhook ควรมี retry ฝั่งผู้ส่ง
DDNS แก้ปัญหา “หาเครื่องเจอ” อย่างเดียว ไม่ได้ทำให้เครื่องปลอดภัยขึ้นหรือลดลง สิ่งที่ตัดสินความปลอดภัยคือ port ที่เราเปิดที่เราเตอร์
ความปลอดภัย
- token ขอบเขตแคบที่สุด DigitalOcean ให้ตั้ง scope ต่อ token ได้ เลือกเฉพาะ
domainอ่านและเขียน ไม่ต้องให้สิทธิ์สร้าง droplet หรืออ่าน billing ถ้าหลุด ความเสียหายจำกัดอยู่ที่ DNS - ไม่อยู่ใน repo โฟลเดอร์สคริปต์มี
.env.exampleที่เป็นค่าปลอมเพื่อให้รู้ว่าต้องมีตัวแปรอะไร ส่วน.envจริงอยู่นอก repo และอยู่ใน.gitignoreซ้ำอีกชั้น - record ชี้ที่บ้าน ไม่ใช่เปิดบ้าน การที่ชื่อโดเมนบอก IP บ้านไม่ได้แปลว่าใครเข้าได้ ถ้าเราเตอร์ไม่ forward port ชื่อนั้นก็แค่ชี้ไปที่ประตูปิด เรื่องว่าจะเปิดอะไรให้ใครอยู่ใน บทเข้าโฮมเซิร์ฟเวอร์จากข้างนอกโดยไม่เปิดพอร์ต และถ้าจะเปิด SSH ให้อ่าน บท SSH hardening ก่อน
- log ไม่มีความลับ พิมพ์แค่ชื่อ IP และเวลา ไม่พิมพ์ token หรือ response เต็มจาก API
ทางเลือกที่ไม่ต้องเขียนเอง
| วิธี | ดีตรงไหน | ติดตรงไหน |
|---|---|---|
| DDNS ในเราเตอร์ (No-IP, DynDNS) | ตั้งครั้งเดียวในหน้าเราเตอร์ | ได้ชื่ออย่าง xxx.ddns.net บางเจ้าต้องยืนยันทุก 30 วัน |
| เราเตอร์รองรับผู้ให้บริการของเรา | ไม่ต้องมีเครื่องรันสคริปต์ | เฟิร์มแวร์เราเตอร์ส่วนใหญ่รู้จักแค่ไม่กี่เจ้า |
| ddclient หรือ inadyn | รองรับหลายผู้ให้บริการ โตมานาน | ไฟล์ config ยาว และบางเจ้าต้องใช้ปลั๊กอิน |
| สคริปต์เอง (บทนี้) | รู้ทุกบรรทัดว่าทำอะไร เพิ่มชื่อได้ไม่จำกัด | ต้องมีเครื่องเปิดตลอด ซึ่งเซิร์ฟเวอร์บ้านเปิดอยู่แล้ว |
ถ้าเราเตอร์รองรับผู้ให้บริการ DNS ของคุณโดยตรง ใช้เราเตอร์ก่อน ถ้าไม่ และมีเครื่อง Linux เปิดตลอดอยู่แล้ว สคริปต์ 60 บรรทัดกับ cron หนึ่งบรรทัดก็พอ
แหล่งอ้างอิง
- DigitalOcean API: Update a Domain Record วิธีเรียก
PUT /v2/domains/{domain_name}/records/{domain_record_id} - DigitalOcean DNS Limits TTL ต่ำสุด 30 วินาที
- DigitalOcean: How to Create, Edit, and Delete DNS Records คำแนะนำ TTL 300 สำหรับ record ที่เปลี่ยนบ่อย
- Release note: domain record TTL ที่มาของ TTL ขั้นต่ำและค่าเริ่มต้น
คำถามที่พบบ่อย
ใช้ DDNS ฟรีของเราเตอร์ เช่น No-IP หรือ DynDNS ไม่ง่ายกว่าเหรอ
ง่ายกว่าถ้ายอมใช้ชื่ออย่าง xxx.ddns.net ได้ แต่ถ้าอยากได้ชื่อใต้โดเมนของตัวเอง และ DNS อยู่กับผู้ให้บริการที่มี API อยู่แล้ว สคริปต์สั้น ๆ ทำได้เหมือนกันโดยไม่ต้องสมัครบริการเพิ่ม
ทำไมไม่ตั้ง TTL ให้ต่ำสุดเลย เช่น 30 วินาที
ยิ่งต่ำยิ่งมี DNS query เข้ามาบ่อย และ resolver หลายตัวก็ไม่เคารพค่าต่ำกว่า 300 อยู่ดี ในขณะที่สคริปต์เราเช็กทุก 5 นาที ตั้งต่ำกว่านั้นก็ไม่ได้เร็วขึ้นจริง
ถ้าบ้านใช้ IPv6 ล่ะ
หลักการเดิม เปลี่ยนจาก A record เป็น AAAA และถาม IP จากบริการที่ตอบ IPv6 ได้ เช่น api64.ipify.org แต่ ISP ในไทยส่วนใหญ่ยังแจก IPv4 แบบเปลี่ยนได้เป็นหลัก
คำสั่งและค่าตั้งในบทความทดสอบบนระบบตัวอย่าง ลองบนเครื่องทดสอบก่อนใช้กับระบบจริงเสมอ