"로그인이 안 돼요" — 이 한 마디가 4시간의 디버깅 여정을 시작시켰다
🎯 미션: 운영 서버에 프론트엔드 포함 전체 배포
EcoNiPass 탄소관리 플랫폼을 운영 서버에 처음 배포하는 날이었다. 백엔드(FastAPI), 프론트엔드(React), Keycloak 인증서버까지 풀셋.
계획은 심플했다:
로컬에서 빌드 → GHCR에 푸시 → 서버에서 Pull → docker-compose up
"30분이면 되겠지?" — 이 생각이 모든 삽질의 시작이었다.
🧱 1차 벽: ARM64에서 amd64 이미지를 빌드하라고?
Mac M2(ARM64)에서 운영 서버(amd64)용 Docker 이미지를 빌드해야 한다. docker buildx로 크로스 플랫폼 빌드를 하면 되지.
docker buildx build --platform linux/amd64 -f Dockerfile.frontend --push .
결과:
env: can't execute 'node': Exec format error
QEMU 에뮬레이션으로 ARM64 위에서 amd64 바이너리를 돌리는 건데, npm의 style-dictionary 패키지가 네이티브 node를 직접 호출하면서 터진 것.
💡 해결: "빌드는 로컬에서, 이미지는 복사만"
# Dockerfile.frontend.prebuilt
FROM nginx:alpine
COPY deploy/nginx/frontend.conf /etc/nginx/conf.d/default.conf
COPY econipass-frontend-develop/build /usr/share/nginx/html
EXPOSE 80
로컬 Mac에서 npm run build로 빌드 → 결과물만 nginx 이미지에 복사. QEMU가 node를 돌릴 필요가 없다.
인사이트: 크로스 플랫폼 빌드에서 네이티브 바이너리 문제가 나면, "빌드 분리" 패턴을 생각하자. 빌드는 호스트에서, 이미지는 결과물만.
🧱 2차 벽: 로그인하면 500 에러 (feat. Keycloak URL)
서버에 배포 완료. 프론트엔드 화면 잘 뜬다. 로그인 버튼을 누르면...
POST /v1/auth/login → 500 Internal Server Error
"시스템 에러가 발생했습니다" 😱
디버깅 과정
# 컨테이너 로그 확인
docker logs econipass-backend --tail 100
# 발견: user_id = '' (빈 문자열)
[cached since 186s ago] ('',)
user_repository.py L.1525 No rows found.
user_id가 빈 문자열? JWT 토큰 디코딩이 실패하는 건가?
그런데 .env 파일을 확인하면:
KEYCLOAK_BASE_URL=http://econipass-keycloak:8080 ← 올바른 값
컨테이너 안에서 확인하면:
$ docker exec econipass-backend env | grep KEYCLOAK_BASE_URL
KEYCLOAK_BASE_URL=http://localhost:8080 ← ?!?!?!
.env 파일은 맞는데, 컨테이너 환경변수가 다르다!
원인
.env 파일이 컨테이너가 시작된 후에 수정되었다.
컨테이너 생성: 15:38:19 (이때 .env 로딩)
.env 수정: 15:39:00 (이미 늦었다...)
Docker Compose의 env_file은 컨테이너 생성 시점에만 읽는다. 런타임에 파일을 수정해도 반영 안 됨.
💡 해결
docker compose -f docker-compose.prod.yml up -d --force-recreate backend
--force-recreate로 컨테이너를 완전히 재생성하면 새로운 .env를 읽는다.
인사이트:
.env수정 후에는 반드시--force-recreate. 단순restart로는 환경변수가 갱신되지 않는다.
🧱 3차 벽: 또 500 에러 (feat. Redis 인증)
Keycloak URL 문제를 고쳤다. 다시 로그인을 시도하면...
POST /v1/auth/login → 500 Internal Server Error
"또?!" 🤦
이번에는 로그가 다르다:
CRITICAL main.py L.543 -> Error: Authentication required.
"Authentication required"... Keycloak 인증 문제인가? 아니, Keycloak 로그인은 200 OK로 성공하고 있다:
HTTP Request: POST http://econipass-keycloak:8080/realms/econipass/... "HTTP/1.1 200 OK"
그렇다면 대체 뭐가 "Authentication required"를 던지는 거지?
범인: Redis
# 테스트
$ docker exec econipass-backend python -c "
import redis
r = redis.Redis(host='172.18.0.1', port=6379)
r.ping()
"
# 결과
redis.exceptions.AuthenticationError: Authentication required.
Redis에 비밀번호가 걸려 있었다! 기존 서버에서 운영 중인 Redis(querypie-redis-1)가 --requirepass Querypie1!로 설정되어 있었는데, 백엔드의 RedisAdapter는 비밀번호를 지원하지 않았다.
💡 해결
# config.py
KVS_PASSWORD: str = os.getenv("KVS_PASSWORD", "")
# redis_adapter.py
self.client = redis.Redis(
host=redis_host,
port=redis_port,
password=redis_password or None, # 빈 문자열이면 None
decode_responses=True,
)
개발 환경(비밀번호 없음)과 운영 환경(비밀번호 있음)을 모두 지원하도록 os.getenv에 기본값 빈 문자열 폴백.
인사이트: "Authentication required"가 어디서 오는지 모를 때, Keycloak이 아니라 Redis일 수 있다. 에러 메시지만 보고 범인을 단정하지 말 것.
🎉 결과: 드디어 로그인 성공
$ curl -X POST http://localhost:8000/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"login_id": "econipass_support@wingarc.com", "password": "password123"}'
# HTTP 200 + access_token + id_token 🎊
3번의 500 에러를 뚫고 드디어 로그인이 된다.
📝 오늘의 핵심 교훈 5가지
1. 크로스 플랫폼 빌드: "빌드 분리" 패턴
ARM64에서 amd64 이미지를 빌드할 때 네이티브 바이너리 문제가 나면, 호스트에서 빌드하고 결과물만 이미지에 복사하자. QEMU에 의존하지 마라.
2. .env 수정 ≠ 컨테이너 반영
Docker Compose의 env_file은 컨테이너 생성 시점에만 읽힌다. .env 수정 후에는 반드시 --force-recreate.
3. "Authentication required"의 진짜 범인
같은 에러 메시지라도 원인이 다를 수 있다. Keycloak인지 Redis인지 DB인지, 스택 트레이스를 끝까지 읽어라.
4. GitHub Actions 없이 배포하기
소규모 팀이라면 CI/CD 파이프라인 없이 deploy.sh 하나로 충분하다:
로컬 테스트 → docker buildx → GHCR 푸시 → SSH로 pull & restart
복잡한 인프라보다 실행 가능한 단순함이 낫다.
5. 외부 서비스 공유 시 네트워크 설정
기존에 운영 중인 DB/Redis를 새 서비스에서 쓰려면 Docker의 외부 네트워크를 활용하자:
networks:
external_services:
external: true
name: pgvector_default # 기존 네트워크 이름
같은 Docker 네트워크에 있으면 컨테이너 이름으로 통신 가능.
🔗 배포 아키텍처 최종본
[Mac M2] [GHCR] [운영 VM]
│ │ │
├─ npm build (로컬) │ │
├─ docker buildx ───────▶│ │
│ --platform amd64 │◀── docker pull ───┤
│ │ │
└─ SSH rsync (.env) ────┼──────────────────▶│
│ docker-compose up
│ │
│ ┌──────────────┤
│ │ :8000 백엔드 │
│ │ :3000 프론트 │
│ │ :8080 Keycloak│
│ └──────────────┘
마무리
운영 배포는 항상 **"이것만 하면 되지"**로 시작해서 **"대체 왜 안 되는 거야"**로 끝난다.
오늘의 3연타 500 에러도 마찬가지였다. 하지만 각각의 에러를 풀어가면서 Docker 네트워크, 환경변수 라이프사이클, Redis 인증 메커니즘을 제대로 이해하게 됐다.
삽질이 곧 학습이다. 다만, 같은 삽질을 두 번 하지 않으려면 기록하자. 이 블로그처럼. ✍️
다음 목표: SSL 인증서 설정 + MCP 서버 실전 테스트