본문 바로가기

Backend/Study

[Study] Docker에서 MySQL 연결 안 될 때 해결법

반응형

Docker에서 MySQL 연결 안 될 때 해결법

제가 실무를 하면서 RDS를 사용하지 않고 EC2에서 Docker를 이용하는 경우는 대부분 RDS를 쓰자니 금액적인 부분이 걸려서 혹은 서비스의 범위가 크지 않은 경우에 EC2에서 Docker를 이용해 DB를 올리는 경우가 많았습니다.
그 과정에서 발생했던 자주보였던 이슈에 대해 오늘은 공부해 보려고 합니다. 연결에 관한 부분은 RDMS가 어떤것인지에 상관없이 공통적으로 나타나는 부분이므로 참고하여 알아두면 좋을것 같습니다.


1. Docker MySQL 연결 안 되는 대표적인 3가지 원인

원인 구분 설명 대표 에러 메시지
1️⃣ 네트워크 문제 컨테이너 간 네트워크 미연결 또는 호스트 접근 불가 ECONNREFUSED, Connection timed out
2️⃣ MySQL 설정 문제 bind-address, 사용자(host) 권한 문제 Host not allowed to connect
3️⃣ 환경 변수 / 포트 문제 .env 오타, 포트 중복, MYSQL_ROOT_PASSWORD 누락 Access denied, Connection refused

기본 구조 예시 (정상 연결 기준)

version: '3.8'
services:
  mysql:
    image: mysql:8.0
    container_name: mydb
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_DATABASE: testdb
      MYSQL_USER: testuser
      MYSQL_PASSWORD: testpass
    ports:
      - "3306:3306"
    volumes:
      - ./data/mysql:/var/lib/mysql
    networks:
      - app-net

  app:
    build: .
    depends_on:
      - mysql
    environment:
      DB_HOST: mysql
      DB_PORT: 3306
      DB_USER: testuser
      DB_PASSWORD: testpass
    networks:
      - app-net

networks:
  app-net:
    driver: bridge

- 핵심 포인트

  • appmysql같은 네트워크(app-net)에 있어야 함
  • 애플리케이션에서 DB 접근 시 localhost 대신 서비스명(mysql) 사용해야 함
    DB_HOST=mysql # X: localhost → O: mysql

이 두가지는 docker의 특성인데 docker에서 컨테이너를 동작시킬때 컨테이너 내부ip는 재기동시 바뀔 가능성이 높습니다. 이러한 불편함을 없애는 방법은 같은 네트워크에 등록하면 서비스명으로 해당 컨테이너에 접근하는 것 입니다. 이 방법은 docker로 mysql을 연동하는 것 뿐만이 아니라 컨테이너를 이용하는 서비스들 간에서는 동일하게 적용이 가능합니다.


2. 원인별 실전 해결 가이드

2.1 네트워크 연결 문제

  • 문제 증상
    • 다른 컨테이너에서 MySQL 접근 불가
    • Connection timed out 또는 ECONNREFUSED
  • 원인
    • 컨테이너 간 네트워크 미연결
    • 외부 호스트에서 접근 시 포트 미노출
  • 해결 방법
    • 컨테이너 간 접근 시
      • docker exec -it app ping mysql # ping이 되지 않으면 같은 network에 없다는 뜻
    • 같은 network 연결 확인
      • docker network ls
      • docker network inspect app-net
    • 외부 접속 허용 (로컬에서 접근 시)
      • ports: - "3306:3306"

