Ghost getaddrinfo ENOTFOUND db 해결: Docker Compose에서 MySQL을 찾지 못할 때
Ghost를 Docker Compose로 운영하다가 컨테이너 로그에 다음 오류가 반복될 수 있다.
Invalid database host.
Error Code: ENOTFOUND
Error: getaddrinfo ENOTFOUND db
경우에 따라 마지막 값이 db:3306으로 보이기도 한다.
Error: getaddrinfo ENOTFOUND db:3306
이 오류는 MySQL 비밀번호가 틀렸다는 뜻이 아니다. Node.js 공식 문서에서 ENOTFOUND는 DNS 조회 실패다. Ghost가 MySQL 인증을 시도하기도 전에 db라는 호스트명을 IP 주소로 바꾸지 못한 상태다.
이 오류는 DB가 중지된 상태에서 Ghost만 --no-deps로 기동했을 때도 발생할 수 있다. 해결의 출발점은 MySQL 볼륨 삭제나 Ghost 재설치가 아니라 DB 실행 상태, host·port 설정, Compose 서비스명과 공통 네트워크를 순서대로 확인하는 것이다.
이 글의 명령은 서비스명을
ghost,db로 일반화한 예시다. 실제 Compose 파일의 서비스명이 다르면 그대로 바꿔 실행한다. 비밀번호·토큰·서버 주소가 포함될 수 있는 전체 환경변수나docker inspect원문은 외부에 공유하지 않는다.
3줄 요약
ENOTFOUND는 인증 실패가 아니라 이름 해석 실패다. 비밀번호를 바꾸기 전에 DB 서비스명과 네트워크부터 확인한다.- Ghost의 DB host는
db, port는3306처럼 분리한다. host에db:3306을 통째로 넣으면 잘못된 호스트명을 조회할 수 있다. docker compose down -v나 DB 볼륨 삭제는 해결책이 아니다. 먼저config,ps -a, 내부 DNS, 공통 네트워크를 비파괴 방식으로 확인한다.
ENOTFOUND·ECONNREFUSED·ETIMEDOUT을 먼저 구분한다
비슷해 보이는 연결 오류도 실패한 단계가 다르다.
| 오류 | 공식 의미 | 먼저 확인할 곳 |
|---|---|---|
ENOTFOUND |
DNS 조회 실패 | DB host 값, Compose 서비스명, 공통 네트워크 |
ECONNREFUSED |
대상이 연결을 거부함 | DB 프로세스, 컨테이너 상태, 포트, 준비 완료 여부 |
ETIMEDOUT |
연결 또는 전송 시간 초과 | 네트워크 경로, 방화벽, 원격 DB 응답 |
getaddrinfo ENOTFOUND db라면 MySQL 계정 권한부터 바꾸지 않는다. 이름 조회가 실패한 단계에서는 아직 인증 요청이 MySQL까지 도착하지 않았기 때문이다.
또한 Compose의 version is obsolete나 networks.external.name is deprecated는 구성 문법 경고다. 같은 로그에 보여도 ENOTFOUND의 직접 원인은 아니다. 경고와 실행 오류를 분리하는 방법은 Docker Compose version·external.name 경고 해결법에서 자세히 설명했다.
1단계: 실제 Compose 서비스명과 설정을 확인한다
Compose가 최종적으로 해석한 구성을 먼저 검증한다.
docker compose config -q
docker compose config --services
Docker 공식 문서에서 config -q는 출력 없이 구성을 검증하고, --services는 서비스명을 한 줄씩 출력한다.
예상 결과가 다음과 같다고 가정한다.
ghost
db
Ghost의 DB host가 db라면 서비스 목록에도 db가 있어야 한다. 실제 서비스명이 mysql인데 Ghost 설정은 db를 바라본다면 둘 중 하나를 일치시킨다.
Compose는 기본 네트워크에서 서비스명을 내부 DNS에 등록한다. 따라서 같은 네트워크의 Ghost는 MySQL 컨테이너의 일시적인 IP가 아니라 안정적인 서비스명으로 연결해야 한다.
2단계: host와 port를 분리한다
Ghost 공식 설정은 데이터베이스 주소와 포트를 별도 항목으로 받는다.
services:
ghost:
environment:
database__client: mysql
database__connection__host: db
database__connection__port: 3306
다음처럼 host에 포트를 합치지 않는다.
database__connection__host: db:3306
db:3306 전체가 호스트명으로 전달되면 로그에 getaddrinfo ENOTFOUND db:3306가 나타날 수 있다. 올바른 형태는 host db, port 3306이다.
실제 컨테이너에서 host 값만 제한적으로 확인할 수 있다.
docker compose exec ghost sh -lc \
'printf "database_host=%s\n" "$database__connection__host"'
전체 env나 docker compose config 출력에는 DB·SMTP 비밀번호가 섞일 수 있으므로 외부에 그대로 붙이지 않는다.
3단계: DB 컨테이너가 실행 중인지 확인한다
실행 중인 컨테이너만 보여 주는 목록보다 중지된 서비스까지 확인한다.
docker compose ps -a
다음 항목을 본다.
ghost와db가 모두 존재하는가- DB 상태가
running또는healthy인가 - DB가
exited,created,restarting상태는 아닌가 - 다른 디렉터리나 다른 Portainer Stack의 서비스를 보고 있지는 않은가
DB가 멈췄다면 삭제하지 말고 로그부터 제한적으로 확인한다.
docker compose logs --tail=100 db
docker compose logs --tail=100 ghost
단순히 DB가 시작되지 않은 상태라면 다음처럼 DB를 먼저 기동한 뒤 전체 상태를 확인한다.
docker compose up -d db
docker compose ps
4단계: --no-deps 사용 여부를 확인한다
Docker 공식 문서에서 docker compose up --no-deps는 연결된 의존 서비스를 시작하지 않는 옵션이다.
docker compose up -d --no-deps ghost
DB가 이미 정상 실행 중이라면 특정 서비스만 재생성할 때 사용할 수 있다. ghost에 depends_on: db가 선언돼 있어도 --no-deps를 사용하면 DB를 시작하지 않는다. depends_on이 없다면 ghost만 지정해 기동하는 것 자체가 DB 시작을 보장하지 않는다.
전체 스택을 정상화할 때는 의존 서비스를 제외하지 않는다.
docker compose up -d --wait
--wait는 서비스가 running 또는 healthy 상태가 될 때까지 기다린다. 다만 DB의 실제 준비 상태를 판단하려면 해당 서비스에 유효한 healthcheck가 있어야 한다. 또한 Compose 구성이나 이미지가 바뀌었다면 up 과정에서 컨테이너가 재생성될 수 있으므로, 적용 전 변경 내용과 서비스 영향을 확인한다.
5단계: Ghost 컨테이너에서 db 이름을 직접 조회한다
DB가 실행 중인데도 같은 오류가 나면 Ghost 컨테이너 내부에서 서비스명 DNS를 확인한다. 다음 docker compose exec 명령은 Ghost 컨테이너가 실행 중일 때만 사용할 수 있다.
docker compose exec ghost getent hosts db
Ghost 이미지에 getent가 없다면 이미지에 포함된 Node.js로 조회할 수 있다.
docker compose exec ghost node -e \
"require('node:dns').lookup('db', (err, address) => console.log(err || address))"
정상이라면 Docker 네트워크의 내부 IP가 출력된다. 같은 ENOTFOUND가 나오면 서비스명이나 네트워크 연결을 계속 점검한다.
컨테이너 IP를 Compose 파일에 하드코딩해 우회하지 않는다. Docker 공식 문서에 따르면 컨테이너를 재생성할 때 IP는 달라질 수 있지만 서비스명은 유지된다.
6단계: 두 서비스가 같은 네트워크인지 확인한다
Compose는 기본적으로 프로젝트용 네트워크를 만들고 모든 서비스를 연결한다. 사용자 정의 네트워크를 쓴다면 Ghost와 DB가 최소 하나의 네트워크를 공유해야 한다.
services:
ghost:
image: ghost:6
environment:
database__client: mysql
database__connection__host: db
database__connection__port: 3306
networks:
- internal
db:
image: mysql:8.0
networks:
- internal
networks:
internal:
네트워크 이름을 확인하고 대상 네트워크만 검사한다.
docker compose config --networks
docker network ls
docker network inspect ACTUAL_NETWORK_NAME
ACTUAL_NETWORK_NAME에는 docker network ls에서 확인한 실제 네트워크 이름을 넣는다. Containers 항목에는 Ghost와 DB가 모두 있어야 한다. 여러 Compose 프로젝트나 Portainer Stack을 나눠 운영한다면 같은 외부 네트워크에 두 서비스가 모두 명시적으로 연결되고, DB 서비스명 또는 네트워크 별칭을 해당 네트워크에서 찾을 수 있어야 한다.
Portainer에서 Stack을 다시 만들면서 프로젝트명이 바뀌면 기본 네트워크 이름도 바뀔 수 있다. 이때 기존 컨테이너와 새 컨테이너가 서로 다른 네트워크에 남지 않았는지 확인한다. 관리면 자체의 접근 통제는 Portainer·Agent·Docker 관리면 점검을 참고한다.
7단계: depends_on과 healthcheck의 역할을 구분한다
Compose는 의존 순서대로 컨테이너를 시작하지만, 기본 상태에서는 컨테이너가 실행됐는지만 보고 애플리케이션이 요청을 처리할 준비까지 기다리지는 않는다.
DB가 실제로 준비된 뒤 Ghost를 시작하려면 healthcheck와 service_healthy를 조합한다.
services:
ghost:
depends_on:
db:
condition: service_healthy
db:
image: mysql:8.0
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 --silent"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
위 mysqladmin ping 예시는 MySQL 서버 프로세스가 응답하는지를 확인하는 용도다. MySQL 공식 문서에 따르면 서버가 실행 중이면 인증이 거부되더라도 ping의 종료 코드는 성공일 수 있으므로, 이 검사만으로 Ghost 계정·비밀번호·스키마 접근까지 검증됐다고 판단하면 안 된다. 현재 이미지 안에서 명령을 시험하고, 애플리케이션 로그로 Ghost의 실제 연결도 별도로 확인한다. 비밀번호를 healthcheck 명령줄에 직접 노출하지 않도록 현재의 비밀 관리 방식도 함께 검토한다.
service_healthy는 잘못된 host 값이나 서로 다른 네트워크를 자동으로 고쳐 주지 않는다. 시작 시점 문제를 줄이는 장치이지 DNS 설정의 대체 수단은 아니다.
MySQL 3306 포트를 외부에 열어야 할까
Ghost와 MySQL이 같은 Docker 네트워크에서 통신한다면 Ghost는 db:3306의 컨테이너 포트를 사용한다. 호스트의 3306:3306 포트 공개는 내부 서비스 간 연결에 필요하지 않다.
문제를 해결하려고 MySQL 포트를 인터넷에 공개하지 않는다. 외부 관리가 꼭 필요하다면 접근 출처, 방화벽, TLS, 계정 권한을 별도로 설계한다. 관련 점검 순서는 외부 포트와 방화벽 점검에서 확인할 수 있다.
복구 확인
변경 뒤에는 컨테이너가 단순히 Up인지에 그치지 말고 구성, DNS, 로그, HTTP를 차례로 확인한다.
docker compose config -q
docker compose ps
docker compose exec ghost node -e \
"require('node:dns').lookup('db', (err, address) => console.log(err || address))"
docker compose logs --since=10m ghost
curl -I https://mlog.me/
확인 기준:
db가running또는healthy상태다.- Ghost 컨테이너에서
db가 내부 IP로 해석된다. - Ghost 로그에 새
ENOTFOUND db가 쌓이지 않는다. - 블로그 홈이 정상 HTTP 응답을 반환한다.
- Ghost Admin 로그인과 게시물 조회가 정상이다.
절대 먼저 실행하지 말아야 할 명령
# 실행 금지: 데이터베이스 볼륨이 삭제될 수 있음
# docker compose down -v
# docker volume rm DATABASE_VOLUME
Docker 공식 문서에서 down -v는 Compose 파일에 선언된 named volume과 컨테이너에 연결된 anonymous volume을 제거한다. MySQL 데이터가 해당 볼륨에 있다면 복구가 어려워질 수 있다.
ENOTFOUND를 해결하는 데 DB 데이터 삭제는 필요하지 않다. 별도의 손상이나 마이그레이션 문제로 볼륨 작업이 필요하다면 먼저 Ghost·MySQL 백업 무결성을 확인하고 실제 복원 테스트까지 마친다.
증상별 빠른 점검표
| 증상 | 가능성이 높은 원인 | 다음 확인 |
|---|---|---|
ENOTFOUND db |
서비스명 불일치, DB 미기동, 공통 네트워크 없음 | config --services, ps -a, 내부 DNS |
ENOTFOUND db:3306 |
host에 port까지 합쳐 입력 | host db, port 3306으로 분리 |
ECONNREFUSED db:3306 |
DB 프로세스 미기동 또는 준비 전 | DB 로그, healthcheck, --wait |
ETIMEDOUT |
네트워크 격리, 방화벽, 원격 DB 경로 | 네트워크 멤버십과 경로 점검 |
Access denied |
DB 사용자·비밀번호·권한 문제 | 이때부터 인증 설정 점검 |
재발 방지 체크리스트
- [ ] Ghost의 DB host가 실제 Compose 서비스명과 같은가
- [ ] host와 port를
db,3306으로 분리했는가 - [ ] Ghost와 DB가 하나 이상의 네트워크를 공유하는가
- [ ] DB가 멈춘 상태에서
--no-deps로 Ghost만 시작하지 않았는가 - [ ] DB healthcheck가 현재 이미지에서 실제로 성공·실패를 구분하는가
- [ ] Ghost가
service_healthy조건을 기다리는가 - [ ] 배포 전
docker compose config -q를 실행하는가 - [ ] 배포 후
docker compose ps와 최근 Ghost 로그를 확인하는가 - [ ] MySQL 3306을 문제 해결 목적으로 외부에 공개하지 않았는가
- [ ] DB 백업과 복원 절차를 정기적으로 검증하는가
- [ ] 외부에 로그를 공유하기 전 비밀번호·IP·계정·토큰을 제거했는가
마무리
getaddrinfo ENOTFOUND db의 핵심은 MySQL 인증이 아니라 Ghost 컨테이너가 db라는 이름을 찾지 못했다는 것이다.
안전한 해결 순서는 다음과 같다.
docker compose config -q와--services로 서비스명을 확인한다.- DB host와 port를 분리한다.
docker compose ps -a로 DB 실행 상태를 확인한다.--no-deps로 의존 서비스를 제외하지 않았는지 본다.- Ghost 내부에서
dbDNS 조회를 시험한다. - Ghost와 DB의 공통 네트워크를 확인한다.
- healthcheck와
service_healthy로 준비 상태를 정의한다. - 로그와 HTTP 응답으로 복구를 검증한다.
이 순서를 따르면 Ghost나 MySQL 데이터를 지우지 않고 이름 해석 단계부터 원인을 좁힐 수 있다.