# Fallo de DNS en Docker rompió notificaciones GitLab CI a MAX: Depuración y solución
Un trabajo de GitLab CI destinado a enviar una notificación a MAX Messenger tras un despliegue se convirtió en una pesadilla de depuración de una semana. Las solicitudes se enviaban, pero las respuestas devolvían {"code":"proto.payload","message":"Can't deserialize body"}. Los logs apuntaban a un problema en el cuerpo de la solicitud, así que nos centramos en construir el payload JSON.
Aunque simplificamos todo, nada funcionaba. Las variables de entorno MAX_BOT_TOKEN y MAX_NOTIFY_CHAT_ID estaban configuradas, y curl estaba en la imagen. Pero los mensajes nunca llegaban.
Intentos fallidos con payloads
El equipo probó varias iteraciones:
- printf a archivo:
printf '{"text":"%s"}' "$MSG" > /tmp/payload.json; curl ... -d @/tmp/payload.json. Respuesta:Can't deserialize body.
- jq en Alpine: Cambiamos a
alpine:3.20conapk add jq.jq -nc --arg t "$MSG" '{text:$t}' > /tmp/payload.json. Misma respuesta.
- jq a variable:
PAYLOAD=$(jq ...); curl ... --data "${PAYLOAD}". AhoraEmpty request body, ademásapk addfalló con código de salida 2 por repositorios inalcanzables.
- Hardcodeado:
curl ... -d '{"text":"test ok"}'. De nuevoEmpty request body.
El progreso fue mínimo: el cuerpo de la solicitud fallaba consistentemente en llegar al servidor.
Prueba clave: Host vs Contenedor
Una comparación lado a lado reveló al culpable:
Desde el host:
ssh ... "curl -s -X POST 'https://platform-api.max.ru/messages?chat_id=...' -H 'Authorization: ...' -H 'Content-Type: application/json' -d '{"text":"test from server"}'"
HTTP 200, mensaje entregado.
Desde Docker:
docker run --rm curlimages/curl:8.7.1 curl -s -X POST 'https://platform-api.max.ru/messages?chat_id=...' -H 'Authorization: ...' -d '{"text":"test from docker"}'
código de salida 6: Could not resolve host: platform-api.max.ru.
Diagnóstico: La resolución DNS fallaba dentro de los contenedores Docker, aunque el host la resolvía sin problemas.
Probar con --dns 8.8.8.8 lo confirmó: el mensaje se envió al instante.
Por qué el DNS se disfrazó de error JSON
Esperábamos un error DNS claro como curl exit code 6 sin respuesta HTTP. En cambio, MAX devolvía JSON con proto.payload. El problema venía de la configuración DNS personalizada del runner de GitLab para los contenedores de trabajos. Esto causaba conexiones inestables: los cuerpos de solicitud se truncaban o perdían por completo. MAX interpretaba la entrada vacía como un error de deserialización.
Solución: DNS en config.toml del runner
Solución: Agregar servidores DNS en /etc/gitlab-runner/config.toml:
[[runners]]
...
[runners.docker]
dns = ["8.8.8.8", "8.8.4.4"]
Reiniciar: sudo gitlab-runner restart.
Alternativa en el daemon de Docker en /etc/docker/daemon.json:
{
"dns": ["8.8.8.8", "8.8.4.4"]
}
sudo systemctl restart docker.
Tras la corrección, el payload hardcodeado funcionó en el primer intento.
Plantilla de script funcional
Script final con diagnósticos y limpieza de variables:
if [ -z "$MAX_BOT_TOKEN" ] || [ -z "$MAX_NOTIFY_CHAT_ID" ]; then
echo "WARN: MAX_BOT_TOKEN o MAX_NOTIFY_CHAT_ID no configurados"
exit 0
fi
TOKEN="$(printf '%s' "$MAX_BOT_TOKEN" | tr -d '\r\n')"
CHAT_ID="$(printf '%s' "$MAX_NOTIFY_CHAT_ID" | tr -d '\r\n[:space:]')"
BODY_FILE="$(mktemp)"
RESPONSE_FILE="$(mktemp)"
printf '{"text":"%s"}' "${MSG}" > "${BODY_FILE}"
BODY_SIZE="$(wc -c < "${BODY_FILE}" | tr -d '[:space:]')"
echo "DEBUG: CHAT_ID=${CHAT_ID}"
echo "DEBUG: BODY_SIZE=${BODY_SIZE}"
echo "DEBUG: BODY_TEXT=$(cat "${BODY_FILE}")"
CURL_STATUS=0
HTTP_CODE=$(curl -sS --max-time 15 \
-o "${RESPONSE_FILE}" \
-w "%{http_code}" \
-X POST "https://platform-api.max.ru/messages?chat_id=${CHAT_ID}" \
-H "Authorization: ${TOKEN}" \
-H "Content-Type: application/json" \
--data @"${BODY_FILE}") || CURL_STATUS=$?
RESPONSE="$(cat "${RESPONSE_FILE}" 2>/dev/null || true)"
rm -f "${BODY_FILE}" "${RESPONSE_FILE}"
echo "DEBUG: HTTP_CODE=${HTTP_CODE} CURL_STATUS=${CURL_STATUS}"
echo "DEBUG: Response: ${RESPONSE}"
if [ "${CURL_STATUS}" -ne 0 ] || [ "${HTTP_CODE}" != "200" ]; then
echo "ERROR: Falló el envío. Response: ${RESPONSE}"
exit 1
fi
Formato de notificaciones y extracción de datos
Las notificaciones extraen datos de variables de GitLab CI:
CI_PROJECT_NAME— proyectoCI_COMMIT_BRANCH— ramaCI_COMMIT_SHORT_SHA— hash del commitCI_COMMIT_TITLE— mensaje del commit (escapado consed 's/\/\\/g; s/\"\"/g')CI_COMMIT_AUTHOR— autorCI_PIPELINE_URL— enlace al pipeline
Ejemplo:
✅ Despliegue DEV exitoso
📦 Proyecto: u.clinic
🌍 Entorno: DEV
🌿 Rama: dev
🔖 Versión: f7a249d0
💬 Commit: fix(ci): mejorar manejo de notificaciones MAX
👤 Autor: Anton Kurganskii
⏰ Hora (UTC): 2026-04-08 21:29:07
⏱️ Duración pipeline: 9 min 52 seg
🔗 Pipeline: https://git.tverdasoft.ru/...
✓ Despliegue completado con éxito
Elección de imagen y alternativas
curlimages/curl:8.7.1 supera a alpine + jq: no necesita red para paquetes, se ejecuta desde caché con pull_policy: if-not-present. El apk add de Alpine fallaba por repositorios inalcanzables.
Lecciones clave
- Fallos de DNS en Docker de runners GitLab imitan errores de payload por conexiones inestables.
- Prueba primero:
docker run curl ... platform-api.max.ruen el host del runner — código 6 indica DNS. - Solución:
dns = ["8.8.8.8", "8.8.4.4"]en[runners.docker]de config.toml. - Depuración: Registra
BODY_SIZE,HTTP_CODE, respuesta completa. - Para mensajes de commit:
tr -d '\r\n'+ escape consed.
— Editorial Team
Aún no hay comentarios.