본문 바로가기

직접 만든 소프트웨어, 두 가지 개발 기록

구현 구조와 적용 범위, 확인한 기능과 남은 한계를 함께 기록합니다.

소프트웨어개발

Nginx 정적 파일 404 해결: root·alias 경로와 try_files 점검 순서

by 아빠띠띠뽀 2026. 10. 15.

파일은 있는데 Nginx 정적 요청이 404라면, 요청 URI에서 실제 파일 경로를 한 번 계산하고 오류 로그가 찾은 경로와 비교하세요. root는 URI를 지정한 디렉터리 뒤에 붙이고, alias는 선택된 location 부분을 다른 경로로 바꿉니다. 호스트에 파일이 존재한다는 사실만으로 Nginx 프로세스가 같은 경로를 본다고 확정할 수 없습니다.

Linux의 Nginx가 정적 파일을 직접 제공하는 경우를 다룹니다. Docker를 쓰면 컨테이너 안의 파일 경로를 기준으로 점검합니다. proxy_pass 뒤 애플리케이션이 돌려준 404는 별도 경로입니다. 공식 문서 확인일은 2026년 10월 14일이며 아래 경로는 설명용 예시입니다. 실제 서버에서 재현하거나 설정을 적용했다고 주장하지 않습니다.

404 원인부터 분리: 누가 응답했고 어떤 파일을 찾았나

동일한 404라도 정적 파일 탐색 실패, 잘못 선택된 server/location, try_files의 마지막 분기, 애플리케이션 404, 캐시된 응답은 원인이 다릅니다. 문제 URL 하나를 고정하고 Host, 요청 시각, 상태, Content-Type, 오류 로그의 실제 경로를 묶어 확인합니다. CDN을 앞에 뒀다면 원본과 캐시 경로를 구분합니다. 응답의 Server 헤더 하나만으로 실제 발생 지점을 단정하지 않습니다.

관찰한 증상 먼저 확인할 것 서두르면 안 되는 변경
로그 경로에 /assets/가 중복됨 root 뒤 URI 결합과 alias 치환 무조건 모든 location을 alias로 변경
파일이 호스트에만 있음 컨테이너 마운트와 실제 파일 경로 호스트 경로를 그대로 설정에 복사
.js 요청이 200인데 화면은 실패 Content-Type과 HTML fallback 본문 200만 보고 정적 파일 정상 판정
로그에 Permission denied 부모 디렉터리 탐색·파일 읽기·보안 정책 전체 디렉터리를 777로 변경

1. root와 alias를 한 요청으로 계산한다

요청이 /assets/app.js일 때 다음 두 설정은 의도한 경로가 다릅니다. A에서는 /srv/site/assets/app.js를 찾습니다. B에서는 /srv/static/app.js를 찾습니다. 실제 파일이 후자에 있다면 root /srv/static을 그대로 쓰는 방식은 /srv/static/assets/app.js를 찾게 됩니다. 먼저 이 경로 차이를 종이에 적듯 계산하면 불필요한 reload를 줄일 수 있습니다.

# A: URI 전체를 root 뒤에 결합
location /assets/ {
    root /srv/site;
    try_files $uri =404;
}

# B: location 접두부를 alias 경로로 치환
location /assets/ {
    alias /srv/static/;
}

위 두 location은 비교용 대안이므로 같은 server 블록에 동시에 붙이지 않습니다. /assets/ 같은 디렉터리 접두부에는 alias 경로의 끝 슬래시도 의도에 맞게 둡니다. 정규식 location의 alias는 캡처를 이용한 완전한 파일 경로 구성이 필요할 수 있어 이 단순 접두부 예제를 그대로 복사하지 않습니다. 다른 location과 rewrite가 먼저 요청을 바꾸는지도 확인합니다.

2. 실제 실행 환경의 설정과 파일을 확인한다

# 호스트 Nginx의 읽기 중심 점검 예시
nginx -v
nginx -T
stat /srv/site/assets/app.js
namei -l /srv/site/assets/app.js
curl -I https://example.com/assets/app.js

# Docker라면 해당 컨테이너 내부에서 경로 확인
docker compose exec nginx nginx -T
docker compose exec nginx stat /srv/static/app.js