2.2 MySQL 설정 문제 (bind-address, 사용자 권한)

  • 문제 증상
    • MySQL 컨테이너 실행은 정상인데 외부 접속 실패
      • 에러: Host '172.18.0.5' is not allowed to connect to this MySQL server
  • 원인
    • MySQL이 내부 IP(127.0.0.1)만 바인딩
    • 해당 호스트가 접근 권한(GRANT)이 없음
  • 해결 방법
    • bind-address 확인
      • docker exec -it mydb bash cat /etc/mysql/my.cnf | grep bind-address
      • 기본 설정이 127.0.0.1이라면 아래처럼 수정합니다.
      • # /etc/mysql/my.cnf [mysqld] bind-address=0.0.0.0
    • 사용자 접근 권한 추가
      #root 권한 접속
      mysql -u root -p
      # 모든 호스트에서 testuser로 접속 허용
      GRANT ALL PRIVILEGES ON _._ TO 'testuser'@'%' IDENTIFIED BY 'testpass';
      FLUSH PRIVILEGES;
    • Dockerfile로 자동 적용하려면
      # custom.cnf
      [mysqld] bind-address=0.0.0.0
      #Dockerfile
      FROM mysql:8.0
      COPY ./custom.cnf /etc/mysql/conf.d/custom.cnf

2.3 환경 변수 / 포트 관련 문제

  • 문제 증상
    • 컨테이너는 정상인데 접속 시 Access denied => MYSQL_ROOT_PASSWORD가 없거나 .env 값 불일치
  • 해결 방법
    • 환경 변수 일치 확인
      docker-compose config # 실제 적용된 env 값 확인 가능
    • 포트 충돌 확인
      sudo lsof -i :3306
    • 호스트 PC에서 MySQL 접속 테스트
      mysql -h 127.0.0.1 -P 3306 -u testuser -p

3. 실전 트러블슈팅 케이스

케이스 1: Spring Boot에서 "Connection refused"

spring: datasource: url: jdbc:mysql://mysql:3306/testdb?useSSL=false username: testuser password: testpass

해결: localhostmysql 변경 (같은 네트워크 내 DNS 이름)


케이스 2: “Host not allowed to connect”

mysql> SHOW GRANTS FOR 'testuser'@'%';

없다면 아래 실행:

GRANT ALL PRIVILEGES ON *.* TO 'testuser'@'%' IDENTIFIED BY 'testpass'; FLUSH PRIVILEGES;


케이스 3: “Too many connections”

  • max_connections 설정을 늘려야 함

SET GLOBAL max_connections = 500;

또는 Dockerfile 내 설정:

[mysqld] max_connections=500


4. 실전 적용

4.1 최소 docker-compose 템플릿

version: "3.8"

services:
  mysql:
    image: mysql:8.0
    container_name: mysql8
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_DATABASE: appdb
      MYSQL_USER: appuser
      MYSQL_PASSWORD: apppass
    ports:
      - "3306:3306"
    volumes:
      - ./data/mysql:/var/lib/mysql
      - ./init:/docker-entrypoint-initdb.d  # (옵션) 초기 스키마/데이터
    healthcheck:
      # 중요: $$ 로 이스케이프 (Compose의 env 치환 방지)
      test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p$$MYSQL_ROOT_PASSWORD --silent"]
      interval: 10s
      timeout: 5s
      retries: 10
      start_period: 30s   # 초기 InnoDB 리커버리/DDL 시간 버퍼

  app:
    build: .
    container_name: app
    depends_on:
      mysql:
        condition: service_healthy   # DB healthy 될 때까지 app 시작 대기
    environment:
      DB_HOST: mysql
      DB_PORT: 3306
      DB_NAME: appdb
      DB_USER: appuser
      DB_PASSWORD: apppass
    restart: on-failure

포인트

  • healthcheck.start_period: 초기화(디스크 복구/DDL) 동안 false-negative 방지.
  • retries/interval 튜닝으로 초기 1~3분 지연도 흡수 가능.
  • 앱은 DB_HOST=mysql(서비스명 DNS) 사용. localhost사용 불가

4.2 Spring Boot 연결 예시

application.yml

