Docker 컨테이너에서 Telegram Bot API 호출 시 ETIMEDOUT 발생 원인과 해결
증상
NestJS 서버에서 에러 발생 시 Telegram Bot API로 알림을 보내는 기능
간헐적으로 알림이 오지 않는 현상 발생
서버 로그에는 에러가 정상적으로 찍히지만, Telegram 메시지는 도착하지 않음
디버깅 과정
1단계: 코드 문제 확인
TelegramTransport에서 axios 에러를 .catch(() => {})로 무시하고 있어서, 실패해도 아무 로그가 남지 않았음.
catch에 로그를 추가하여 원인 파악 시작.
// 기존 — 에러를 삼킴
axios.post(url, data).catch(() => {});
// 디버깅용 — 에러 내용 출력
axios.post(url, data).catch((err) => {
console.error('[TelegramTransport]', err.code, err.message);
});
결과: ETIMEDOUT 확인
2단계: 네트워크 문제 확인
docker exec server wget -q -O- https://api.telegram.org
docker exec server node -e "require('https').get('https://api.telegram.org', ...)"
docker exec server node -e "require('https').get('https://google.com', ...)"
wget은 되고 Node.js는 안 되는 상황.
3단계: DNS 조회
docker exec server node -e "
require('dns').resolve4('api.telegram.org', (e, a) => console.log('IPv4:', a));
require('dns').resolve6('api.telegram.org', (e, a) => console.log('IPv6:', a));
"
IPv4: [ '149.154.166.110' ]
IPv6: [ '2001:67c:4e8:f004::9' ]
api.telegram.org는 IPv4, IPv6 둘 다 지원.
4단계: IPv4 직접 연결 테스트
IPv4 IP로 직접 연결 → 성공. IPv6 경로가 문제라는 것을 확인.
원인
api.telegram.org는 IPv4/IPv6 듀얼 스택
Node.js는 기본적으로 IPv6를 우선 사용 (happy eyeballs 알고리즘)
Docker 기본 네트워크(bridge)는 IPv6 외부 라우팅을 지원하지 않는 경우가 많음
IPv6로 연결 시도 → 라우팅 불가 → ETIMEDOUT
wget은 IPv4를 우선 사용하기 때문에 성공
간헐적으로 알림이 온 이유: Node.js가 IPv4로 먼저 연결되는 경우도 있었기 때문
해결
axios 인스턴스에 family: 4 옵션을 추가하여 IPv4 연결을 강제.
// 변경 전
axios.post(this.apiUrl, { chat_id: this.chatId, text }).catch(() => {});
// 변경 후
this.httpClient = axios.create({
timeout: 10000,
family: 4, // IPv4 강제
});
this.httpClient.post(this.apiUrl, { chat_id: this.chatId, text }).catch(() => {});
핵심 정리
┌──────┬───────────────────────────────────────────────┐
│ 항목 │ 내용 │
├──────┼───────────────────────────────────────────────┤
│ 환경 │ Docker (bridge network) + Node.js │
├──────┼───────────────────────────────────────────────┤
│ 증상 │ 외부 API 호출 간헐적 ETIMEDOUT │
├──────┼───────────────────────────────────────────────┤
│ 원인 │ Node.js IPv6 우선 + Docker IPv6 라우팅 미지원 │
├──────┼───────────────────────────────────────────────┤
│ 해결 │ family: 4로 IPv4 강제 │
├──────┼───────────────────────────────────────────────┤
│ 교훈 │ .catch(() => {})로 에러를 삼키지 말 것 │
└──────┴───────────────────────────────────────────────┘