Nginx + WAS 풀스택 배포와 반복 배포 워크플로우
개발 서버에서는 잘 동작하던 API가 배포 직후 Nginx 502를 반환하고, 핫픽스를 올려도 반영이 늦습니다. 문제는 코드 자체보다 Web/WAS 역할 분리와 배포 루프가 팀 내에서 표준화되지 않은 데 있습니다. Nginx 앞단과 WAS 뒷단을 Compose로 정확히 묶고, 수정→빌드→검증→롤백 흐름을 고정하면 장애 대응 속도가 달라집니다. 이 모듈은 그 운영 루프를 실습 단위로 체득하게 만듭니다.
웹 서비스는 보통 두 개의 레이어로 구성됩니다. **정적 파일과 리버스 프록시를 담당하는 Nginx(Web Server)**와 **비즈니스 로직을 처리하는 WAS(Web Application Server)**입니다. Docker Compose로 이 두 컨테이너를 함께 구성하고, 코드를 수정한 뒤 재빌드하여 반영하는 전체 워크플로우를 실습합니다.
실제 서비스 구조(Nginx + WAS)를 Docker Compose로 로컬에 재현하고, 개발 중 코드 수정을 즉시 반영하는 방법과 프로덕션 배포 시 이미지를 재빌드해 반영하는 방법을 모두 익힙니다.
- 1Nginx 리버스 프록시의 proxy_pass로 WAS에 요청을 전달하는 구조를 구성할 수 있다
- 2Docker Compose로 Nginx와 WAS 두 서비스를 연결할 수 있다
- 3개발 환경에서 bind mount와 hot reload로 재빌드 없이 코드를 반영할 수 있다
- 4프로덕션 환경에서 docker compose up --build로 변경사항을 빌드·배포할 수 있다
- 5수정 → 빌드 → 확인 반복 사이클을 빠르게 돌리는 실전 패턴을 적용할 수 있다
이 챕터는 로컬 파일 편집과 Docker Compose를 반복해서 사용합니다. 실습 디렉토리를 미리 만들고 시작하세요.
docker compose versionmkdir -p ~/web-was-lab && cd ~/web-was-labdocker pull node:20-alpinedocker pull nginx:alpineNginx + WAS 아키텍처 — 왜 두 개로 나누는가
확대
리버스 프록시 패턴의 역할 분리
단일 WAS(예: Node.js Express)만으로도 HTTP 서버를 만들 수 있습니다. 그런데 실무에서는 왜 Nginx를 앞에 두는 걸까요?
확대
Nginx가 처리하는 것:
- 정적 파일 서빙 (HTML, CSS, JS, 이미지) — WAS보다 10~100배 빠름
- SSL/TLS 종단 처리 — WAS는 HTTP만 처리
- 요청 버퍼링 — 느린 클라이언트로부터 WAS 보호
- 여러 WAS 인스턴스로 로드 밸런싱
WAS가 처리하는 것:
- API 요청 처리 (
/api/*) - 데이터베이스 연결과 쿼리
- 인증/인가 로직
- 비즈니스 규칙
Docker Compose에서의 연결 방식
두 서비스는 같은 Compose 네트워크 안에서 서비스 이름으로 서로를 찾습니다.
# docker-compose.yml
services:
nginx:
image: nginx:alpine
ports:
- "80:80" # 외부에서 80포트로 접근
depends_on:
- app
app:
build: ./app
# ports를 외부에 노출하지 않음 — nginx를 통해서만 접근 가능
# nginx.conf
server {
listen 80;
# /api 로 시작하는 요청은 WAS로 전달
location /api {
proxy_pass http://app:3000; # 서비스 이름 'app' 사용
}
# 나머지는 정적 파일 서빙
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
}
proxy_pass http://app:3000에서 app은 docker-compose.yml에 정의한 서비스 이름입니다. Docker의 내장 DNS가 app을 해당 컨테이너 IP로 자동 해석합니다.
프로젝트 구조 설계
Nginx와 WAS를 연동할 때 가장 먼저 막히는 것은 "파일을 어디에 두어야 하는가"입니다. nginx.conf를 컨테이너에 어떻게 전달하는지, app 코드는 이미지에 넣는지 볼륨으로 마운트하는지, docker-compose.yml은 어디서 nginx를 참조하는지—이 구조를 처음부터 일관되게 잡지 않으면 나중에 파일 경로 오류와 mount 충돌로 디버깅이 복잡해집니다. 이 ConceptBlock에서는 Nginx + WAS 실습 프로젝트의 디렉토리 레이아웃과 각 파일의 역할을 다룹니다.
확대
디렉토리 레이아웃
실습에서 사용할 프로젝트 구조입니다.
web-was-lab/ 디렉토리 구조입니다.
docker-compose.yml— 전체 서비스 구성docker-compose.dev.yml— 개발 환경 overridenginx/—Dockerfile(또는 config만 mount),nginx.conf(리버스 프록시 설정)app/—Dockerfile(WAS 이미지 빌드),package.json,src/index.js(애플리케이션 코드)
WAS 코드 (Node.js Express)
// app/src/index.js
const express = require('express')
const app = express()
const PORT = process.env.PORT || 3000
app.use(express.json())
// 헬스체크 엔드포인트
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', version: process.env.APP_VERSION || '1.0.0' })
})
// 메인 API
app.get('/api/hello', (req, res) => {
res.json({ message: '안녕하세요, Docker WAS입니다!' })
})
app.listen(PORT, () => {
console.log(`WAS 서버 시작: 포트 ${PORT}`)
})
// app/package.json
{
"name": "was-app",
"version": "1.0.0",
"scripts": {
"start": "node src/index.js",
"dev": "nodemon src/index.js"
},
"dependencies": {
"express": "^4.18.2"
},
"devDependencies": {
"nodemon": "^3.0.2"
}
}
WAS Dockerfile
# app/Dockerfile
FROM node:20-alpine
WORKDIR /app
# 의존성 먼저 복사 (레이어 캐시 최적화)
COPY package*.json ./
RUN npm ci --only=production
# 소스 코드 복사
COPY src/ ./src/
ENV PORT=3000
EXPOSE 3000
CMD ["node", "src/index.js"]
Nginx 설정
# nginx/nginx.conf
upstream was_backend {
server app:3000;
}
server {
listen 80;
server_name localhost;
# 액세스 로그 포맷
access_log /var/log/nginx/access.log;
# 정적 파일 서빙
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
# API 요청을 WAS로 전달
location /api/ {
proxy_pass http://was_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_connect_timeout 10s;
proxy_read_timeout 30s;
}
}
개발 환경 vs 프로덕션 환경 구성
개발 중에는 코드를 수정할 때마다 이미지를 다시 빌드하면 너무 느립니다. 반대로 프로덕션에서 소스 코드를 bind mount로 제공하면 서버 파일 시스템에 직접 노출되는 보안 문제가 생깁니다. 두 환경의 요구가 다르기 때문에 Compose는 base 파일과 override 파일을 분리하는 패턴을 제공합니다. "개발할 때 잘 됐는데 서버에 올리면 안 돼요"라는 상황의 절반은 개발/프로덕션 설정이 분리되지 않아서 생깁니다. 이 ConceptBlock에서는 docker-compose.yml과 docker-compose.dev.yml을 분리해 환경별로 다른 설정을 적용하는 방법을 다룹니다.
확대
기본 docker-compose.yml (프로덕션 기준)
# docker-compose.yml
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./nginx/html:/usr/share/nginx/html:ro
depends_on:
app:
condition: service_healthy
restart: unless-stopped
app:
build:
context: ./app
dockerfile: Dockerfile
environment:
- NODE_ENV=production
- PORT=3000
- APP_VERSION=1.0.0
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/api/health"]
interval: 10s
timeout: 5s
retries: 3
start_period: 10s
restart: unless-stopped
networks:
default:
name: web-was-network
개발 환경 override 파일
# docker-compose.dev.yml
# 사용법: docker compose -f docker-compose.yml -f docker-compose.dev.yml up
services:
app:
build:
target: development # 멀티 스테이지 dev 스테이지 사용 시
volumes:
# 소스 코드를 bind mount — 파일 변경이 즉시 반영됨
- ./app/src:/app/src
environment:
- NODE_ENV=development
- APP_VERSION=dev
command: npm run dev # nodemon으로 hot reload
ports:
- "3000:3000" # 개발 중 직접 접근 허용
개발 환경에서는 소스 코드 디렉토리를 bind mount하기 때문에, 호스트에서 파일을 수정하면 컨테이너 내부에 즉시 반영됩니다. nodemon이 파일 변경을 감지해 서버를 자동 재시작합니다.
docker compose up 하면 2계층이 어떤 순서로 뜨나 — 빌드부터 첫 요청까지
docker compose up --build를 치면 잠시 뒤 브라우저에서 페이지가 뜨지만, 그 사이 Web(Nginx)과 WAS는 정해진 순서로 기동됩니다. 이 순서를 모르면 "배포 직후 502"가 났을 때 Nginx 문제인지, WAS가 아직 안 뜬 건지, 이름을 못 찾는 건지 구분하지 못합니다. Web-WAS 2계층 배포는 이미지 빌드 → 컨테이너 기동 → WAS 리슨 → 헬스체크 통과 → Nginx 프록시 연결 → 첫 요청 라우팅의 결과입니다.
[명령] docker compose up --build
│
① 이미지 빌드 — WAS Dockerfile로 앱 이미지 생성 (Nginx는 보통 공식 이미지 그대로)
│
② 네트워크·컨테이너 기동 — 사용자 정의 네트워크 생성 후 컨테이너 start
│
③ WAS 리슨 — 앱이 부팅되어 3000 포트를 LISTEN (기동에 수 초~수십 초)
│
④ 헬스체크 통과 — WAS가 healthy가 될 때까지 대기 (depends_on: service_healthy)
│ → Nginx는 이 신호를 기다렸다 뜬다
│
⑤ 프록시 연결 — Nginx가 proxy_pass의 app:3000을 DNS로 풀어 연결
│
⑥ 첫 요청 라우팅 — location /는 정적 파일, location /api는 WAS로 분기
▼
[결과] 브라우저 200 — 정적은 Nginx가, 동적은 WAS가 응답
각 단계에서 무슨 일이 일어나고, 막히면 어떤 증상인가:
| 단계 | 하는 일 | 여기서 막히면 |
|---|---|---|
| ① 빌드 | WAS Dockerfile로 이미지 생성. --build 없이 올리면 옛 이미지 재사용 | 코드 고쳤는데 반영 안 됨 → --build 빠뜨린 것 / 빌드 실패면 컨테이너가 아예 안 뜸 |
| ② 기동 | Compose가 프로젝트 네트워크를 만들고 두 컨테이너를 그 위에 올린다 | 같은 사용자 정의 네트워크가 아니면 서비스 이름 해석 불가(host not found in upstream) |
| ③ WAS 리슨 | 앱이 부팅되어 3000을 LISTEN. PORT·바인드 주소가 맞아야 함 | 앱이 127.0.0.1에만 바인드하면 다른 컨테이너에서 못 붙음 → 0.0.0.0 바인드 필요 |
| ④ 헬스체크 | healthcheck가 통과해야 Nginx가 service_healthy로 기동 | WAS가 아직 준비 전인데 Nginx가 먼저 요청 받으면 502 Bad Gateway |
| ⑤ 프록시 연결 | Nginx가 proxy_pass의 서비스 이름을 임베디드 DNS로 IP 해석해 연결 | proxy_pass 이름이 compose 서비스 이름과 다르면 502, localhost로 쓰면 자기 자신을 찾다 실패 |
| ⑥ 라우팅 | location /는 정적, location /api는 WAS로 분기 | WAS는 살아있는데 응답이 느리면 504 Gateway Timeout(502와 원인 다름) |
즉 **"배포했는데 502"는 대부분 ④(WAS가 아직 안 떴는데 Nginx가 먼저 받음) 또는 ⑤(이름 불일치·localhost 오사용)**에서 갈립니다 — 앱 코드 문제가 아닐 때가 많습니다. curl -I로 502(연결 실패)와 504(응답 지연)를 먼저 가르고, docker compose exec nginx wget -qO- http://app:3000/api/health로 Nginx→WAS 경로가 실제로 열려 있는지 확인하면 어느 단계에서 끊겼는지 좁혀집니다. depends_on: service_healthy가 ④를, 정확한 서비스 이름이 ⑤를 보장합니다.
기본 실습
실습 디렉토리 구조를 만들고 위의 파일들을 각 경로에 생성합니다.
실습 전 디렉토리와 예제 파일을 먼저 준비합니다.
# 실습 디렉토리 준비
mkdir -p /tmp/docker/part5/exam_18 && cd /tmp/docker/part5/exam_18
# 실습 디렉토리 준비
mkdir -p /tmp/docker/part3/exam_2 && cd /tmp/docker/part3/exam_2
# Nginx + WAS 연동 실습용 기본 구조 생성
mkdir -p nginx/html app/src
cat > nginx/nginx.conf << 'EOF'
upstream was {
server app:3000;
}
server {
listen 80;
location / {
root /usr/share/nginx/html;
}
location /api/ {
proxy_pass http://was/;
}
}
EOF
이제 실습을 진행합니다.
cd ~/web-was-lab
# nginx/html/index.html 생성 (간단한 정적 페이지)
cat > nginx/html/index.html << 'EOF'
<!DOCTYPE html>
<html>
<head><title>Docker Web+WAS 실습</title></head>
<body>
<h1>Nginx 정적 파일 서빙</h1>
<button onclick="fetch('/api/hello').then(r=>r.json()).then(d=>alert(d.message))">
WAS API 호출
</button>
</body>
</html>
EOF
mkdir -p ~/web-was-lab/{nginx/html,app/src}--build 플래그로 app 이미지를 빌드하고 전체 스택을 실행합니다.
docker compose up --build -d
# 실행 상태 확인
docker compose ps
# 예상 출력:
# NAME SERVICE STATUS PORTS
# web-was-lab-app-1 app running 3000/tcp
# web-was-lab-nginx-1 nginx running 0.0.0.0:80->80/tcp
# 헬스체크 상태 확인 (healthy가 될 때까지 대기)
docker compose ps --format "table {{.Service}}\t{{.Status}}"
브라우저에서 http://localhost 접속 — Nginx가 정적 파일을 서빙합니다.
/api/hello 호출 — Nginx가 WAS로 요청을 전달합니다.
docker compose up --build -dapp/src/index.js를 수정하고 WAS 서비스만 재빌드해서 반영합니다.
# 1. 코드 수정: 응답 메시지 변경
cat > app/src/index.js << 'EOF'
const express = require('express')
const app = express()
const PORT = process.env.PORT || 3000
app.use(express.json())
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', version: process.env.APP_VERSION || '1.0.0' })
})
app.get('/api/hello', (req, res) => {
// 메시지 수정!
res.json({ message: '수정된 버전입니다!', timestamp: new Date().toISOString() })
})
// 새 엔드포인트 추가
app.get('/api/items', (req, res) => {
res.json({ items: ['사과', '바나나', '체리'] })
})
app.listen(PORT, () => console.log(`포트 ${PORT} 실행 중`))
EOF
# 2. app 서비스만 재빌드 + 재시작 (nginx는 그대로)
docker compose up --build -d app
# 3. 빌드 과정 로그 확인
docker compose logs app --tail=20
# 4. 변경 확인
curl http://localhost/api/hello
# {"message":"수정된 버전입니다!","timestamp":"..."}
curl http://localhost/api/items
# {"items":["사과","바나나","체리"]}
docker compose up --build -d appbind mount + nodemon으로 재빌드 없이 코드 변경을 즉시 반영합니다.
# 개발 환경 실행 (override 파일 추가)
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
# nodemon이 시작되는 로그 확인
docker compose logs app -f
# 예상 출력:
# [nodemon] 3.0.2
# [nodemon] watching path(s): *.*
# [nodemon] watching extensions: js,mjs,cjs,json
# [nodemon] starting `node src/index.js`
# 포트 3000 실행 중
# 이제 호스트에서 파일을 수정하면 즉시 반영됨
echo "// 주석 추가" >> app/src/index.js
# 로그에서 nodemon이 자동 재시작하는 것을 확인
# [nodemon] restarting due to changes...
# [nodemon] starting `node src/index.js`
# 포트 3000 실행 중
개발 중에는 이 방식으로, 배포할 때는 --build로 이미지를 새로 만들어 배포하는 패턴을 구분해서 사용합니다.
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d배포 버전을 이미지 태그로 관리하면 이전 버전으로 빠르게 롤백할 수 있습니다.
# v1.0 이미지 보존용 태그
docker compose build app
docker tag web-was-lab-app:latest web-was-lab-app:v1.0
# 코드 수정 후 v1.1 빌드
# ... (코드 수정)
docker compose build app
docker tag web-was-lab-app:latest web-was-lab-app:v1.1
# v1.1 배포
docker compose up -d app
# 문제 발생 시 v1.0으로 롤백
# docker-compose.yml의 app.image를 web-was-lab-app:v1.0으로 변경 후
docker compose up -d app
# 이미지 목록 확인
docker images | grep web-was-lab-app
docker compose build --build-arg APP_VERSION=1.1.0 app- 먼저 curl -I http://localhost 의 응답 코드를 확인한다 — 그 다음 docker logs nginx-container 에서 upstream connect error 메시지가 있는지 읽는다. 502 Bad Gateway는 Nginx가 WAS에 연결을 시도했지만 실패한 것이고, 504 Gateway Timeout은 WAS가 응답하지 않는 것이다 — 원인이 다르므로 두 코드를 구분해야 한다
- nginx.conf의 proxy_pass에 서비스 이름이 docker-compose.yml의 서비스 이름과 정확히 일치하는지 확인한다 — 하나라도 다르면 DNS 해석 실패로 502가 난다. 컨테이너 이름(--name)이 아닌 서비스 이름(docker-compose.yml의 최상위 키)을 써야 한다
- docker compose ps 에서 모든 서비스가 Up 상태이고 — docker stats 에서 WAS 컨테이너 CPU 사용률이 100% 근처이면 → WAS 과부하로 502가 날 수 있다. CPU가 정상인데도 502가 나면 네트워크 설정 또는 포트 불일치 문제다
트러블슈팅
Nginx가 502 오류를 반환하면 WAS 컨테이너와의 연결에 문제가 있는 것입니다.
# 1. WAS 컨테이너 상태 확인
docker compose ps
docker compose logs app --tail=30
# 2. Nginx 오류 로그 확인
docker compose logs nginx --tail=20
# upstream connect error / connection refused 메시지 확인
# 3. 네트워크 연결 직접 테스트
docker compose exec nginx wget -qO- http://app:3000/api/health
# 이 명령이 실패하면 서비스 이름 불일치 또는 WAS가 아직 시작 중
# 4. WAS가 시작되기 전에 Nginx가 연결 시도하는 경우
# → docker-compose.yml에 depends_on + healthcheck 추가
# 해결: nginx가 app의 헬스체크 통과 후 시작하도록
nginx:
depends_on:
app:
condition: service_healthy
가장 흔한 원인은 세 가지입니다:
- nginx.conf의
proxy_passURL이 docker-compose.yml 서비스 이름과 다름 (appvswas) - WAS가 아직 시작 중인데 Nginx가 먼저 트래픽을 받는 경우 →
healthcheck+depends_on으로 해결 - WAS가 크래시된 경우 →
docker compose logs app으로 오류 확인
docker compose up -d를 재실행했는데 이전 코드가 그대로 동작하는 경우입니다.
# 문제: --build 없이 실행하면 기존 이미지 재사용
docker compose up -d # 이미지 재빌드 안 함!
# 해결 1: --build 플래그 명시
docker compose up --build -d app
# 해결 2: 이미지 강제 삭제 후 재빌드
docker compose down
docker image rm web-was-lab-app
docker compose up --build -d
# 현재 실행 중인 컨테이너가 어느 이미지를 사용하는지 확인
docker inspect web-was-lab-app-1 --format '{{.Image}}'
# sha256:... 로 확인 후 docker images와 비교
개발 환경에서 bind mount를 사용하는 경우에는 이 문제가 발생하지 않습니다. 프로덕션 배포 워크플로우에서는 항상 --build를 명시하는 습관을 들이세요.
개발 환경에서 ./app:/app으로 전체 디렉토리를 mount하면 컨테이너 내부의 node_modules가 호스트 디렉토리로 덮어쓰여 오류가 발생합니다.
# 증상: nodemon 시작 후 모듈을 찾을 수 없다는 오류
# Error: Cannot find module 'express'
# 원인: 전체 /app을 mount하면 컨테이너에서 RUN npm ci로 설치한
# node_modules가 호스트의 빈 디렉토리로 덮어쓰임
# 해결: 소스 코드만 mount하고 node_modules는 컨테이너 내부 유지
app:
volumes:
- ./app/src:/app/src # 소스만 mount
# node_modules는 mount 안 함 → 컨테이너 내부 것 사용
# 또는 named volume으로 node_modules 보호
app:
volumes:
- ./app:/app # 전체 mount
- /app/node_modules # node_modules는 anonymous volume으로 보호
두 번째 방식에서 /app/node_modules 줄은 익명 볼륨으로 컨테이너 내부의 node_modules를 호스트 mount로부터 보호합니다.
심화 — 컨테이너 통신의 실제: NET 네임스페이스와 임베디드 DNS
심화: 왜 서비스 이름은 되고 localhost는 안 되나 — 네트워크 네임스페이스
"nginx.conf에 proxy_pass만 맞게 쓰면 되겠지"에서 멈추면, localhost로 썼을 때 왜 502가 나는지·왜 서비스 이름은 되는지 설명하지 못합니다. 그 답은 컨테이너가 네트워크를 어떻게 갖는지에 있습니다.
- 컨테이너마다 독립된 NET 네임스페이스:
docker run은 컨테이너에 자기만의 네트워크 네임스페이스를 줍니다 — 자기 loopback(127.0.0.1)과 자기 인터페이스를 갖습니다. 그래서 nginx 컨테이너 안에서의localhost는 nginx 자신이지 WAS가 아닙니다.proxy_pass http://localhost:3000은 nginx가 자기 자신의 3000 포트를 찾다 실패합니다. - veth로 브리지에 연결된다: 컨테이너의 인터페이스는 veth 페어로 호스트의 브리지에 연결됩니다. Compose는 프로젝트마다 사용자 정의 브리지 네트워크를 자동으로 만들어 모든 서비스를 그 위에 올립니다.
- 임베디드 DNS가 이름을 IP로 푼다: 같은 사용자 정의 네트워크의 컨테이너끼리는 Docker의 임베디드 DNS(127.0.0.11) 가 서비스 이름을 현재 컨테이너 IP로 해석해 줍니다. 그래서
proxy_pass http://app:3000처럼 서비스 이름을 쓰면, 컨테이너가 재시작돼 IP가 바뀌어도 이름으로 계속 찾습니다. - 이름 해석은 '같은 네트워크'에서만: 이 DNS 해석은 두 컨테이너가 같은 사용자 정의 네트워크에 있을 때만 됩니다. 기본 bridge 네트워크는 이름 기반 DNS를 제공하지 않고, 서로 다른 네트워크에 있으면 이름을 풀 수 없습니다.
정리하면 컨테이너 간 통신은 'localhost 공유'가 아니라 '같은 네트워크 위에서 이름으로 서로를 찾는' 모델입니다. 이 그림이 있으면 502의 원인이 이름인지, 네트워크인지, 앱 준비 상태인지 나눠서 볼 수 있습니다.
상황: docker compose up으로는 잘 붙던 Nginx + WAS 구성을, 급히 nginx만 docker run으로 따로 띄웠습니다. 그랬더니 nginx가 시작조차 못 하고 nginx: [emerg] host not found in upstream "app"을 뱉거나, 접속 시 502가 납니다. 같은 nginx.conf인데 Compose에서만 되는 것이 이상합니다.
원인: Docker의 서비스 이름 → IP 해석(임베디드 DNS)은 두 컨테이너가 같은 사용자 정의 네트워크에 있을 때만 동작합니다. Compose는 프로젝트마다 사용자 정의 브리지 네트워크를 자동으로 만들어 모든 서비스를 그 위에 올리기 때문에 이름으로 서로를 찾습니다. 그런데 nginx를 docker run으로 따로 띄우면 기본 bridge(또는 app과 다른 네트워크)에 붙는데, 기본 bridge는 이름 기반 DNS를 제공하지 않고 app과도 다른 네트워크라 app이라는 이름을 풀 수 없습니다.
진단: docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' <nginx> 와 <app>을 각각 실행해 두 컨테이너가 같은 네트워크에 있는지 확인합니다. docker network ls로 Compose가 만든 네트워크(보통 <프로젝트>_default)가 보이는지, nginx가 그 위에 있는지 봅니다.
해결: 두 컨테이너를 같은 사용자 정의 네트워크에 둡니다 — docker network create appnet 후 두 컨테이너 모두 --network appnet으로 실행하거나, 애초에 하나의 Compose로 함께 관리합니다. 임시로는 docker network connect <app의 네트워크> <nginx>로 nginx를 app의 네트워크에 붙일 수 있습니다. 핵심 원칙은 '서비스 이름 해석은 같은 사용자 정의 네트워크 안에서만 된다'는 것입니다.
실무 맥락
팀에 새로운 백엔드 개발자가 합류했습니다. 기존에는 각자 PC에 Node.js를 직접 설치하고, nginx는 EC2 서버에만 있어서 "내 로컬에서는 되는데 서버에서는 안 돼요" 문제가 반복됐습니다.
Docker Compose로 Nginx + WAS 구성을 코드로 관리하면서 달라진 점:
# 새 팀원 온보딩: 이 세 줄로 끝
git clone https://github.com/company/backend.git
cd backend
docker compose up --build -d
# 개발 사이클:
# 1. 코드 수정 (VSCode에서 일반적으로 편집)
# 2. 로컬 확인: curl http://localhost/api/xxx
# 3. 커밋 & 푸시
# 4. 서버에서: git pull && docker compose up --build -d
환경 변수로 개발/스테이징/프로덕션 구분: 같은 이미지를 쓰되 환경별 변수만 바꾸는 패턴입니다.
# 개발
APP_VERSION=dev NODE_ENV=development docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
# 스테이징
APP_VERSION=1.2.0-rc1 NODE_ENV=staging docker compose up --build -d
# 프로덕션
APP_VERSION=1.2.0 NODE_ENV=production docker compose up --build -d
이 패턴이 익숙해지면, 나중에 CI/CD 파이프라인에서 docker compose up --build -d를 자동 실행하도록 확장하기 쉽습니다. 빌드와 배포가 표준화된 명령어 하나로 수렴되기 때문입니다.
기존 PHP/Apache 모놀리식 서비스를 Node.js API + Nginx로 전환하는 마이그레이션 프로젝트에서, 전환 기간 동안 구버전과 신버전을 동시에 운영해야 했습니다.
# 마이그레이션 기간 compose 구성
services:
nginx:
volumes:
- ./nginx/migration.conf:/etc/nginx/conf.d/default.conf:ro
new-api:
build: ./new-api
# 새 API 서버
legacy-php:
image: php:8.2-apache
volumes:
- ./legacy:/var/www/html
# 기존 PHP 서비스
# nginx/migration.conf — 경로별로 트래픽 분리
server {
listen 80;
# 새로 개발된 엔드포인트는 새 API로
location /api/v2/ {
proxy_pass http://new-api:3000;
}
# 아직 마이그레이션 안 된 엔드포인트는 레거시로
location / {
proxy_pass http://legacy-php:80;
}
}
Nginx의 location 블록으로 URL 경로 기준으로 트래픽을 분리했습니다. 새 엔드포인트가 준비될 때마다 nginx.conf만 수정하고 docker compose restart nginx로 반영해 점진적으로 마이그레이션을 진행했습니다.
핵심 요약
| 상황 | 명령어 | 설명 |
|---|---|---|
| 최초 빌드 및 실행 | docker compose up --build -d | Dockerfile 빌드 후 전체 스택 실행 |
| 코드 수정 후 재배포 | docker compose up --build -d app | app 서비스만 재빌드 |
| 개발 환경 실행 | docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d | hot reload 포함 |
| 로그 확인 | docker compose logs -f app | WAS 실시간 로그 |
| 전체 종료 | docker compose down | 컨테이너+네트워크 삭제 |
| 완전 초기화 | docker compose down -v --rmi local | 볼륨+이미지까지 삭제 |
| 서비스 상태 확인 | docker compose ps | 헬스체크 포함 상태 |
| WAS에 직접 접속 | docker compose exec app sh | 내부 디버깅 |
명령어·단축키 빠른 참조
Nginx+WAS 배포 루프에서 실제로 쓴 Compose·디버깅·롤백 명령을 모았습니다.
| 명령어/단축키 | 용도 | 자주 쓰는 예 |
|---|---|---|
docker compose up --build | 재빌드 후 실행(전체/특정 서비스) | docker compose up --build -d / ... -d app(app만) |
docker compose up --no-deps | 의존 서비스 재시작 없이 한 서비스만 교체 | docker compose up --build -d --no-deps app |
docker compose -f -f | base+override로 개발 환경 전환 | docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d |
docker compose build --build-arg | 빌드 인자로 버전 주입 | docker compose build --build-arg APP_VERSION=1.1.0 app |
docker compose ps | 서비스 상태·헬스 확인 | docker compose ps --format "table {{.Service}}\t{{.Status}}" |
docker compose logs | 서비스 로그 확인 | docker compose logs -f app / docker compose logs nginx --tail=20 |
docker compose exec | 컨테이너 내부 접속·연결 테스트 | docker compose exec nginx wget -qO- http://app:3000/api/health |
curl -I | Nginx 응답 코드로 502/504 구분 | curl -I http://localhost (502=연결실패, 504=타임아웃) |
docker tag / images | 버전 태그로 롤백 대비 | docker tag web-was-lab-app:latest web-was-lab-app:v1.0 |
docker inspect | 실행 컨테이너가 쓰는 이미지 확인 | docker inspect web-was-lab-app-1 --format '{{.Image}}' |
docker network create/connect | 별도 실행 컨테이너 이름 해석 연결 | docker network create appnet → docker run --network appnet ... |
docker compose down | 스택 종료(볼륨·이미지까지 초기화) | docker compose down / docker compose down -v --rmi local |
관련 모듈로 더 깊이:
- 복잡한 멀티 서비스 환경의 유기적 연동과 배포 — Nginx·WAS·DB를 한 파일로 선언하는 Compose 문법과 서비스 의존성의 기초
- 실무에 필수적인 멀티 스테이지 빌드와 최적화 기법 — 배포할 WAS 이미지를 작고 안전하게 빌드해 프로덕션에 올리는 법
- 컨테이너 상태 진단을 위한 헬스체크와 Restart Policy 설정 — depends_on + service_healthy로 시작 순서와 자가 치유를 보장하는 법
다음 모듈에서는 컨테이너의 상태를 자동으로 진단하고 장애 시 재시작하는 헬스체크와 Restart Policy 설정을 다룹니다.