| 폴더 | 내용 |
|---|---|
bin/ |
startup.sh shutdown.sh catalina.sh setenv.sh |
conf/ |
server.xml context.xml |
webapps/ |
war 와 풀린 폴더 |
work/ |
JSP 컴파일 캐시 |
logs/ |
catalina.out 접근 로그 |
temp/ |
임시 파일 |
CATALINA_HOME 은 설치 폴더, CATALINA_BASE 는 인스턴스 폴더입니다. 하나만 쓰면 둘이 같습니다. 여러 인스턴스를 띄울 때 BASE 를 나눠 conf/·webapps/·logs/ 를 분리합니다.
war 를 webapps/ 에 놓으면 Tomcat 이 자동으로 풀어 같은 이름의 폴더를 만듭니다. ROOT.war 는 / 경로, shop.war 는 /shop 경로입니다.
| 방식 | 장점 | 단점 |
|---|---|---|
| 실행 중 war 복사 | 정지 없음 | 옛 클래스 잔존 위험 |
| 정지 → 교체 → 시작 | 깨끗함 | 수십 초 중단 |
| 인스턴스 2개 교대 | 무중단 | nginx 조합 필요 |
이 레슨은 두 번째 방식을 기본으로 합니다. 실행 중 교체는 autoDeploy 가 풀린 폴더를 지우지 못하는 경우가 있어 운영에서는 피합니다.
옛 결과물을 전부 지워야 새 war 만 남습니다. 지울 것은 세 가지입니다.
rm -rf webapps/ROOT webapps/ROOT.war # 풀린 폴더와 war
rm -rf work/Catalina/localhost/ROOT # JSP 컴파일 캐시
cp new.war webapps/ROOT.warwork/ 를 안 지우면 JSP 를 고쳤는데 옛 화면이 나옵니다. temp/ 는 지우지 않아도 되지만 배포 전 backup/ 에 이전 war 를 복사해 두면 롤백이 cp 한 번입니다.
startup.sh 는 catalina.sh start, shutdown.sh 는 catalina.sh stop 의 줄임입니다. JVM 옵션은 bin/setenv.sh 에 둡니다.
# bin/setenv.sh
export CATALINA_OPTS="-Xms1g -Xmx1g -Duser.timezone=Asia/Seoul"
export JAVA_HOME=/usr/lib/jvm/java-21-openjdkCATALINA_OPTS 는 서버 실행에만, JAVA_OPTS 는 shutdown.sh 같은 도구에도 적용됩니다. 힙처럼 서버 전용 값은 CATALINA_OPTS 에 넣습니다. catalina.sh run 은 포그라운드로 떠서 systemd 의 Type=simple 에 맞습니다.
shutdown.sh 는 8005 포트로 정지 명령을 보냅니다. 스레드가 끝나지 않으면 JVM 이 남아 있고, 그 상태에서 startup.sh 를 치면 8080 포트 충돌이 납니다.
shutdown.sh
for _ in $(seq 1 30); do
pgrep -f "catalina.base=$CATALINA_BASE" >/dev/null || break
sleep 1
done
pkill -9 -f "catalina.base=$CATALINA_BASE" # 30초 뒤에도 남았으면catalina.base= 문자열로 찾으면 같은 서버의 다른 Tomcat 인스턴스를 건드리지 않습니다. pkill -f java 는 절대 쓰지 않습니다.
03 레슨과 같은 형태로 Tomcat 도 서비스로 올립니다. catalina.sh run 을 쓰면 Type=simple 로 충분합니다.
[Service]
Type=simple
User=tomcat
Environment=CATALINA_HOME=/opt/tomcat
ExecStart=/opt/tomcat/bin/catalina.sh run
ExecStop=/opt/tomcat/bin/shutdown.sh
SuccessExitStatus=143
Restart=on-failure배포 스크립트는 systemctl 유닛이 있으면 그것을 쓰고, 없으면 startup.sh·shutdown.sh 를 씁니다. 두 방식을 섞어 쓰면 systemd 가 모르는 프로세스가 생겨 상태가 어긋납니다.
nginx 는 앞단에서 세 가지 일을 합니다. 80·443 을 받아 8080 으로 넘기는 리버스 프록시, 정적 파일 직접 서비스, 여러 인스턴스로 분배입니다.
upstream myapp { server 127.0.0.1:8080; }
server {
listen 80;
location /static/ { root /opt/myapp/public; }
location / { proxy_pass http://myapp; }
}설정은 /etc/nginx/nginx.conf 가 conf.d/*.conf 를 읽어 들이는 구조입니다. 앱마다 conf.d/앱이름.conf 하나를 두면 다른 앱 설정을 건드리지 않습니다.
프록시를 거치면 자바 앱은 클라이언트 IP 대신 127.0.0.1 을 봅니다. 원래 정보를 헤더로 넘겨야 로그와 리다이렉트가 맞습니다.
| 헤더 | 값 | 용도 |
|---|---|---|
Host |
$host |
가상 호스트 판별 |
X-Real-IP |
$remote_addr |
클라이언트 IP |
X-Forwarded-For |
$proxy_add_x_forwarded_for |
프록시 경유 IP 목록 |
X-Forwarded-Proto |
$scheme |
http/https 구분 |
Spring Boot 는 server.forward-headers-strategy=native 를 켜면 이 헤더를 읽어 request.getRemoteAddr() 와 리다이렉트 URL 을 바로잡습니다.
설정을 고친 뒤 순서는 검사, reload 입니다. reload 는 마스터가 새 설정을 읽고 워커를 하나씩 교체하므로 기존 연결이 끊기지 않습니다.
| 명령 | 동작 | 연결 |
|---|---|---|
nginx -t |
문법·파일 존재 검사 | 영향 없음 |
systemctl reload nginx |
설정 다시 읽기 | 유지 |
systemctl restart nginx |
완전 재시작 | 끊김 |
nginx -s reload |
reload 와 같음 | 유지 |
restart 는 바이너리 업그레이드나 reload 로 안 바뀌는 항목을 고칠 때만 씁니다. 대부분의 설정 변경은 reload 로 충분합니다.
새 설정을 적용하기 전 이전 파일을 .bak 으로 남기고, 검사에 실패하면 되돌립니다. -t 가 실패한 상태로 두면 다음 사람이 restart 했을 때 nginx 가 안 뜹니다.
cp conf.d/myapp.conf conf.d/myapp.conf.bak
cp new.conf conf.d/myapp.conf
nginx -t || { cp conf.d/myapp.conf.bak conf.d/myapp.conf; exit 1; }
systemctl reload nginxnginx -t 는 root 권한이 필요합니다. 로그 파일 열기 권한까지 검사하기 때문입니다.
프록시 뒤 자바 앱이 문제일 때 nginx 가 돌려주는 코드입니다. 코드로 원인을 좁힙니다.
| 코드 | 뜻 | 확인할 것 |
|---|---|---|
| 502 | 뒷단 연결 실패 | Tomcat 이 떠 있는지, 포트 |
| 504 | 뒷단 응답 시간 초과 | proxy_read_timeout, 느린 쿼리 |
| 413 | 요청 본문 초과 | client_max_body_size |
error_log 에 connect() failed (111: Connection refused) 가 있으면 502 이고 Tomcat 이 내려간 것입니다. upstream timed out 이면 504 로 앱이 느린 것입니다.
upstream 에 인스턴스를 둘 두고 하나씩 교체하면 서비스가 끊기지 않습니다. 교체할 인스턴스를 down 으로 표시하고 reload 하면 nginx 가 그쪽으로 보내지 않습니다.
upstream myapp {
server 127.0.0.1:8080;
server 127.0.0.1:8081 down; # 배포 중인 인스턴스
}배포 뒤 down 을 빼고 다시 reload 합니다. max_fails=3 fail_timeout=10s 를 두면 죽은 인스턴스를 자동으로 제외하지만, 배포 중 실패 응답이 3번 나가므로 명시적 down 이 더 깨끗합니다.