spring:
  datasource:
    url: jdbc:mysql://mysql:3306/appdb?useSSL=false&allowPublicKeyRetrieval=true
    username: ${DB_USER}
    password: ${DB_PASSWORD}
  sql:
    init:
      mode: always   # (선택) /docker-entrypoint-initdb.d 와 충돌되지 않게 조정
  jpa:
    hibernate:
      ddl-auto: none
  # 연결 재시도/타임아웃(드라이버/풀 옵션) 예시
  datasource.hikari:
    initializationFailTimeout: 0
    connectionTimeout: 10000
    validationTimeout: 5000
    maximumPoolSize: 10

실무 팁: 초기 접속 실패 시 앱이 바로 죽지 않게 initializationFailTimeout=0로 지연 허용, 재시도 로직(백오프)도 있으면 안정적.


5. 자주 터지는 함정 & 해결

  1. healthcheck가 계속 Unhealthy
    • 비번 오타/이스케이프 미적용: -p$$MYSQL_ROOT_PASSWORD인지 확인
    • MySQL이 아직 초기화 중: start_period↑ / retries
    • 컨테이너 내부에서만 체크해야 하므로 -h 127.0.0.1 사용 유지
  2. 앱이 DB Healthy 전부터 실행됨
    • Compose(로컬)는 위 예제가 OK.
    • Swarm/stack에선 depends_on.condition 미지원 → wait-for-it/dockerize로 대기 스크립트 사용.

(A) wait-for-it.sh

# 앱 Dockerfile 일부
ADD https://raw.githubusercontent.com/eficode/wait-for/v2.2.4/wait-for /usr/local/bin/wait-for
RUN chmod +x /usr/local/bin/wait-for
CMD ["sh", "-c", "wait-for mysql:3306 -t 120 -- java -jar app.jar"]
  • 장점: 간단, 어디서나 동작
  • 단점: 포트 열림만 확인(인증/권한까지는 아님)

(B) dockerize

ADD https://github.com/jwilder/dockerize/releases/download/v0.7.0/dockerize-linux-amd64-v0.7.0.tar.gz /tmp/
RUN tar -C /usr/local/bin -xzf /tmp/dockerize-linux-amd64-v0.7.0.tar.gz
CMD ["sh","-c","dockerize -wait tcp://mysql:3306 -timeout 2m && java -jar app.jar"]
  • 장점: 템플릿/대기 등 기능 풍부
  • 단점: 바이너리 추가 필요

권장 조합: 로컬/Compose → healthcheck + depends_on. Swarm/쿠버/배포 → 대기 스크립트 + 앱 재시도 함께.

  1. 스키마가 없어서 앱 부팅 실패
    • /docker-entrypoint-initdb.d에 DDL/데이터 넣어 초기 스키마 보장
    • 또는 앱의 DB 마이그레이션 도구(Flyway/Liquibase) 사용
  2. 호스트에서 연결 안 됨
    • ports: "3306:3306" 확인
    • 맥/윈도WSL에서는 방화벽/포트 충돌 확인

6. 상태 확인 & 디버깅 명령 모음

# 컨테이너 상태/헬스
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"

# 특정 서비스 헬스 필드 확인
docker inspect --format='{{json .State.Health}}' mysql8 | jq .

# 로그 확인
docker logs -f mysql8
docker logs -f app

# 네트워크 점검
docker exec -it app ping -c1 mysql
docker exec -it app getent hosts mysql

7. 프로덕션 안정성 높이기 위한 팁

  • healthcheck를 과도하게 촘촘히 하지 않기: 2–10초 간격, retries 5–10 수준 권장(리소스 절약)
  • MySQL 준비 신호 강화: 단순 ping 대신 “특정 DB/테이블 존재” SQL을 테스트하는 스크립트로 커스텀 가능
  • 앱 레벨 Circuit Breaker/Retry: 일시적 DB 장애에 서비스 전체가 멈추지 않도록 보호
  • 관측성: 헬스 상태 변화, 재시작 횟수, 초기화 시간(DDL/리커버리)을 로그/메트릭에 기록
  • 데이터 초기화 충돌 방지: /docker-entrypoint-initdb.d와 애플리케이션 마이그레이션 도구의 역할 분리를 명확히
반응형