Docker Compose로 백엔드+MySQL 함께 띄우며 로그인 장애 해결기

oversleep·2025년 9월 28일

증상: 프론트에서 Google 로그인 버튼 클릭 → 브라우저가 http://localhost:8080/oauth2/authorization/google로 이동했지만 ERR_CONNECTION_REFUSED.

원인: 백엔드 컨테이너가 MySQL에 연결 실패하여 부팅 직후 재시작 루프. 컨테이너 내부에서 DB_HOST=localhost로 설정되어 있어 자기 자신을 가리킴 → DB에 붙지 못하고 JDBCConnectionException: Communications link failure 발생.

해결: 방법 A) 데이터베이스도 컨테이너로 함께 띄우기(권장). Compose 네트워크의 DNS 이름(서비스명)으로 연결하고, DB가 정상 기동(healthy) 된 이후에 백엔드가 뜨도록 healthcheck + depends_on 구성.


1) 환경 개요

  • FE: Vite 개발 서버 (예: http://localhost:5173)
  • BE: Spring Boot (이미지 parkjihyeon/linkrew-server:latest)
  • DB: MySQL 8.0
  • OS/툴: macOS, Docker Desktop, Docker Compose v2

2) 증상 상세

  • 로그인 버튼 클릭 → http://localhost:8080/oauth2/authorization/google 접속 시도 → 브라우저 연결 거부.

  • docker ps 확인 시 백엔드 컨테이너 Restarting (...) 상태.

  • docker logs linkrew-server 핵심 로그:

    org.hibernate.exception.JDBCConnectionException: Unable to open JDBC Connection ...
    com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure
    java.net.ConnectException: Connection refused

    DB에 접속 자체가 안 됨.


3) 원인 분석

  1. 컨테이너 내부의 localhost 오해

    • 컨테이너 안에서 localhost해당 컨테이너 자신을 의미.
    • DB가 별도 컨테이너라면 DB_HOSTlocalhost가 아니라 Compose 서비스명(예: linkrew-database)이어야 함.
  2. DB 서비스가 Compose에 없음 또는 준비 전 접속 시도

    • 초기 구성엔 DB 서비스가 없거나, 있어도 백엔드가 DB 준비 전에 먼저 기동하여 실패.
    • 해결을 위해 healthcheck + depends_on: condition: service_healthy 필요.
  3. 볼륨 삭제 습관(down -v)

    • -v는 DB 데이터를 완전히 초기화. 매번 계정/스키마가 사라질 수 있음. (재현·테스트 용도 외엔 지양)

4) 최종 설정 (방법 A)

4.1 .env.local

핵심: DB_HOST=linkrew-database, DB_PORT=3306 (컨테이너 내부 포트 기준)으로 변경

# Spring
ENV_FILE=.env.local
SPRING_PROFILE=local
SPRING_PROFILES_ACTIVE=local
SPRING_PORT=8080

# Database (컨테이너-컨테이너 연결)
DB_HOST=linkrew-database
DB_PORT=3306
DB_DATABASE=linkrew_db
DB_USERNAME=linkrew-user
DB_PASSWORD=linkrew1234
DB_URL=jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_DATABASE}?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Seoul

# JWT
JWT_SECRET=...적절한_값...

# OAuth2 Redirects (백엔드가 8080에 노출된다는 가정)
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...
GOOGLE_REDIRECT_URI=http://localhost:8080/login/oauth2/code/google
KAKAO_CLIENT_ID=...
KAKAO_CLIENT_SECRET=...
KAKAO_REDIRECT_URI=http://localhost:8080/login/oauth2/code/kakao

# Front URL
FRONTEND_BASE_URL=http://localhost:5173

참고: 호스트(맥)에서 DB에 직접 붙고 싶다면 외부 포트를 예: 3307로 노출하고, 로컬 클라이언트는 localhost:3307로 접속하면 됨. 그러나 백엔드 컨테이너가 DB에 붙을 때는 항상 내부 포트(3306)와 서비스명을 사용.

4.2 docker-compose.yml

핵심: DB 서비스 추가, 헬스체크, 백엔드 depends_on: service_healthy, 컨테이너 이름 하드코딩 제거

version: '3.8'

services:
  linkrew-database:
    image: mysql:8.0
    environment:
      MYSQL_DATABASE: linkrew_db
      MYSQL_USER: linkrew-user
      MYSQL_PASSWORD: linkrew1234
      MYSQL_ROOT_PASSWORD: rootpass
    ports:
      - "3307:3306"   # 호스트에서 접속할 때만 사용(선택). 컨테이너 간 연결엔 3306 사용.
    volumes:
      - linkrew-mysql:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-prootpass"]
      interval: 5s
      timeout: 3s
      retries: 30

  linkrew-server:
    image: parkjihyeon/linkrew-server:latest
    env_file:
      - ${ENV_FILE}
    # 우변(컨테이너 내부 포트)은 보통 8080 (이미지에 따라 다르면 로그로 확인)
    ports:
      - "${SPRING_PORT}:8080"
    restart: always
    depends_on:
      linkrew-database:
        condition: service_healthy

