docker: driver failed programming external connectivity (iptables)
안녕하세요, BlueByte입니다. docker run -p가 갑자기 포트 게시를 거부하고, 최근에 방화벽을 건드렸다면 원인이 아주 분명합니다. 겁먹지 않으셔도 됩니다 — 오류를 읽고, 무슨 일이 있었는지 확인하고, 사라진 것을 다시 세운 뒤, 다음 방화벽 reload가 이걸 또 지우지 않게 하는 순서로 하나씩 짚어보겠습니다.
이 오류가 실제로 말하는 것
포트 게시가 이렇게 실패합니다:
docker: Error response from daemon: driver failed programming external
connectivity on endpoint web: (iptables failed: iptables -t nat -A DOCKER ...
No chain/target/match by that name)Failed to program NAT chain: ... No chain/target/match by that name나, docker compose up에선 그냥 driver failed programming external connectivity로도 나옵니다. 마지막 줄만 보시면 됩니다 — 데몬이 NAT 규칙을 추가하려는데 의존하는 DOCKER 체인이 없는 것입니다. 이미지·컴포즈 파일·포트 번호는 멀쩡합니다. 망가진 건 Docker 아래에 깔린 호스트의 방화벽 상태입니다.
방화벽 reload가 DOCKER 체인을 지우는 이유
Docker는 포트를 게시한 컨테이너를 띄울 때 nat 테이블에 DOCKER 체인과 컨테이너별 규칙을 만듭니다. 무언가가 실행 중인 데몬 밑에서 그 체인을 지운 겁니다. 흔한 범인은 이렇습니다.
- 방화벽 reload —
firewall-cmd --reload,ufwenable/reload,netfilter-persistent— 가 자체 설정으로 룰셋을 다시 만들었는데 거기엔 Docker 체인이 없음. --noflush없는 수동iptables-restore— 테이블 전체를 교체하며 Docker 체인까지 가져감.- 주기적으로 iptables를 flush하는 하드닝 도구나 cron 작업.
체인이 정말 사라졌는지 먼저 확인하기
추측할 필요 없습니다. 직접 확인해 보면 됩니다:
sudo iptables -t nat -L DOCKER -nNo chain/target/match by that name이면 지워진 게 맞습니다. 무엇이 지웠는지는 iptables를 다시 쓰는 서비스와 최근 실행 시각으로 찾습니다:
systemctl status firewalld ufw netfilter-persistent
sudo journalctl -u firewalld --since "15 min ago"깨진 시점이 reload나 재부팅과 맞아떨어지면 그게 원인입니다.
데몬을 재시작해 규칙을 다시 세우기
Docker를 재시작하면 체인과 컨테이너별 규칙이 다시 만들어집니다. 외울 것도 없습니다 — 명령 하나면 됩니다:
sudo systemctl restart docker
sudo iptables -t nat -L DOCKER -n # 체인과 규칙이 돌아옴그다음 reload가 또 지우지 않게 합니다. firewalld 호스트라면 firewalld와 Docker 통합을 함께 켜 두세요 — Docker는 firewalld를 통해 규칙을 넣으므로, reload가 규칙을 버리지 않고 다시 적용합니다:
sudo firewall-cmd --reload
sudo iptables -t nat -L DOCKER -n # 여전히 존재iptables-restore로 스크립트화한다면 --noflush를 붙여 테이블 전체 교체 대신 덧붙이게 하세요.
실제 사례: 오후 2시의 reload
방화벽 규칙을 하나 손보고 reload한 뒤로 모든 docker run -p가 실패한다고 합시다. iptables -t nat -L DOCKER -n이 "No chain/target/match by that name"을 반환하고, journalctl -u firewalld에는 실패가 시작되기 2분 전 reload가 찍혀 있습니다. 이게 지문입니다 — reload가 체인을 flush한 것이죠. sudo systemctl restart docker로 체인을 다시 만들면 같은 iptables 명령이 이제 체인을 보여주고, 컨테이너가 정상 게시됩니다. 다음 firewalld reload에도 체인은 살아남습니다 — Docker가 firewalld를 우회하지 않고 통해서 규칙을 등록했기 때문입니다.
고친 뒤 끝까지 되는지 보기
규칙이 맞아 보이는 것에서 그치지 말고, 실제로 되는지 확인합니다:
docker run --rm -p 8080:80 nginx:alpine
curl -sSI http://localhost:8080 | head -1 # HTTP/1.1 200 OK200 응답과 iptables -t nat -L DOCKER -n의 규칙이 함께 보이면 해결이 유지된 것입니다.
다시 겪지 않으려면
부팅 시 방화벽이 Docker보다 먼저 뜨게 유닛 순서를 잡거나, firewalld가 통합을 담당하게 해 reload가 체인을 벗기지 않게 하세요. 자주 reload하는 호스트라면 nat 테이블에 대해 맨 iptables -F나 범위 없는 iptables-restore를 피하고, flush를 자기 체인으로 한정하세요.
"port is already allocated"와의 구분
Bind for 0.0.0.0:8080 failed: port is already allocated는 반대 경우입니다 — 체인은 멀쩡한데 다른 컨테이너가 이미 포트를 쥔 것이죠. 그리고 /etc/docker/daemon.json의 "iptables": false는 게시 포트 연결을 통째로 꺼서 이 오류를 잠재우는 것뿐이라, 외부에서 컨테이너에 닿을 수 없게 됩니다. 모든 NAT 규칙을 직접 관리하는 게 아니라면 iptables 관리는 켜 두세요. 다음에 방화벽을 만진 직후 포트 게시가 실패하면, DOCKER 체인부터 확인해 보세요 — 거의 항상 이 문제입니다.
관련 질문
데몬을 재시작하면 돌던 컨테이너가 멈추나요?
restart 정책이 always나 unless-stopped가 아닌 컨테이너는 재시작됩니다. 운영에서 재시작 전 docker inspect로 정책을 확인하세요.
재부팅할 때마다 다시 생깁니다. 왜죠?
부팅 시 방화벽 서비스가 Docker보다 늦게 떠서 체인을 flush하는 상황입니다. firewalld가 Docker 통합을 담당하게 하거나, 방화벽이 Docker보다 먼저 뜨도록 유닛 순서를 잡으세요.
재시작 없이 DOCKER 체인만 다시 넣을 수 없나요?
손으로 규칙을 재생성할 수 있지만, 데몬 재시작이 지원되는 방법입니다 — 모든 체인을 일관되게 다시 만듭니다. 수동 규칙 수술은 Docker가 기대하는 상태에서 어긋나 다음 컨테이너 시작에서 깨지기 쉽습니다.
host 네트워크 컨테이너도 영향받나요?
아니요. host 네트워크 컨테이너는 게시 포트를 안 써서 DOCKER 체인을 건드리지 않습니다. bridge 네트워크에서 포트를 게시하는 컨테이너만 이 문제를 겪습니다.
OS 업그레이드 직후 시작됐습니다. 관련 있나요?
그럴 수 있습니다. 업그레이드가 iptables 백엔드(legacy vs nf_tables)를 바꿀 수 있습니다. 호스트와 Docker가 같은 백엔드를 쓰게 한 뒤 Docker를 재시작해 체인을 그 위에 다시 만드세요.
참고 자료
Haneul Seo
Infrastructure engineer · 10+ years running Linux fleets
같은 카테고리 다른 글
Git: fatal: detected dubious ownership in repository
Git 2.35.2의 CVE-2022-24765 수정 이후, Git은 작업 트리나 .git 디렉터리의 소유자가 명령을 실행한 사용자와 다르면 저장소를 읽지 않습니다. 컨테이너, CI 작업, sudo 세션, 공유 드라이브에서 주로 나타납니다. 저장소가 내 것이어야 한다면 소유권을 바로잡고, 아니라면 global 설정의 safe.directory에 정확한 경로를 추가하세요 — Git은 이 설정을 저장소 자신의 설정에서는 무시합니다.
Kubernetes: Internal error occurred: failed calling webhook
admission webhook이 쓰기 요청 앞에 서 있는데 API server가 응답을 받지 못했고, 기본값인 failurePolicy: Fail이 그 침묵을 거부로 바꾼 것입니다. 메시지 끝부분이 곧 진단입니다 — context deadline exceeded는 호출이 도달하지 못한 것, no endpoints available은 떠 있는 게 없는 것, x509 줄은 API server가 webhook 인증서를 신뢰하지 않는 것입니다. 원인마다 해결이 다르고, 어느 것도 매니페스트 문제가 아닙니다.
MySQL: ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
다른 트랜잭션이 쥐고 있는 행 잠금을 innodb_lock_wait_timeout 만큼 기다리다 포기한 것입니다. sys.innodb_lock_waits 가 막고 있는 세션을 지목하고 KILL 문까지 만들어 주며, blocking_query 가 NULL 이면 블로커가 열린 트랜잭션 위에서 놀고 있다는 뜻입니다. 재시도 로직이 가장 자주 틀리는 지점은 따로 있습니다. 기본값에서는 타임아웃된 문장 하나만 롤백되므로, 트랜잭션은 그대로 열린 채 앞서 잡은 잠금을 전부 쥐고 있습니다.
Redis: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk
읽기는 되는데 모든 쓰기가 거부됩니다. 마지막 백그라운드 저장이 실패했고 stop-writes-on-bgsave-error 기본값이 yes 이기 때문입니다. 진짜 원인은 로그에 적혀 있습니다 — 디스크 공간 부족, redis 사용자가 쓸 수 없는 dir, rename 시점의 read-only 마운트, 또는 fork 가 Cannot allocate memory 로 실패하는 경우입니다. 원인을 고치고 BGSAVE 한 번만 돌리면 재시작 없이 쓰기가 돌아옵니다. rdb_last_bgsave_status 가 err 에서 ok 로 바뀝니다. stop-writes-on-bgsave-error no 는 쓰기를 즉시 되살리지만 스냅샷은 여전히 실패한 상태로 두므로, 해결이 아니라 의도한 맞교환으로 다뤄야 합니다.
Node.js: FATAL ERROR: Reached heap limit — JavaScript heap out of memory (종료 코드 134)
V8 힙에는 시스템 메모리와 Node 릴리스에서 유도되는 자기만의 천장이 있고, 가진 RAM 보다 훨씬 낮은 경우가 흔합니다. 빌드나 서버가 거기 닿으면 V8 은 FATAL ERROR: Reached heap limit 과 종료 코드 134 로 스스로 중단합니다. v8.getHeapStatistics().heap_size_limit 으로 실제 한도를 읽은 뒤, 큰 작업은 --max-old-space-size(MiB)나 NODE_OPTIONS 로 올리고, 컨테이너 안에서는 cgroup 한도보다 낮게 잡고, 오래 도는 프로세스의 누수는 --heapsnapshot-near-heap-limit 으로 잡으세요. FATAL ERROR 줄 없이 137 로 끝나면 컨테이너 kill 이지 이 오류가 아닙니다.
Docker: 컨테이너 시작 직후 "exec format error" — 플랫폼이 다른 이미지, 에뮬레이터 없음, 셔뱅 없는 스크립트
컨테이너가 첫 명령에서 exec format error 로 끝납니다. 커널의 ENOEXEC, 즉 파일은 있지만 여기서는 실행할 수 없다는 뜻입니다. 실제로는 한 CPU 아키텍처에서 빌드한 이미지(Apple 실리콘 Mac은 linux/arm64 를 만듭니다)를 binfmt_misc 에 QEMU 핸들러가 없는 다른 아키텍처(x86_64 서버)에서 돌리거나, 엔트리포인트 스크립트의 첫 줄이 셔뱅이 아닌 경우입니다. uname -m, docker image inspect, ls /proc/sys/fs/binfmt_misc 로 원인을 가르고, docker buildx build --platform 을 명시(또는 두 플랫폼 매니페스트 목록 발행)하거나, 에뮬레이션이 목적이면 QEMU 등록과 --platform, 스크립트라면 #!/bin/sh 한 줄로 해결합니다.