Nginx 리버스 프록시 설정 완벽 가이드
Ubuntu 24.04 환경에서 Nginx 리버스 프록시를 줄 단위로 설정하는 방법을 설명합니다. proxy_pass 설정부터 헤더 전달, 웹소켓 연결, 업로드 제한 및 슬래시 처리까지 실무에 필요한 핵심 지시어를 상세히 다룹니다.
Nginx 리버스 프록시 설정의 역할
Nginx 리버스 프록시는 80번 포트와 443번 포트로 들어오는 요청을 받아 로컬 포트에서 대기 중인 애플리케이션으로 각각 전달한 뒤, 애플리케이션의 응답을 브라우저로 반환합니다. 설정은 하나의 server 블록으로 구성되며, 이 블록은 매우 짧습니다. 설정의 대부분은 실제 클라이언트가 누구인지, 클라이언트가 어떤 프로토콜을 사용했는지 애플리케이션에 알리는 5~6줄의 설정에서 결정됩니다.
아래의 모든 내용은 Ubuntu 24.04 환경에서 배포판의 nginx 패키지를 사용하여 처음부터 구축하는 과정을 다룹니다. 시작점은 이미 127.0.0.1:3000에서 응답하고 있는 애플리케이션입니다. 아직 사용할 프록시를 결정하지 못했다면, Nginx와 Caddy 및 Traefik 비교를 먼저 읽어보시기 바랍니다. 이어지는 내용은 Nginx 설정이 줄 단위로 어떻게 구성되는지 보여줍니다.
이 설정들을 자신의 서버에서 직접 실행해 보십시오. 설정을 다시 불러오기(reload) 전에는 반드시 sudo nginx -t으로 모든 변경 사항을 테스트하고, 출력되는 내용을 확인하십시오.
Ubuntu에서 Nginx 설정 파일의 위치
sudo apt update
sudo apt install -y nginx
ls -l /etc/nginx/sites-enabled/메인 설정 파일은 /etc/nginx/nginx.conf입니다. 이 파일은 http { } 블록 내에서 전역 옵션을 설정한 뒤, /etc/nginx/conf.d/*.conf과 /etc/nginx/sites-enabled/* 두 디렉터리의 설정을 불러옵니다. Ubuntu와 Debian에서는 사이트별로 하나의 파일을 /etc/nginx/sites-available/에 작성하고, /etc/nginx/sites-enabled/로 심볼릭 링크를 생성하여 사이트를 활성화합니다. 심볼릭 링크를 삭제하면 사이트는 비활성화되지만 파일은 그대로 유지됩니다.
이후에 사용될 두 지시어는 http 컨텍스트에서만 작동하며, server 블록 내부에서는 사용할 수 없습니다. 바로 map와 upstream입니다. 이 지시어들은 /etc/nginx/conf.d/ 하위의 별도 파일에 작성하십시오. 해당 디렉터리는 http 수준에서 포함되기 때문입니다.
패키지에는 default이라는 기본 활성화 사이트가 포함되어 있습니다. 이 사이트는 default_server로 표시되어 있는데, 이는 설정 내의 어떤 server_name와도 일치하지 않는 Host 헤더를 가진 모든 요청에 응답한다는 의미입니다. 이 사이트가 활성화되어 있는 동안에는 사용자의 도메인과 일치하지 않는 요청이 애플리케이션이 아닌 이 사이트로 전달됩니다. 본인의 사이트가 정상적으로 작동하면 해당 심볼릭 링크를 삭제하십시오.
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx애플리케이션 하나를 프록시하는 최소 단위의 서버 블록
server {
listen 80;
listen [::]:80;
server_name app.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
}
}이 내용을 /etc/nginx/sites-available/app.example.com으로 저장한 뒤, 활성화하고 로드합니다.
sudo ln -s /etc/nginx/sites-available/app.example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
curl -sI -H 'Host: app.example.com' http://127.0.0.1/listen 80;은 IPv4에 바인딩하고 listen [::]:80;는 IPv6에 바인딩합니다. 두 번째 줄을 생략하면 서버의 DNS(domain name system) 조회 결과로 AAAA 레코드를 받은 방문자는 연결 거부를 겪게 되며, IPv4를 사용하는 사용자만 정상적으로 사이트를 이용하게 됩니다. 이때 받는 버그 리포트는 "저는 잘 되는데요"라는 내용일 것입니다.
server_name은 브라우저가 전송하는 Host 헤더와 대조됩니다. 공백으로 구분하여 여러 도메인 이름을 나열할 수 있습니다. 일치하는 블록이 없으면 Nginx는 default_server로 설정된 블록을 사용하므로, 패키지로 설치된 기본 사이트 설정을 삭제해야 합니다.
location /은 요청 경로에 대한 접두사 일치 방식이며, /은 모든 경로와 일치합니다. proxy_pass은 Nginx가 연결을 맺을 주소입니다. 애플리케이션은 127.0.0.1에 바인딩된 상태로 유지하여 Nginx를 통해서만 접근할 수 있도록 하십시오. 애플리케이션이 컨테이너에서 실행 중이라면 3000:3000이 아닌 127.0.0.1:3000:3000으로 포트를 게시하십시오. Docker는 자체 규칙을 작성하여 ufw 설정을 우회하고 포트를 직접 노출하기 때문에, 방화벽 설정과 관계없이 게시된 포트는 인터넷에서 직접 접근할 수 있게 됩니다.
curl 줄은 서버 자체에서 올바른 Host 헤더를 전송하도록 하므로, DNS를 연결하기 전에 해당 블록을 미리 테스트할 수 있습니다.
추가 설정이 없을 때 nginx가 업스트림으로 전달하는 내용
proxy_pass만 단독으로 사용하면 애플리케이션에서 네 가지 정보가 가려집니다.
nginx는 기본적으로 백엔드와 HTTP/1.0으로 통신하며 Connection: close를 전송하므로, 모든 요청마다 새로운 업스트림 연결이 생성되어 프로토콜 업그레이드가 불가능합니다.
Host 헤더는 proxy_pass의 값인 127.0.0.1:3000로 재작성됩니다. Host를 기반으로 절대 경로 링크를 생성하는 애플리케이션은 이제 서버 외부에서 열 수 없는 링크를 생성하게 됩니다.
애플리케이션에 도달하는 연결은 nginx로부터 오기 때문에, 애플리케이션은 클라이언트 주소를 127.0.0.1으로 인식합니다. 따라서 애플리케이션 내부의 모든 로그와 속도 제한(rate limit) 설정은 방문자가 아닌 프록시를 기록하게 됩니다.
애플리케이션은 브라우저가 HTTPS를 사용했는지 알 수 없습니다. 수신한 연결이 루프백 주소에서의 일반 HTTP이기 때문입니다.
이 모든 문제는 네 줄의 설정으로 해결할 수 있습니다.
설정해야 할 4가지 헤더와 각 헤더가 백엔드에 전달하는 정보
location / {
proxy_pass http://127.0.0.1:3000;
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_set_header X-Forwarded-Proto $scheme;
}Host은 방문자가 입력한 도메인 이름을 전달합니다. $host는 요청에서 포트 번호를 제거하고 소문자로 변환한 이름입니다. 이 헤더를 설정해야 애플리케이션이 올바른 절대 경로 URL을 생성할 수 있습니다. 로그인 후 리다이렉트나 비밀번호 재설정 이메일 내 링크가 이에 해당합니다. 이 설정을 누락하면 URL이 127.0.0.1:3000을 가리키게 되어, 로그인 시 브라우저가 연결을 거부하는 주소로 이동하게 됩니다. 만약 애플리케이션이 8080 포트 등 특정 포트를 사용해야 한다면, 클라이언트가 보낸 헤더를 그대로 전달하는 $http_host를 사용하십시오.
X-Real-IP는 단일 값을 전달합니다. $remote_addr은 Nginx가 연결을 수락한 클라이언트의 주소입니다. 애플리케이션은 이 값을 자체 접근 로그 기록이나 속도 제한(rate limiting)에 사용합니다.
X-Forwarded-For은 목록을 전달합니다. $proxy_add_x_forwarded_for은 클라이언트가 이미 헤더에 포함한 값 뒤에 $remote_addr를 추가하므로, 값은 쉼표로 구분되며 Nginx가 추가한 항목이 마지막에 위치합니다. 이 세부 사항은 헤더의 신뢰 여부를 결정합니다. 클라이언트는 임의의 X-Forwarded-For을 보낼 수 있으므로, 첫 번째 항목만 읽는 애플리케이션은 잘못된 주소로 속을 수 있습니다. Nginx가 최전방 서버(edge server)라면 $remote_addr을 사용하여 클라이언트가 보낸 버전을 무시하십시오. CDN이나 다른 프록시가 앞에 있다면 realip 모듈의 set_real_ip_from와 real_ip_header을 사용하여 $remote_addr 자체가 실제 클라이언트 주소가 되도록 설정하십시오.
X-Forwarded-Proto는 http 또는 https을 전달합니다. 프레임워크는 이 값을 읽어 쿠키를 Secure로 표시할지, HTTPS로 강제 리다이렉트할지 결정합니다. TLS 사이트에서 이 설정을 누락하면, HTTPS를 강제하도록 설정된 애플리케이션은 http를 확인하고 HTTPS 주소로 리다이렉트 응답을 보냅니다. 이후 Nginx를 통해 다음 요청을 받더라도 여전히 http으로 인식하여 다시 리다이렉트를 반복합니다. 결국 브라우저는 요청을 포기하고 ERR_TOO_MANY_REDIRECTS 오류를 표시합니다.
모든 location 블록에 이 4줄을 반복해서 작성하면 설정이 서로 어긋나기 쉽습니다. 하나의 파일로 작성한 뒤 include 하십시오.
# /etc/nginx/snippets/proxy-headers.conf
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_set_header X-Forwarded-Proto $scheme;location / {
include snippets/proxy-headers.conf;
proxy_pass http://127.0.0.1:3000;
}이곳의 상속에는 함정이 있습니다. location 블록은 해당 블록 내에 proxy_set_header 지시어가 하나도 없을 때만 server 블록의 설정을 상속받습니다. location 내부에 proxy_set_header을 하나라도 추가하면, server 레벨에서 정의된 모든 헤더는 해당 location에서 무시됩니다. 따라서 모든 헤더를 단일 레벨에서 관리하거나, 프록시를 수행하는 각 location마다 스니펫을 include 하십시오.
WebSocket 애플리케이션이 연결되었다가 바로 끊기는 이유는 무엇입니까?
기본 설정이 업그레이드를 허용하지 않고, 기본 읽기 타임아웃 설정이 60초 동안 유휴 상태인 터널을 닫아버리기 때문입니다. WebSocket은 Upgrade: websocket와 Connection: Upgrade을 포함한 HTTP 요청으로 시작됩니다. 이들은 홉 단위(hop-by-hop) 헤더이므로 프록시가 이를 전달하지 않고 소비해야 하며, HTTP/1.0에는 업그레이드 메커니즘이 아예 없습니다. 따라서 두 헤더를 수동으로 다시 설정해야 합니다.
map은 http 컨텍스트 내의 별도 파일에 작성합니다.
# /etc/nginx/conf.d/websocket.conf
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}그다음 location 설정을 추가합니다.
location / {
include snippets/proxy-headers.conf;
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}map을 사용하는 이유는 하나의 location에서 두 종류의 트래픽을 모두 처리하기 위함입니다. 일반적인 요청 시 $http_upgrade은 비어 있으므로 $connection_upgrade는 close이 됩니다. 업그레이드 요청 시에는 websocket 값을 가지므로 업스트림으로 전달되는 헤더는 Connection: upgrade가 됩니다. proxy_set_header Connection "upgrade";을 하드코딩하면 일반 페이지 요청 시에도 해당 헤더가 전송되어, 일부 백엔드는 이를 400 오류로 응답합니다.
proxy_read_timeout는 "로딩은 되는데 업데이트가 멈춘다"는 보고의 주원인입니다. 이 값은 기본적으로 60초이며, 연결의 전체 수명이 아니라 백엔드로부터 데이터를 읽는 간격을 측정합니다. 60초 동안 아무런 데이터가 없는 WebSocket은 nginx에 의해 닫히며, 브라우저 콘솔에는 1006 코드로 소켓이 종료되었다고 표시됩니다. 1분보다 짧은 주기로 자체 하트비트를 보내는 애플리케이션은 문제가 없지만, 그렇지 않은 애플리케이션은 1분 뒤에 연결이 끊깁니다. 실시간 편집기나 대시보드에서 이 문제가 가장 먼저 나타나며, HTTPS 뒤에서 실행되는 n8n 인스턴스가 흔한 사례입니다.
proxy_pass의 끝에 붙는 슬래시가 URL을 변경하는 이유는 무엇입니까?
규칙은 한 문장입니다. proxy_pass가 URI(Uniform Resource Identifier)로 끝나면, 설령 그것이 단순한 /일지라도, nginx는 요청 경로에서 location 접두사와 일치하는 부분을 제거하고 그 자리에 해당 URI를 삽입합니다. proxy_pass이 호스트와 포트에서 끝난다면, 요청 경로는 변경되지 않고 그대로 전달됩니다.
location /app/ {
proxy_pass http://127.0.0.1:3000/;
}/app/status에 대한 요청은 백엔드에 /status으로 전달됩니다.
location /app/ {
proxy_pass http://127.0.0.1:3000;
}/app/status에 대한 요청은 백엔드에 /app/status로 전달됩니다.
어떤 형식을 사용할지는 애플리케이션에 따라 다릅니다. 베이스 경로(base-path)나 하위 폴더 설정이 있는 애플리케이션은 두 번째 형식을 원하며, 설정에 /app을 알려주어야 합니다. 접두사에 대해 아무것도 모르는 애플리케이션은 첫 번째 형식이 필요합니다. 첫 번째 형식은 즉시 확인할 수 있는 문제가 있습니다. 애플리케이션이 반환하는 HTML에 여전히 /static/main.css와 같은 절대 경로가 포함되어 있으면, 브라우저는 사이트 루트에서 해당 파일을 찾으려 시도합니다. 일치하는 location이 없으므로 페이지는 스타일 없이 렌더링됩니다. 브라우저의 네트워크 탭을 보면 해당 에셋 요청이 404 오류로 반환되는 것을 확인할 수 있습니다. 해결 방법은 애플리케이션 자체의 베이스 경로 설정을 수정하거나, 동일한 백엔드를 가리키는 두 번째 location /static/를 추가하는 것입니다.
정규 표현식(regex) location은 proxy_pass에 URI를 포함할 수 없습니다. sudo nginx -t은 해당 설정을 거부하며 그 이유를 "proxy_pass" cannot have URI part in location given by regular expression, or inside named location, or inside "if" statement, or inside "limit_except" block이라고 명시합니다.
이러한 유형의 문제는 각 애플리케이션에 고유한 도메인 이름인 app.example.com를 부여하고 location /에서 프록시를 설정하면 모두 사라집니다. 하위 경로(sub-path) 방식은 DNS 레코드를 추가할 수 없는 경우에만 감수할 가치가 있습니다.
하나의 도메인 이름 뒤에 여러 백엔드를 배치하려면 어떻게 해야 합니까?
upstream 블록을 사용하십시오. 이 블록은 http 컨텍스트에 속하므로, 같은 파일 내의 server 블록 위에 작성하거나 /etc/nginx/conf.d/에 작성하십시오.
upstream app_backend {
least_conn;
server 127.0.0.1:3000 max_fails=3 fail_timeout=30s;
server 127.0.0.1:3001 max_fails=3 fail_timeout=30s;
keepalive 32;
}그런 다음 location 블록에서 이를 지정합니다: proxy_pass http://app_backend;.
기본 방식은 라운드 로빈입니다. least_conn은 활성 연결 수가 가장 적은 백엔드로 각 요청을 전달하며, 이는 요청 처리 시간이 일정하지 않을 때 적합합니다. ip_hash은 특정 클라이언트 주소를 하나의 백엔드에 고정합니다. 애플리케이션이 세션을 자체 메모리에 유지하는 경우 ip_hash이 필요합니다. 라운드 로빈을 사용하면 요청이 세션 정보를 가지고 있지 않은 인스턴스로 전달될 때 사용자가 무작위로 로그아웃되기 때문입니다. 세션을 공유 저장소로 옮기는 것이 더 나은 해결책입니다.
max_fails=3 fail_timeout=30s는 30초 동안 3번의 실패가 발생하면 해당 서버를 30초 동안 제외한다는 의미입니다. 블록 내의 모든 서버가 이 상태가 되면 클라이언트는 502 오류를 받게 되며, 오류 로그에는 no live upstreams while connecting to upstream이 기록됩니다.
keepalive 32은 워커 프로세스당 최대 32개의 유휴 연결을 백엔드와 유지하여 대부분의 요청에서 TCP 핸드셰이크 과정을 생략합니다. 이는 proxy_http_version 1.1와 함께 사용할 때만 작동하며, 업스트림으로 Connection: close을 전달하지 않아야 합니다. 동일한 location에서 WebSocket 맵을 함께 사용하는 경우, 빈 케이스를 close에서 빈 문자열로 변경하십시오. 이렇게 하면 일반 요청에 Connection 헤더가 포함되지 않아 풀링된 연결을 재사용할 수 있습니다.
map $http_upgrade $connection_upgrade {
default upgrade;
'' '';
}upstream 블록 내부의 이름은 nginx가 시작될 때 해석됩니다. 백엔드가 재시작 시 새로운 주소를 할당받는 컨테이너라면, nginx는 설정을 다시 로드하기 전까지 이전 주소를 계속 사용합니다. Docker 네트워크 내부에서는 내장 리졸버를 사용하여 요청 시점에 주소를 조회하도록 설정할 수 있습니다.
resolver 127.0.0.11 valid=10s;
set $backend http://app:3000;
proxy_pass $backend;컨테이너가 자주 생성되고 삭제되어 nginx 설정을 계속 수정해야 하는 상황이라면, 컨테이너 레이블을 읽어 자동으로 설정하는 프록시가 더 나은 도구입니다. 여러 Docker Compose 앱 앞단의 Traefik은 컨테이너 자체에서 라우팅 정보를 구성합니다.
업로드 시 413 Request Entity Too Large 오류가 발생하는 이유는 무엇입니까?
client_max_body_size의 기본값은 1 메가바이트입니다. 이보다 큰 요청 본문은 애플리케이션에 도달하기 전에 nginx에서 거부되며, 오류 로그에는 client intended to send too large body가 기록됩니다. server 블록 또는 업로드가 발생하는 location 블록에서 이 값을 높이십시오.
client_max_body_size 512m;0 값을 설정하면 해당 검사가 완전히 비활성화됩니다. 애플리케이션 자체에도 제한이 있으므로, 이 설정을 변경한 후에도 413 오류가 발생한다면 백엔드에서 오류가 발생하는 것이며, 애플리케이션의 업로드 설정을 확인해야 합니다.
기본적으로 nginx는 업스트림 연결을 열기 전에 요청 본문 전체를 읽으며, 큰 데이터는 먼저 디스크의 임시 파일에 기록합니다. 이는 백엔드가 로컬 속도로 업로드를 처리하게 하여 느린 클라이언트로부터 애플리케이션을 보호합니다. 매우 큰 업로드를 처리해야 한다면 스트리밍 방식을 사용할 수 있습니다.
proxy_request_buffering off;이 경우 백엔드는 본문이 도착하는 대로 데이터를 수신하며 이를 처리할 수 있어야 합니다. 또한 본문 데이터가 이미 전송되었으므로, nginx는 다른 업스트림으로 요청을 재시도할 수 없습니다.
client_body_timeout(기본값 60초)은 전체 업로드 시간이 아니라 본문을 읽는 과정에서 연속된 두 읽기 작업 사이의 시간에 적용됩니다. 따라서 느리지만 꾸준히 진행되는 업로드는 유지되지만, 전송이 중단된 업로드는 차단됩니다.
응답 버퍼링과 실시간 출력을 방해하는 설정
proxy_buffering은 기본적으로 활성화되어 있으며, 일반적으로는 이 설정이 권장됩니다. Nginx는 애플리케이션이 응답을 작성하는 속도만큼 빠르게 이를 읽어 들여 저장한 뒤, 느린 클라이언트의 속도에 맞춰 데이터를 전달합니다. 덕분에 애플리케이션 워커는 느린 다운로드가 완료될 때까지 기다릴 필요 없이 작업을 일찍 종료할 수 있습니다.
하지만 이 설정은 스트리밍 응답을 방해합니다. 서버 전송 이벤트(Server-sent events)나 실시간 로그 출력의 경우, 버퍼가 가득 찰 때까지 사용자에게 아무런 내용도 표시되지 않습니다. 이러한 경우에는 해당 위치에서만 버퍼링을 비활성화하십시오.
proxy_buffering off;애플리케이션을 직접 제어할 수 있다면, 스트리밍 응답에만 X-Accel-Buffering: no 헤더를 전송하는 것이 더 나은 방법입니다. Nginx는 응답마다 이 헤더를 확인하여 해당 응답에 대해서만 버퍼링을 비활성화하므로, 일반적인 페이지들은 여전히 버퍼링의 이점을 누릴 수 있습니다.
오류 로그에 upstream sent too big header while reading response header from upstream이 기록된다면, 응답 헤더가 하나의 버퍼에 들어가지 못한 것입니다. proxy_buffer_size는 기본적으로 메모리 페이지 하나 크기인 4KB 또는 8KB(플랫폼에 따라 다름)로 설정되어 있는데, 긴 쿠키나 대용량 인증 헤더가 포함되면 이 크기를 초과하게 됩니다. 이 두 값을 모두 높이십시오.
proxy_buffer_size 16k;
proxy_buffers 8 16k;이 설정에서 TLS는 어디에 위치해야 합니까?
Nginx에서 모든 서비스의 앞단에 위치해야 합니다. TLS(Transport Layer Security)는 프록시에서 종료되며, Nginx에서 애플리케이션으로 연결되는 구간은 루프백 주소를 통해 일반 HTTP로 유지되므로 네트워크상의 다른 어떤 요소도 이를 읽을 수 없습니다. 애플리케이션은 4개의 헤더 중 네 번째인 X-Forwarded-Proto를 통해 방문자가 HTTPS를 사용했음을 알 수 있습니다.
인증서 경로를 직접 작성하지 마십시오. DNS 레코드를 서버로 지정하고 방화벽을 연 다음, Certbot이 이 서버 블록을 직접 수정하도록 하십시오. Certbot은 ssl_certificate 경로를 포함한 listen 443 ssl 라인을 추가하고 포트 80에서 리다이렉트를 설정합니다. Certbot을 사용하여 Nginx용 Let's Encrypt 인증서 발급하기에서 발급 및 갱신 타이머 설정 방법을 다룹니다.
sudo ufw allow 'Nginx Full'
sudo ufw statusNginx Full은 Nginx 패키지가 설치하는 애플리케이션 프로필이며, 포트 80과 포트 443을 동시에 엽니다. 모든 방문자를 HTTPS로 리다이렉트한 이후에도 HTTP-01 갱신 챌린지를 위해 포트 80은 계속 열려 있어야 합니다.
설정 테스트 및 리로드
sudo nginx -t
sudo systemctl reload nginxnginx -t는 포함된 모든 파일을 구문 분석하여 테스트 성공 여부를 보고하거나, 오류가 발생한 파일과 줄 번호를 출력합니다. 리로드하기 전에 반드시 이 출력을 확인하십시오. 설정이 잘못된 상태에서 리로드하면 변경 사항이 적용되지 않습니다. Nginx는 이전 설정을 유지하며 계속 작동하므로, 변경 사항이 적용되지 않은 채 사이트는 그대로 유지됩니다. systemctl restart은 이보다 더 위험하게 동작합니다. 재시작 시 실행 중인 서버를 먼저 종료하기 때문에, 설정 오류가 있으면 Nginx가 아예 실행되지 않는 상태가 됩니다. 기본적으로 리로드를 사용하고, 반드시 필요한 경우에만 재시작을 수행하십시오.
sudo tail -f /var/log/nginx/error.log
sudo ss -lntp | grep -E ':(80|443|3000)'ss 행을 확인하면 각 포트를 점유 중인 프로세스를 알 수 있습니다. 이를 통해 애플리케이션이 proxy_pass에서 지정한 위치에서 실제로 리스닝 중인지 확인할 수 있습니다.
실제로 마주하게 될 오류들
502 Bad Gateway, 에러 로그에 connect() failed (111: Connection refused) while connecting to upstream이 기록된 경우. proxy_pass에 지정된 주소에서 수신 대기 중인 프로세스가 없습니다. 애플리케이션이 중단되었거나, 다른 포트에 바인딩되었거나, 호스트에서 접근할 수 없는 컨테이너 내부 주소에 바인딩된 상태입니다.
no live upstreams while connecting to upstream가 포함된 502 오류. upstream 블록 내의 모든 서버가 현재 max_fails에 의해 실패 상태로 표시되었습니다. 백엔드를 복구하십시오. fail_timeout이 만료되면 Nginx가 재시도를 수행합니다.
504 Gateway Time-out, upstream timed out (110: Connection timed out) while reading response header from upstream가 기록된 경우. 백엔드가 연결을 수락했으나 proxy_read_timeout초 동안 아무런 응답을 보내지 않았습니다. 실제로 처리가 느린 보고서 작업이라면 타임아웃을 늘리는 것이 맞지만, 애플리케이션이 멈춘 상태라면 잘못된 해결책입니다.
모든 경로에서 애플리케이션이 404를 반환하는 경우. 후행 슬래시 규칙이 경로를 재작성했습니다. 애플리케이션 로그에 기록된 경로와 요청한 경로를 비교하십시오.
다른 사이트가 응답하는 경우. server_name이 Host 헤더와 일치하지 않아 요청이 default_server 블록으로 넘어갔습니다.
페이지는 로드되지만 약 1분 후 인터페이스가 멈추는 경우. WebSocket 관련 문제입니다. Upgrade 처리가 누락되었거나 proxy_read_timeout가 여전히 60초로 설정되어 있습니다.
FAQ
Why does nginx return 502 Bad Gateway after I add proxy_pass?
nginx could not open a connection to the address in proxy_pass. The error log at /var/log/nginx/error.log names the cause: connect() failed (111: Connection refused) while connecting to upstream means nothing is listening there, and no live upstreams means every server in an upstream block has been marked failed. Run sudo ss -lntp | grep 3000 to see which process holds the port and which address it is bound to. An app bound to a container-internal address, or to a port other than the one you wrote, gives this error every time.
Why does my app disconnect after about a minute behind nginx?
The connection is a WebSocket and proxy_read_timeout is still at its default of 60 seconds, which measures the gap between two reads from the backend. A quiet socket is closed by nginx and the browser console reports close code 1006. Set proxy_http_version 1.1, pass Upgrade and Connection through with a map on $http_upgrade, and raise proxy_read_timeout to something like 3600s. Without the Upgrade header the upgrade never happens at all, so the app falls back to polling or shows no live updates.
Does the trailing slash in proxy_pass matter?
Yes, and it changes the path your backend receives. With location /app/ and proxy_pass http://127.0.0.1:3000/, a request for /app/status arrives at the backend as /status, because any URI after the host and port replaces the matched location prefix. Drop that final slash and the same request arrives as /app/status. Stripping the prefix often breaks the app's own asset links, which stay absolute and then 404 at the site root, so an app with a base-path setting is better served by the form that passes the path through.
Why does my application log 127.0.0.1 as every visitor's IP address?
Because the connection the app receives really does come from nginx on the loopback address. The visitor's address only reaches the app in a header you set: proxy_set_header X-Real-IP $remote_addr; for a single value, and proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; for the appended chain. The app then has to be configured to trust those headers. Remember that a client can send its own X-Forwarded-For, so when nginx is the edge server, overwrite it with $remote_addr instead of appending.
Do I need TLS on the connection between nginx and my app?
Not when the app runs on the same server and is bound to 127.0.0.1, because that traffic never leaves the machine. Terminate TLS at nginx, keep proxy_pass on plain HTTP over loopback, and send X-Forwarded-Proto $scheme so the app knows the visitor used HTTPS. If the backend sits on a different host across a network you do not control, that hop needs its own protection, either HTTPS to the backend or a private tunnel between the two machines.