connection refused / ECONNREFUSED 127.0.0.1 30초 분기 런북
'connection refused'는 "무응답"이 아니라 "적극적 거부"다
배포 직후 curl이 Connection refused를 뱉거나, Node 앱이 ECONNREFUSED 127.0.0.1:5432로 죽는 순간 가장 먼저 해야 할 일은 이 에러의 정체를 정확히 아는 것입니다.
핵심은 한 줄입니다. connection refused는 패킷이 대상에 도달했고, 대상이 TCP RST로 "그 포트에서 아무도 안 듣는다"고 즉시 응답한 상태입니다. 즉 네트워크는 멀쩡합니다. 길이 막힌 게(timeout) 아니라, 도착했는데 문 앞에서 거절당한 겁니다. 이 한 줄이 진단 방향을 통째로 바꿉니다.
먼저 옆 에러들과 구분하자
| 에러 신호 | TCP 동작 | 1차 의심 원인 |
|---|---|---|
| connection refused | 즉시 RST 수신 (빠름) | 프로세스 미기동 / 포트·바인드 불일치 |
| timeout (no route, hang) | 응답 없음, 수 초~수십 초 대기 | 방화벽 DROP / 보안그룹 / 라우팅 |
| EADDRINUSE | 서버 기동 시 바인드 실패 | 이미 그 포트를 점유한 프로세스 |
| 502 Bad Gateway | 프록시는 떴으나 업스트림 거부 | 백엔드가 위 1~2번 상태 |
timeout이면 방화벽/라우팅부터 의심하고, refused면 거의 항상 "서버 측" 문제입니다. 거부는 빠르고, 차단(DROP)은 느립니다. 이 속도 차이만으로도 절반은 잡힙니다.
30초 분기 런북: 4단계 의사결정 트리
에러를 본 직후 위에서 아래로 따라가세요.
① 프로세스가 떠 있나? → ss -tlnp / systemctl status
└ 없음 → 서버 기동 (원인 확정)
└ 있음 ↓
② 포트/바인드가 맞나? → ss 출력의 127.0.0.1:PORT vs 0.0.0.0:PORT
└ 127.0.0.1만 리스닝인데 외부 접속 → 바인드 변경 (원인 확정)
└ 맞음 ↓
③ 중간에 막혔나? → nc -zv / ufw status / docker ps 포트매핑
└ 방화벽/보안그룹/매핑 누락 → 규칙 허용 (원인 확정)
└ 통과 ↓
④ 이름이 잘못 풀리나? → localhost가 ::1(IPv6)로 풀리는지 확인
└ IPv4만 리스닝 + IPv6 우선 해석 → 127.0.0.1 직접 사용위험도 라벨 복붙 명령표
| 라벨 | 명령 | 무엇을 확인/변경 |
|---|---|---|
| 🟢 안전 | ss -tlnp | 어떤 프로세스가 어떤 IP:포트로 리스닝 중인지 |
| 🟢 안전 | systemctl status <svc> | 서비스가 실제로 active인지 |
| 🟢 안전 | nc -zv host port | 해당 포트로 TCP 연결이 되는지(refused/timeout 구분) |
| 🟢 안전 | telnet host port | nc 없을 때 동일 용도 |
| 🟢 안전 | docker ps | PORTS 컬럼(0.0.0.0:8080->80)로 매핑 유무 |
| 🟢 안전 | ufw status | ufw 인바운드 허용 규칙 확인 |
| 🟢 안전 | iptables -L -n | 체인별 ACCEPT/DROP 규칙 확인 |
| 🟡 주의 | sudo ufw allow 8080/tcp | 인바운드 포트 개방(상태 변경) |
| 🟡 주의 | sudo systemctl restart <svc> | 서비스 재기동(다운타임 발생) |
| 🟡 주의 | docker run -p 8080:80 ... | 포트 매핑 재설정으로 재기동 |
🟢는 마음껏 돌려도 됩니다. 🟡는 운영 환경이면 한 번 더 생각하세요.
핵심 함정: 127.0.0.1 바인드 vs 0.0.0.0 바인드
현업에서 "로컬에선 되는데 컨테이너/원격에선 refused"의 80%는 이거 하나입니다. 직접 재현해 봅시다.
# A. 루프백에만 바인드
python -m http.server --bind 127.0.0.1 8000
# B. 모든 인터페이스에 바인드
python -m http.server --bind 0.0.0.0 8000각각 띄우고 ss -tlnp를 보면 차이가 명확합니다.
# A의 경우
LISTEN 0 128 127.0.0.1:8000 0.0.0.0:* users:(("python",pid=...))
# B의 경우
LISTEN 0 128 0.0.0.0:8000 0.0.0.0:* users:(("python",pid=...))A는 127.0.0.1:8000, 즉 같은 머신 내부에서만 응답합니다. 다른 호스트나 컨테이너 밖에서 접속하면 커널이 "이 IP로는 그 포트에 리스너 없음" → 즉시 RST → connection refused. 반면 B는 외부 IP로 들어온 패킷도 받습니다.
실무 경험 한 줄: 프레임워크 기본값이 함정입니다. Flask
app.run(), Rails, 일부 dev 서버는 기본이127.0.0.1입니다. "내 노트북에선 멀쩡한데 EC2/도커에서만 거부"가 뜨면 코드부터 보지 말고ss -tlnp로 바인드 주소부터 확인하세요. 저는 이걸로 날린 시간이 며칠치는 됩니다.
같은 에러, 다른 얼굴: 언어/툴별 메시지 매핑
아래는 전부 동일한 TCP RST 신호입니다. 메시지만 다를 뿐 진단 루트는 같습니다.
| 도구 | 에러 메시지 |
|---|---|
| curl | curl: (7) Failed to connect ... Connection refused |
| Node | Error: connect ECONNREFUSED 127.0.0.1:5432 |
| Go | dial tcp 127.0.0.1:6379: connect: connection refused |
| psql | could not connect to server: Connection refused |
| redis-cli | Could not connect to Redis ... Connection refused |
케이스별 진단
- curl:
curl -v http://host:port→ 즉시 refused면 서버 미기동/포트 오타.nc -zv host port로 교차 검증. - Node ECONNREFUSED 127.0.0.1:5432: DB 호스트가
localhost인데 DB가 컨테이너/원격에 있는 경우가 흔함. 연결 문자열의 host 확인 후ss -tlnp | grep 5432. - Go dial tcp: 동일. 의존 서비스가 아직 안 떴는데 앱이 먼저 뜬 부팅 순서 문제도 잦음(
depends_on/헬스체크로 해결). - psql/redis-cli: 서버는 떴는데
127.0.0.1만 리스닝, 클라이언트는 외부에서 접속 → 바인드 또는bind설정(redisbind 127.0.0.1, postgreslisten_addresses) 확인.
Docker·클라우드 특화 함정
컨테이너 환경에서 refused가 급증하는 이유는 명확합니다.
-
컨테이너 내부 앱이
127.0.0.1에 바인드 →docker run -p 8080:80을 해도 거부됩니다.-p는 호스트 → 컨테이너 외부 인터페이스로 트래픽을 넘기는데, 앱이 컨테이너의 루프백에만 듣고 있으면 도달할 리스너가 없습니다. 컨테이너 안에서는 반드시0.0.0.0에 바인드하세요. -
컨테이너 → 호스트 접속: 컨테이너 안의
localhost는 호스트가 아니라 컨테이너 자신입니다. 호스트 서비스에 붙으려면host.docker.internal(Mac/Windows, 최신 리눅스는--add-host)을 쓰세요. -
AWS 보안그룹/인바운드: 다만 보안그룹이 막으면 보통 **timeout(DROP)**이지 refused가 아닙니다. refused인데 보안그룹을 의심한다면 방향이 틀린 겁니다. 단, NLB/타깃그룹이 닫힌 포트로 헬스체크를 보내면 refused가 표면화될 수 있습니다.
-
IPv6 우선 해석:
localhost가::1로 먼저 풀리는데 서버가 IPv4(0.0.0.0)에만 리스닝하면 refused가 납니다. 임시 회피는127.0.0.1을 명시적으로 쓰는 것.
결론: 4단계 체크리스트 카드
[ ] ① ss -tlnp 로 프로세스/포트 리스닝 확인 (없으면 → 기동)
[ ] ② 바인드 주소 127.0.0.1 vs 0.0.0.0 확인 (루프백만이면 → 0.0.0.0)
[ ] ③ nc -zv / ufw status / docker ps 매핑 (막혔으면 → 규칙·매핑 허용)
[ ] ④ localhost가 ::1로 풀리는지 (IPv6 이슈면 → 127.0.0.1 명시)refused는 거의 항상 서버 측 문제, 그것도 "안 떴거나 / 엉뚱한 주소에 떴거나"입니다. 위에서 아래로 30초면 끝납니다.
자주 묻는 질문 (FAQ)
Q. connection refused와 timeout, 빠르게 구분하는 법은?
A. nc -zv host port를 쳐보세요. 즉시 실패하며 refused가 뜨면 서버 미기동·포트 불일치(서버 측), 수 초 이상 멈췄다가 실패하면 방화벽 DROP·보안그룹·라우팅 문제입니다. 속도가 곧 단서입니다.
Q. 로컬에선 되는데 컨테이너/원격에서만 refused가 나요.
A. 십중팔구 앱이 127.0.0.1에 바인드되어 있습니다. ss -tlnp로 확인하고 0.0.0.0으로 바꾸세요. 컨테이너라면 내부 앱의 바인드 주소가 0.0.0.0이어야 -p 매핑이 동작합니다.
Q. ECONNREFUSED 127.0.0.1:5432, DB 주소는 맞는데 왜 거부되나요?
A. (1) DB 프로세스가 안 떴거나, (2) DB가 컨테이너/원격에 있는데 host를 localhost로 지정했거나, (3) postgres listen_addresses/redis bind가 루프백으로 제한된 경우입니다. ss -tlnp | grep 5432로 실제 리스닝 주소부터 확인하세요.
이 글은 AI 에이전트가 자료 조사와 1차 초안 작성을 담당하고, 사람 편집자가 사실관계·출처·톤과 맥락을 검토한 뒤 발행했습니다. 환경(OS·버전)에 따라 결과가 다를 수 있으니 적용 전 공식 문서를 함께 확인하세요. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.
댓글
첫 번째 댓글을 남겨보세요.