nginx는 예시 서비스 이름이므로 실제 Compose 서비스명으로 바꿉니다. nginx -T는 활성 설정 확인에 쓰지만 배포 설정이나 민감한 경로가 출력될 수 있으니 원문을 공개 게시판에 올리지 않습니다. 명령 권한, nginx 바이너리 경로와 Linux에서 namei 사용 가능 여부를 확인합니다. 파일 읽기는 소유자 한 명의 권한만 보는 것이 아니라 Nginx worker가 부모 디렉터리를 통과할 수 있는지도 봅니다.

컨테이너라면 호스트 파일명·컨테이너 파일명·마운트 대상을 각각 적습니다. 이미지 안의 파일을 bind mount가 가리는 경우도 있으므로 배포 이미지에 존재한다는 확인으로 끝내지 않습니다. 대소문자와 실제 배포 버전의 파일명도 대조합니다. 로그가 permission 문제를 가리키면 UID/GID, 디렉터리 접근, SELinux 등 해당 보안 정책을 따로 검토합니다. 보안 기능을 끄는 것은 기본 해결 절차가 아닙니다.

3. try_files와 SPA fallback을 파일 경로와 분리해 본다

try_files는 후보를 현재 root 또는 alias 기준으로 확인하고 없으면 마지막 URI로 내부 이동하거나 지정 상태를 반환합니다. “항상 /index.html로 보내기”는 존재하지 않는 JavaScript나 CSS 요청도 HTML로 바꿀 수 있습니다. 이때 200이 나와도 브라우저는 올바른 자산을 받지 못했습니다. 정적 자산 경로는 실제 파일이 없으면 실패를 드러내고, 앱 화면 라우트의 fallback은 필요한 범위에서 별도로 구성합니다.

alias와 복잡한 정규식·rewrite·fallback을 한 번에 섞기보다 단순한 접두부 location으로 파일 하나가 맞게 전달되는지 먼저 확인합니다. 작동하는 경로에서 필요한 분기를 한 가지씩 추가합니다. 단순히 =404를 없애서 증상을 숨기면 실제 누락 파일이나 잘못된 배포가 계속 남습니다.

수정 뒤 검증과 주의사항

운영 변경 전 활성 설정을 안전한 승인된 위치에 보관하고 변경할 server/location을 좁힙니다. nginx -t로 구문 검사를 통과시킨 뒤 사용하는 서비스 관리 방식의 reload 절차를 따릅니다. 이 글의 읽기 점검 명령은 reload를 실행하지 않습니다. 구문 검사 성공도 실제 URL 정상 제공과 다르므로 변경 후 같은 URL의 상태·Content-Type·파일 내용, 없는 자산 URL의 실패, 앱 화면 라우트를 함께 확인합니다.

HEAD 요청을 지원하지 않는 앱 경로는 GET 결과로 확인해야 할 수 있습니다. 정적 파일은 응답 상태만 보지 말고 기대한 배포 파일 내용과 크기 또는 해시도 대조합니다. 파일 경로를 공개 URL에 과도하게 노출하지 않고, 캐시 무효화가 필요한지 확인합니다. 실패하면 보존한 설정으로 해당 변경만 되돌리고 다른 원인을 계속 분리합니다.

FAQ

파일이 있는데 왜 404인가요? 다른 root/alias 경로, 다른 server/location, 컨테이너 안의 누락 파일, 내부 redirect 등을 먼저 구분합니다.

alias로 바꾸면 항상 해결되나요? 아닙니다. 배포 디렉터리가 URI 구조와 일치하면 root가 더 단순할 수 있습니다. 정규식 location과 다른 분기의 영향도 확인합니다.

try_files에 /index.html을 넣으면 되나요? SPA 화면 라우트에는 필요할 수 있지만 정적 자산 누락을 HTML 200으로 가리는 결과는 피해야 합니다. 자산 경로와 화면 경로를 따로 검증합니다.

공식 출처와 관련 글

공식 자료 확인일은 2026년 10월 14일입니다. 아래 예시는 문서로 검토한 설정·진단 절차이며 운영 계정이나 서버에서 직접 실행한 결과가 아닙니다. 발행일의 가격이나 실제 성능을 확정하지 않습니다.

Nginx core: alias·root·location·try_files 정의

Nginx: 정적 파일 경로와 파일 탐색

함께 확인하면 좋은 글: Nginx 502의 upstream 연결 문제 구분

브라우저 URL의 경로 조각이 파일 경로로 연결되며 잘못된 경로와 교정 경로를 대비한 그림
직접 생성한 개념 이미지입니다. URL과 실제 파일 경로의 대응을 설명하며 실제 오류 화면이나 실행 결과가 아닙니다.