키워드
: 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_on + healthcheck !!
# 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 의 동작은
- $uri: 요청 경로에 해당하는 파일이 있으면 그걸 반환
- $uri/: 디렉토리가 있으면 그걸 반환
- /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 요청 기반의 부하테스트를 돌려서, "홈서버가 어느 지점에서 한계에 도달하는지" 데이터로 확인할 예정 !