☁️ 🏗️

클라우드/인프라 엔지니어가 되는 과정을 기록합니다.

홈서버 구축 일지

[8] "한 줄이면 뜰 줄 알았지...🤦‍♂️" - 5가지 배포 트러블슈팅

d4eh0 2026. 4. 23. 16:08
키워드
 : Docker Network, SPA, CORS, localhost, DNS

 


[7]편에서 Dockerfile, docker-compose.yml, nginx.conf를 모두 작성했다.

이제 docker compose up --build 한 줄이면 뜰 것 같았는데...!

 

여러 에러들이 발생했다...

이번 편은 실제 배포 과정에서 맞닥뜨린 에러 5개를 기록한다. 각각 왜 터졌는지 → 어떻게 고쳤는지 순서로 정리한다.


📋 현재 상황

 

배포 완료 후 트래픽 흐름은 아래와 같다.

외부 브라우저
    ↓ HTTP :80
공유기 (포트포워딩 80 → 갤북:80)
    ↓
Galaxy Book (Ubuntu)
└── Docker Compose (app-network)
    ├── nginx:alpine          ← :80 외부 노출
    │   ├── /        → frontend:80
    │   └── /api     → backend:8080
    ├── frontend (React + nginx, 내부만)
    ├── backend (Spring Boot, 내부만)
    └── mysql:8 (내부만)

 

  • 컨테이너끼리는 Docker 내부 네트워크(app-network)로 통신하고,
  • 외부에 열린 포트는 nginx의 :80 하나뿐이다.

🔹 트러블슈팅 (1) :: 80포트가 이미 사용 중

증상

$ docker compose up --build
...
Error response from daemon: driver failed programming external connectivity:
Bind for 0.0.0.0:80 failed: port is already allocated

 

원인 → 해결

  • Ubuntu 설치 시 기본으로 깔린 시스템 nginx(host nginx) 가 이미 80포트를 점유하고 있었다.
  • Docker의 nginx 컨테이너도 80포트를 쓰려 하니 충돌이 난 것이다.
$ sudo ss -tlnp | grep :80
LISTEN  0  511  0.0.0.0:80  ...  users:(("nginx",pid=...,fd=...))

 

시스템 nginx를 멈추고, 부팅 시 자동 시작도 꺼준다.

sudo systemctl stop nginx
sudo systemctl disable nginx

 

이후 docker compose up --build 재실행하면 포트 충돌 없이 올라온다.


🔹 트러블슈팅 (2) :: nginx "host not found in upstream frontend"

증상

nginx: [emerg] host not found in upstream "frontend" in /etc/nginx/nginx.conf:10

 

  • ?? 뭐지. nginx 컨테이너가 뜨자마자 죽는다.

원인

nginx.conf 안에 이런 설정이 있다.

upstream frontend {
    server frontend:80;
}
  • nginx는 시작할 때 upstream에 적힌 호스트명(frontend)을 DNS로 조회한다.
  • 그런데 frontend 컨테이너가 아직 완전히 준비되지 않은 상태라면,
  • Docker 내부 DNS가 해당 컨테이너를 아직 등록하지 않아 조회 실패 → nginx 즉시 에러남.

해결

  • depends_onhealthcheck !!
# docker-compose.yml

frontend:
  build: ./frontend
  networks:
    - app-network
  healthcheck:
    test: ["CMD", "curl", "-f", "http://localhost:80"] # 200 OK 나오면 'healthy'한 상태
    interval: 5s
    timeout: 3s
    retries: 5

nginx:
  image: nginx:alpine
  depends_on:
    frontend:
      condition: service_healthy   # frontend가 'healthy' 상태여야 실행
    backend:
      condition: service_started
  ports:
    - "80:80"
  restart: on-failure
  networks:
    - app-network

 

(1) depends_on: - frontend

  • frontend 컨테이너가 시작되면 nginx도 시작
  • 하지만 시작 순서만 정할뿐, frontend가 실제로 요청을 받을 준비가 됐는지는 보장xx

(2) frontend 컨테이너에 healthcheck 추가

  • fronted 컨테이너가 5초마다 헬스체크 진행,
  • nginx는 이 service_healthy 상태를 확인하면 시작

🔹 트러블슈팅 (3) :: React SPA에서 새로고침 시 404

증상

 

/login, /attendance 같은 경로에 접속하면 nginx가 404를 반환한다.

 

원인

 

React는 SPA(Single Page Application) 다.

실제 파일은 index.html 하나뿐이고, /attendance 같은 경로는 JavaScript가 브라우저 안에서 처리하는 가상 경로.

nginx는 /attendance라는 실제 파일을 찾으려 하지만 존재하지 않으니 404를 리턴했던것

 

해결

 

frontend 컨테이너 내부의 nginx 설정(frontend/nginx.conf)에 try_files 추가 !

# frontend/nginx.conf

