Si tu servidor doméstico, tu NAS o tu Raspberry Pi corren Docker y tu operador te cambia la IP pública de vez en cuando, lo más limpio es un contenedor que actualice tu registro DNS en Cloudflare cada pocos minutos. Esta guía te da un docker-compose.yml y un script que solo llama a la API de Cloudflare cuando la IP ha cambiado de verdad.
Qué necesitas antes de empezar
- Un dominio gestionado en Cloudflare (el plan gratuito sirve).
- Un registro A ya creado, por ejemplo
casa.midominio.com, con cualquier IP provisional. El script lo actualiza, no lo crea. - Docker y Docker Compose instalados en una máquina que esté siempre encendida.
- Un token de API de Cloudflare con permiso de edición de DNS, limitado a tu zona.
1. Crea el token de API en Cloudflare
En el panel de Cloudflare entra en Mi perfil > Tokens de API > Crear token y elige la plantilla Editar DNS de zona. En «Recursos de zona» selecciona solo tu dominio. Copia el token cuando se muestre: Cloudflare no lo enseña una segunda vez. No uses la clave global de la cuenta: un token limitado a una zona hace menos daño si se filtra.
2. Crea la carpeta y el fichero .env
Crea una carpeta (por ejemplo cloudflare-ddns) y dentro un fichero .env con tus datos:
CF_API_TOKEN=pega_aqui_tu_token
CF_ZONE=midominio.com
CF_RECORD=casa.midominio.com
Restringe el permiso del fichero para que solo lo lea tu usuario: chmod 600 .env. Si usas Git, añade .env a .gitignore.
3. El script que actualiza la IP
Guarda esto como ddns.sh en la misma carpeta. Obtiene tu IP pública, busca el registro en Cloudflare y solo lo modifica si la IP es distinta:
#!/bin/sh
set -eu
API="https://api.cloudflare.com/client/v4"
AUTH="Authorization: Bearer ${CF_API_TOKEN}"
IP="$(curl -fsS https://api.ipify.org)"
ZONE_ID="$(curl -fsS -H "$AUTH" "$API/zones?name=${CF_ZONE}" | jq -r '.result[0].id')"
RECORD="$(curl -fsS -H "$AUTH" "$API/zones/${ZONE_ID}/dns_records?type=A&name=${CF_RECORD}")"
RECORD_ID="$(echo "$RECORD" | jq -r '.result[0].id')"
CURRENT="$(echo "$RECORD" | jq -r '.result[0].content')"
if [ "$RECORD_ID" = "null" ]; then
echo "No existe un registro A llamado ${CF_RECORD}: créalo primero en Cloudflare."
exit 1
fi
if [ "$IP" = "$CURRENT" ]; then
echo "Sin cambios: ${CF_RECORD} ya apunta a ${IP}"
exit 0
fi
curl -fsS -X PUT -H "$AUTH" -H "Content-Type: application/json" \
--data "{\"type\":\"A\",\"name\":\"${CF_RECORD}\",\"content\":\"${IP}\",\"ttl\":120,\"proxied\":false}" \
"$API/zones/${ZONE_ID}/dns_records/${RECORD_ID}" > /dev/null
echo "Actualizado: ${CF_RECORD} ${CURRENT} -> ${IP}"
El valor "proxied":false deja el registro como «solo DNS» (nube gris). Si quieres que pase por el proxy de Cloudflare, cámbialo a true; ten en cuenta que entonces solo funcionarán los puertos que admite ese proxy.
4. El docker-compose.yml
Guarda esto como docker-compose.yml. Usa una imagen Alpine mínima que instala curl y jq al arrancar y ejecuta el script cada 5 minutos:
services:
cloudflare-ddns:
image: alpine:3.20
container_name: cloudflare-ddns
restart: unless-stopped
env_file: .env
volumes:
- ./ddns.sh:/ddns.sh:ro
command: sh -c "apk add --no-cache curl jq > /dev/null && while true; do sh /ddns.sh || true; sleep 300; done"
El || true evita que un fallo puntual (por ejemplo, que Cloudflare tarde en responder) pare el bucle: se reintenta en el siguiente ciclo.
5. Arráncalo y comprueba que funciona
docker compose up -d
docker logs -f cloudflare-ddns
En el primer ciclo deberías ver «Actualizado: …» si la IP de tu registro era distinta, o «Sin cambios: …» si ya coincidía. Para confirmarlo desde fuera, consulta el DNS: dig +short casa.midominio.com (o nslookup en Windows) debe devolver tu IP pública actual. Si no coincide, espera al TTL del registro (120 segundos en este script) y vuelve a probar.
Alternativa: una imagen ya hecha
Si prefieres no mantener tu propio script, existen imágenes de la comunidad pensadas para esto, como favonia/cloudflare-ddns. Se configuran con variables de entorno (token y lista de dominios) en el mismo docker-compose.yml. Antes de usarla, lee su documentación oficial: las opciones disponibles cambian con las versiones y conviene fijar una etiqueta de versión en lugar de usar latest.
Errores frecuentes
- «No existe un registro A…»: crea antes el registro en Cloudflare con el nombre exacto de
CF_RECORD. - Error de autenticación (403 o «Invalid request headers»): el token no tiene permiso de edición de DNS sobre esa zona, o lo has copiado con espacios.
jqdevuelve «null» como ID de zona:CF_ZONEdebe ser el dominio raíz (midominio.com), no el subdominio.- El contenedor se reinicia en bucle: mira
docker logs cloudflare-ddns; normalmente falta una variable del.env. - Tu operador usa CGNAT: si tu IP «pública» no es realmente tuya, ningún DDNS te dará acceso desde fuera. Compruébalo en esta guía sobre CGNAT.
¿Y si no usas Docker?
El mismo planteamiento funciona sin contenedores: tienes el script bash con cron para Cloudflare y la guía de Cloudflare DDNS en Raspberry Pi. Si el equipo que cambia de IP es un PC con Windows, SyncMyIP se encarga de mantener el registro al día desde la bandeja del sistema sin tocar la consola.
Preguntas frecuentes
¿Cada cuánto debería comprobar mi IP?
Cada 5 minutos es un buen equilibrio: el script solo llama a Cloudflare para escribir cuando la IP cambia, así que comprobar a menudo cuesta muy poco.
¿Funciona con IPv6?
Este script actualiza solo registros A (IPv4). Para IPv6 harían falta un registro AAAA y obtener la dirección IPv6 de tu conexión; es un script aparte.
¿Es seguro guardar el token en un fichero .env?
Es razonable en un servidor propio si limitas el token a una zona, proteges el fichero con chmod 600 y no lo subes a ningún repositorio. Para entornos compartidos, usa los secretos de Docker.
¿Puedo actualizar varios subdominios?
Sí: ejecuta una copia del script por cada registro (cambiando CF_RECORD) o añade un bucle sobre una lista de nombres.