volumes:
  linkrew-mysql:

  • container_name:는 가급적 지양. 폴더·프로젝트별 자동 네이밍이 충돌을 줄여준다.
  • 내부 포트가 8080이 아닐 수 있음. 백엔드 로그에서 Tomcat started on port(s) 줄을 확인해 우변 숫자를 맞춰라.

5) 실행·검증 절차

# 0) 기존 리소스 정리 (데이터 보존을 원하면 -v 쓰지 않기)
docker compose down --remove-orphans

# 1) 재기동
docker compose --env-file .env.local up -d --build

# 2) 상태 확인
docker compose ps

# 3) 로그 확인 (DB → healthy, BE → 에러 없이 기동)
docker logs -n 200 linkrew-database
docker logs -n 200 linkrew-server

# 4) 헬스체크/엔드포인트 확인
curl -i http://localhost:8080/actuator/health

정상이면:

  • linkrew-databasehealthy.
  • linkrew-serverRestarting 아님, 0.0.0.0:8080->8080/tcp 바인딩.
  • 브라우저에서 http://localhost:8080/oauth2/authorization/google 접근 시 더 이상 ECONNREFUSED 없음.

6) 왜 이렇게 해야 하나 (모형 이해)

  • 컨테이너 네트워크: 같은 Compose 네트워크의 서비스끼리는 서비스명으로 DNS가 자동 구성됨. linkrew-serverlinkrew-database:3306으로 접속.

  • 포트 매핑: 호스트:컨테이너 형태.

    • 호스트에서 DB 접속을 위해 3307:3306으로 노출했더라도, 컨테이너-컨테이너 간에는 항상 컨테이너 포트(3306) 를 사용.
  • 기동 순서: DB 준비 없이 BE가 먼저 뜨면 접속 실패. healthcheck + depends_on(healthy)로 완화.


7) 흔한 함정 & 빠른 점검표

  • .env에서 DB_HOST=localhost로 남아있다 → 서비스명으로 바꿔라.
  • DB 외부 포트를 3307로 노출했는데, BE가 그 포트로 붙는다 → 잘못. BE는 3306으로.
  • container_name 하드코딩 → 폴더/프로젝트 간 이름 충돌. 지우는 게 안전.
  • 매번 down -v데이터 날림. 특별한 이유 없으면 쓰지 말기.
  • ports: "${SPRING_PORT}:${SPRING_PORT}" → 내부 포트가 8080이 아닐 수 있다. 보통 ${SPRING_PORT}:8080 이 안전.
  • ERR_CONNECTION_REFUSED 나오면 우선 docker ps, docker logs에서 포트 바인딩/재시작 여부 확인.

8) 트러블슈팅 부록

  • DB가 healthy가 안 됨

    • 로그 확인: docker logs linkrew-database
    • 비밀번호/환경변수 불일치 확인, healthcheck 재시도 수(retries) 증가.
  • 포트 충돌

    • lsof -i :8080 으로 사용 프로세스 확인. 다른 앱이 쓰면 호스트 포트 변경.
  • 로컬 DB로 붙고 싶다

    • 방법 B: DB_HOST=host.docker.internal, DB_PORT=로컬포트. (이번 해결에는 방법 A 사용)
  • 초기 데이터/계정 필요

    • down -v 후엔 데이터 소멸. 필요한 경우 init SQL 또는 앱 시드 로직 적용.

9) 마무리

이번 이슈는 컨테이너 네트워킹 모델기동 시점 문제였다. DB를 Compose에 포함시키고, 서비스명으로 붙이며, healthcheck로 준비 완료를 보장하자. 이렇게 정리하면 로그인(OAuth 포함) 플로우가 안정적으로 복구된다.

필요하면 위 설정을 기반으로 프로파일(로컬/스테이징/프로덕션) 분리, 시드 스크립트 자동화, 모니터링(Loki/Grafana) 까지 확장하면 된다. 다음은 재발 방지용 원클릭 스크립트 예시.

#!/usr/bin/env bash
set -euo pipefail

# 0) 안전 정리 (데이터 유지)
docker compose down --remove-orphans || true

# 1) 재기동
docker compose --env-file .env.local up -d --build

# 2) 상태 체크
docker compose ps
sleep 2
curl -sS http://localhost:8080/actuator/health || echo "health endpoint not ready yet"

여기까지 적용하면, 로그인 버튼 → 정상 동작. 끝.

profile
궁금한 것, 했던 것, 시행착오 그리고 기억하고 싶은 것들을 기록합니다.

0개의 댓글