Docker 컨테이너에서 로컬 MySQL에 연결할 때 localhost가 실패한 이유

애플리케이션의 위치에 따라 호스트명과 포트가 달라지는 Docker DB 연결 경로

배포 전에 Docker 이미지와 컨테이너를 로컬에서 확인하던 중 애플리케이션이 MySQL에 연결하지 못했다. MySQL은 개발 PC에서 실행 중이었고, JDBC 주소도 평소처럼 localhost를 사용하고 있었다. 컨테이너 안에서 애플리케이션을 실행하자 그 주소는 더 이상 PC의 MySQL을 뜻하지 않았다.

컨테이너 안에서 localhost 또는 127.0.0.1은 그 컨테이너 자신이다. 애플리케이션 컨테이너가 자기 내부의 3306 포트를 찾고 있었으니, 호스트 PC에서 실행 중인 MySQL에 닿지 못한 것이다. 당시 Docker Desktop 환경에서 주소를 host.docker.internal로 바꾸어 연결했다.

접속 위치에 맞는 주소 사용

같은 데이터베이스라도 애플리케이션이 어디서 실행되는지에 따라 주소가 달라진다.

애플리케이션 위치 DB 위치 접속 주소 예시
Docker Desktop 컨테이너 개발 PC의 MySQL host.docker.internal:3306
Docker Compose 컨테이너 같은 Compose의 DB 서비스 db:3306
개발 PC의 애플리케이션 포트를 공개한 DB 컨테이너 localhost:3307 등 호스트 포트

Docker Desktop은 컨테이너에서 호스트 서비스를 찾을 때 host.docker.internal이라는 특수 DNS 이름을 제공한다. 이 주소는 컨테이너에서 호스트 내부 IP로 해석된다. 다만 이는 Docker Desktop에서 안내하는 방식이다. 일반 Linux Docker Engine 환경까지 같은 주소가 항상 제공된다고 가정하지 말고, 해당 환경의 네트워크 구성을 확인해야 한다.

Compose 안에서 앱과 DB를 함께 실행한다면 보통 서비스 이름을 사용한다. 서비스 이름이 db이면 앱 컨테이너는 db:3306으로 접근한다. 이때 3307:3306처럼 설정한 호스트 포트는 PC에서 컨테이너로 들어오는 경로에 쓰이며, 같은 Compose 네트워크의 컨테이너끼리는 DB 컨테이너 포트를 사용한다.

services:
  app:
    environment:
      SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/appdb
  db:
    image: mysql:8
    ports:
      - "3307:3306"

이 예에서 앱 컨테이너는 db:3306, 개발 PC의 DB 클라이언트는 localhost:3307로 연결한다. 운영 비밀번호를 Compose 파일에 직접 적어 저장소에 올리지는 말아야 한다.

주소를 바꿔도 연결이 안 될 때

올바른 호스트명을 썼는데도 실패하면 오류 종류를 나눠 본다. Connection refused는 서비스가 해당 인터페이스·포트에서 듣지 않거나 방화벽이 연결을 막는 경우가 많다. 시간 초과는 라우팅·방화벽·네트워크 경로를 확인해야 한다. Access denied라면 서버에는 닿았지만 계정 권한이나 비밀번호가 맞지 않는 것이다.

특히 호스트 DB가 컨테이너에서 들어오는 연결을 허용하는지 확인하되, 테스트를 위해 MySQL 포트를 인터넷 전체에 공개하지 않는다. 개발 PC의 방화벽과 MySQL bind 설정을 바꿔야 한다면 필요한 인터페이스와 출발지 범위만 허용하고, 변경 후에는 다시 제한했는지 확인한다.

당시 문제는 애플리케이션 코드의 JDBC 드라이버가 아니라 네트워크 경계에 있었다. 이후에는 먼저 앱이 호스트에서 도는지 컨테이너에서 도는지, DB는 어디에 있는지 그림으로 적고 주소를 결정한다. 현재 사이트를 PC에서 운영하는 구성은 Windows PC와 Docker와 Cloudflare Tunnel 운영 기록에서 볼 수 있다.

참고 문서: Docker Desktop에서 호스트 서비스에 연결하기, Docker Compose 서비스 간 네트워킹

당시 기록: 배포전 로컬 테스트 중 데이터베이스 연결 문제