server {
    listen 80;

    location / {
        root   /usr/share/nginx/html;
        index  index.html;
        try_files $uri $uri/ /index.html;  # 이거
    }
}

 

try_files $uri $uri/ /index.html 의 동작은 

  1. $uri: 요청 경로에 해당하는 파일이 있으면 그걸 반환
  2. $uri/: 디렉토리가 있으면 그걸 반환
  3. /index.html: 둘 다 없으면 index.html을 반환 → React Router가 처리

SPA 배포에서 nginx 설정에 try_files를 빠뜨리면 새로고침이나 직접 URL 접속 시 404가 뜬다.
Next.js, Vue Router, React Router 모두 마찬가지


🔹 트러블슈팅 (4) :: API 요청 CORS 403

증상

브라우저 콘솔에

Access to fetch at 'http://...' from origin 'http://...' has been blocked by CORS policy

 

원인

 

Spring Boot의 CORS 설정에서 허용 Origin이 특정 주소로 고정되어 있었다.

// 기존 (문제)
.allowedOrigins("http://localhost:3000")

 

nginx 리버스 프록시를 통해 들어오면 Origin 헤더가 다를 수 있고,

홈서버의 공인 IP나 로컬 IP로 접속할 경우 허용 목록에 없어 CORS 차단이 발생했다.

 

해결

 

allowedOriginPatterns("*")로 변경한다.

// SecurityConfig.java (혹은 CorsConfig.java)

configuration.setAllowedOriginPatterns(List.of("*"));
configuration.setAllowCredentials(true);

allowedOrigins("*")는 allowCredentials(true)와 함께 쓸 수 없음. (Spring Security 제약).
→ 와일드카드를 쓰면서 쿠키 인증도 허용하려면 반드시 allowedOriginPatterns("*")를 써야 한다.

운영 환경에서는 "*" 대신 실제 도메인을 명시하는 것이 보안상 올바르다.
이번 홈서버 환경은 내부 테스트 목적이므로 편의상 전체 허용으로 설정했다.
AWS 배포 시에는 CloudFront 도메인 등 실제 주소로 좁혀줄 예정이다.


🔹 트러블슈팅 (5) :: API URL localhost:8080 하드코딩

증상

 

프론트엔드에서 API 요청이 http://localhost:8080/api/...로 나가고 있었다.

이 주소는 브라우저가 실행되는 클라이언트 기준 localhost인데,

홈서버에 접속한 외부 사용자 입장에서 localhost는 자기 컴퓨터를 가리킨다.

→ 당연히 요청이 실패 !!

 

원인

 

React 코드에 API Base URL이 직접 박혀 있었다.

// 기존 (문제)
const response = await fetch("http://localhost:8080/api/attendance");

 

해결

 

환경변수(VITE_API_BASE_URL)로 추출하고,

nginx가 /api 경로를 backend로 프록시하도록 구성해줌

 

1단계: React 코드 수정

// .env
VITE_API_BASE_URL=/api

// 코드
const BASE = import.meta.env.VITE_API_BASE_URL;
const response = await fetch(`${BASE}/attendance`);

 

2단계: nginx 리버스 프록시 설정 확인

# nginx/nginx.conf

location /api/ {
    proxy_pass http://backend:8080/api/;
}

 

이렇게 하면 브라우저는 같은 Origin(/api/...)으로 요청을 보내고, nginx가 내부적으로 backend 컨테이너로 전달한다.

브라우저 → GET /api/attendance
    ↓ (같은 호스트, CORS 없음)
nginx
    ↓ proxy_pass
backend:8080/api/attendance

리버스 프록시 구조에서는 API URL을 절대 경로(localhost:8080)로 하드코딩하면 안 됨.
환경변수로 분리하고, nginx가 라우팅을 담당하게 하자.


트러블슈팅 요약

1 80포트 already allocated 호스트 nginx가 포트 점유 systemctl stop/disable nginx
2 host not found in upstream nginx 시작 시 frontend DNS 미등록 healthcheck + condition: service_healthy
3 새로고침 시 404 SPA 경로를 실제 파일로 찾음 try_files $uri /index.html
4 CORS 403 Spring CORS Origin 불일치 allowedOriginPatterns("*")
5 API 요청 실패 localhost:8080 하드코딩 VITE_API_BASE_URL=/api 환경변수화

다음 편 예고

 

내가 만든 CloudAttend 애플리케이션이 이제 홈서버에서 잘 동작한다 !
다음 편에서는 Prometheus + Grafana를 Docker Compose에 추가해서

CPU, 메모리, JVM 메트릭을 실시간으로 모니터링하는 환경을 구성하려고 한다.

 

그리고 k6로 실제 외부 HTTP 요청 기반의 부하테스트를 돌려서, "홈서버가 어느 지점에서 한계에 도달하는지" 데이터로 확인할 예정 !