[태그:] Nginx

  • Nginx 502 Bad Gateway를 보고 Nginx 설정부터 고치지 않게 된 이유

    Docker로 FastAPI 서비스를 운영하면서 Nginx를 앞단에 두면 사용자의 요청은 애플리케이션으로 바로 들어오지 않는다. 그래서 많은 사람들이 Nginx를 안정적인 서비스를 하기 위해서 서비스 앞단에 설치하고 호출을 받는다.

    내가 사용하던 구조도 크게 보면 이런 형태였다.

    사용자
       ↓
    도메인
       ↓
    Nginx
       ↓
    Docker 내부의 FastAPI
       ↓
    Uvicorn
       ↓
    애플리케이션
    

    처음 이 구조를 만들 때는 Nginx를 단순히 외부 요청을 내부 API로 전달해주는 중간 서버 정도로 생각했다.

    사용자는 80 또는 443 포트로 Nginx에 요청하고, Nginx가 내부 FastAPI가 실행 중인 포트로 요청을 넘겨주면 된다.

    설정도 겉으로 보면 복잡하지 않다.

    대략 이런 형태다.

    location / {
        proxy_pass http://backend:8000;
    }
    

    그래서 어느 날 브라우저에서 다음과 같은 응답을 만나면 자연스럽게 Nginx부터 의심하게 된다.

    502 Bad Gateway
    

    나 역시 처음에는 그랬다.

    화면에 표시되는 서버가 Nginx이고 오류 페이지에도 Nginx가 보이니, nginx.confproxy_pass 설정이 잘못된 것이라고 생각하기 쉬웠다.

    그런데 502라는 상태를 조금 다르게 생각해보니 오히려 중요한 단서가 하나 있었다.

    사용자의 요청이 Nginx까지는 도착했다는 사실이다.

    Nginx가 502를 만들어서 사용자에게 반환했다는 것은 최소한 브라우저에서 Nginx까지의 요청 경로 자체는 동작하고 있었다.

    그렇다면 문제를 처음부터 끝까지 다시 볼 필요는 없었다.

    문제의 범위는 조금 더 좁혀졌다.

    사용자
       ↓
    Nginx        ← 여기까지는 도착
       ↓
    ?
       ↓
    FastAPI
    

    이때부터 내가 먼저 확인해야 할 것은 Nginx 자체가 살아 있는가가 아니라,

    Nginx가 다음 서버, 즉 upstream에 실제로 연결할 수 있는가?

    라는 질문이었다.

    Nginx 공식 문서에서도 upstream은 proxy_pass 등을 통해 요청을 전달할 서버를 의미하고, upstream 서버를 선택하거나 도달할 수 없는 상황에서 Bad Gateway 문제가 발생할 수 있다.

    이걸 이해하고 나니 502 Bad Gateway라는 문구가 이전보다 조금 다르게 보이기 시작했다.

    Nginx가 고장났다는 메시지라기보다,

    “나는 요청을 받았는데 그 다음 서버와 정상적인 응답을 주고받지 못했다”

    라는 신호에 가까웠다.


    처음에는 브라우저에서 FastAPI가 열리는지만 확인했다

    502가 발생했을 때 가장 먼저 확인하기 쉬운 것은 FastAPI가 실제로 살아 있는지다.

    예를 들어 호스트에서 다음 주소가 정상적으로 열린다고 하자.

    http://localhost:8000
    

    또는:

    curl http://localhost:8000/health
    

    를 실행했는데 정상적으로 응답한다.

    그러면 이렇게 생각하기 쉽다.

    FastAPI는 정상인데 왜 Nginx에서는 502가 나오지?

    나도 처음에는 이 테스트가 꽤 강한 증거라고 생각했다.

    호스트에서 FastAPI에 접속할 수 있으니 Nginx에서도 같은 주소로 접근할 수 있을 것이라고 생각하기 때문이다.

    그런데 Docker를 사용하면서 localhost라는 단어를 다시 생각할 필요가 있었다.

    localhost는 항상 같은 컴퓨터를 의미하지 않았다.

    정확히는 현재 그 명령을 실행하는 네트워크 공간의 자기 자신을 가리킨다.

    예를 들어 내 PC나 서버 호스트에서:

    localhost:8000
    

    을 호출하면 호스트 자신을 바라본다.

    하지만 Nginx가 별도의 Docker 컨테이너 안에서 실행 중이라면 그 Nginx 컨테이너에서:

    localhost:8000
    

    은 호스트를 의미하지 않는다.

    Nginx 컨테이너 자신을 의미한다.

    이 차이를 처음 제대로 이해했을 때 Docker 네트워크 오류가 왜 그렇게 헷갈리는지도 조금 이해됐다.


    localhost라는 단어는 같지만 바라보는 곳이 달랐다

    예를 들어 다음과 같은 Docker 구성이 있다고 하자.

    Host Server
    
    ├─ nginx container
    │    └─ localhost
    │
    └─ fastapi container
         └─ localhost:8000
    

    FastAPI 컨테이너 안에서는 Uvicorn이 8000 포트에서 실행되고 있다.

    그런데 Nginx 설정을 이렇게 작성했다고 생각해보자.

    location / {
        proxy_pass http://localhost:8000;
    }
    

    처음에는 그럴듯하다.

    FastAPI가 8000 포트이니 Nginx도 8000으로 보내면 될 것 같다.

    하지만 이 설정에서 Nginx가 바라보는 localhostFastAPI 컨테이너가 아니라 Nginx 컨테이너 자신이다.

    결국 요청 흐름은 내가 생각했던:

    Nginx
       ↓
    FastAPI:8000
    

    이 아니라:

    Nginx
       ↓
    Nginx 자기 자신의 8000번 포트
    

    가 된다.

    그 포트에 아무 서비스도 없다면 당연히 upstream 연결은 실패한다.

    브라우저에서는 FastAPI가 정상적으로 열렸는데 Nginx에서는 계속 502가 발생하는 상황이 충분히 가능해진다.

    Docker Compose에서는 같은 네트워크에 연결된 서비스들이 서비스 이름으로 서로를 찾을 수 있다. Docker 공식 문서에서도 Compose의 기본 네트워크에서 각 서비스가 서비스 이름을 통해 다른 컨테이너에 접근할 수 있다고 설명한다. 예를 들어 db 서비스가 있다면 같은 Compose 네트워크의 다른 서비스가 db:5432처럼 접근할 수 있다.

    그래서 FastAPI 서비스 이름이 backend라면 Nginx에서는 다음과 같은 형태가 더 자연스럽다.

    location / {
        proxy_pass http://backend:8000;
    }
    

    여기서 backend는 컨테이너의 현재 IP를 직접 적은 것이 아니다.

    Compose 서비스 이름이다.

    이 방식이 중요한 이유는 Docker 컨테이너 IP가 영구적인 값이 아니기 때문이다.

    컨테이너가 다시 생성되면 IP가 달라질 수 있지만 Compose 네트워크에서는 같은 서비스 이름으로 다시 찾을 수 있다. Docker도 컨테이너를 IP 대신 서비스 이름으로 참조하도록 권장한다.

    이 사실을 알고 나니 Nginx upstream에 컨테이너 IP를 직접 적는 방식도 다시 생각하게 됐다.


    컨테이너 IP를 직접 적으면 처음에는 잘 되는 것처럼 보일 수 있다

    예를 들어 docker inspect를 해보니 FastAPI 컨테이너 IP가 다음과 같았다고 하자.

    172.18.0.5
    

    그러면 Nginx에:

    proxy_pass http://172.18.0.5:8000;
    

    을 넣으면 당장은 동작할 수 있다.

    그리고 502가 사라지면 문제가 해결됐다고 생각하기 쉽다.

    하지만 컨테이너를 다시 만들었더니 IP가:

    172.18.0.7
    

    로 달라질 수 있다.

    그러면 Nginx는 여전히 이전 IP로 요청을 보내고 있다.

    Nginx
     ↓
    172.18.0.5
     ↓
    이제 FastAPI가 없음
    

    다시 502가 발생한다.

    이 상황을 몇 번 겪으면:

    어제까지 잘 됐는데 Docker를 다시 올리니까 왜 안 되지?

    라는 현상이 만들어진다.

    문제는 애플리케이션 코드가 바뀐 것이 아니라 주소를 고정하는 방법 자체가 Docker의 동작 방식과 맞지 않았던 것이다.

    Docker Compose가 서비스 이름 기반 discovery를 제공하는 이유도 이런 구조에서 이해하기 쉬웠다. 컨테이너가 교체되어 IP가 바뀌어도 서비스 이름은 유지된다.

    이때부터 나는 Docker 환경에서 서버 주소를 볼 때 숫자로 된 IP보다 먼저:

    같은 Docker network인가?

    서비스 이름으로 접근할 수 있는가?

    를 보게 됐다.


    그런데 서비스 이름을 제대로 적어도 502가 날 수 있었다

    여기까지 확인해서 Nginx 설정이 다음처럼 되어 있다고 하자.

    proxy_pass http://backend:8000;
    

    FastAPI 서비스 이름도 실제로 backend다.

    그런데 여전히 502가 발생한다.

    그러면:

    서비스 이름도 맞는데 대체 왜 안 되지?

    라는 생각이 든다.

    여기서 다음으로 확인해야 했던 것이 두 컨테이너가 실제로 같은 Docker network에 있는가였다.

    서비스 이름으로 다른 컨테이너를 찾을 수 있다는 것은 아무 컨테이너끼리나 가능한 것이 아니다.

    같은 네트워크 안에서 서로 발견할 수 있어야 한다.

    예를 들어:

    network A
    
    - nginx
    - backend
    

    라면 서로 통신할 수 있는 구조를 만들 수 있다.

    반면:

    network A
    - nginx
    
    network B
    - backend
    

    처럼 완전히 나뉘어 있다면 단순히 서비스 이름이 맞다는 이유만으로 통신되는 것은 아니다.

    Docker에서는 docker network inspect로 네트워크에 어떤 컨테이너들이 연결되어 있는지 확인할 수 있고, 공식 CLI에서도 이 명령을 네트워크 상세 정보를 확인하는 방법으로 제공한다.

    그래서 지금 같은 문제가 다시 발생하면 단순히 docker ps만 보는 것이 아니라 네트워크부터 확인할 것 같다.

    docker network ls
    

    그리고 사용하는 네트워크를 확인한다.

    docker network inspect <network_name>
    

    여기에서 Nginx와 FastAPI 컨테이너가 둘 다 보이는지 확인한다.

    이 테스트의 의미는 단순하다.

    Nginx가 upstream 주소를 알고 있다는 것과 실제로 그 upstream까지 네트워크 경로가 존재한다는 것은 서로 다른 문제이기 때문이다.


    이쯤에서 나는 502를 네트워크 문제라고 확신하고 싶어졌다

    Nginx.

    Docker network.

    서비스 이름.

    여기까지 확인하다 보면 502는 결국 네트워크 문제라고 생각하기 쉽다.

    그런데 다시 한 가지가 남는다.

    Nginx 컨테이너가 FastAPI 컨테이너까지 갈 수 있다고 해도 FastAPI가 외부 연결을 받을 수 있는 주소에 실제로 bind되어 있어야 한다.

    이 부분 때문에 Uvicorn 실행 옵션도 봐야 했다.

    Uvicorn의 기본 host는 현재 공식 문서 기준 127.0.0.1이고, 다른 네트워크 인터페이스에서도 접근 가능하게 하려면 --host 0.0.0.0을 사용할 수 있다.

    예를 들어 FastAPI 컨테이너에서 이렇게 실행했다고 생각해보자.

    uvicorn main:app --host 127.0.0.1 --port 8000
    

    FastAPI 컨테이너 안에서:

    curl http://127.0.0.1:8000
    

    을 하면 잘 된다.

    그래서 애플리케이션은 정상이라고 생각한다.

    하지만 127.0.0.1에만 bind되어 있다면 다른 컨테이너에서 접근하려는 연결과는 상황이 달라진다.

    Nginx는 다른 컨테이너다.

    그래서 Docker에서 Uvicorn을 실행할 때 흔히 다음처럼 구성한다.

    uvicorn main:app --host 0.0.0.0 --port 8000
    

    Uvicorn의 Docker 배포 문서 예제 역시 --host 0.0.0.0 --port 8000 형태를 사용한다.

    이 지점에서 나는 같은 “FastAPI가 살아 있다”는 말을 두 종류로 나눠야 한다고 느꼈다.

    컨테이너 자기 자신에게는 응답할 수 있다
    

    다른 컨테이너의 연결도 받을 수 있다
    

    는 같은 상태가 아니다.


    결국 브라우저에서 확인한 결과만으로는 부족했다

    처음에는 브라우저에서 FastAPI 주소가 열리면 백엔드는 정상이라고 판단했다.

    하지만 Docker + Nginx 구조에서는 브라우저가 접근하는 경로와 Nginx가 접근하는 경로가 다를 수 있다.

    예를 들어 호스트에서는:

    Host
     ↓
    localhost:8000
     ↓
    Docker published port
     ↓
    FastAPI container
    

    경로로 접근한다.

    Docker에서 포트를 publish하면 호스트의 특정 포트를 컨테이너 포트에 연결할 수 있다. Docker 문서에서도 -p HOST_PORT:CONTAINER_PORT 방식으로 호스트에서 컨테이너 서비스에 접근하도록 하는 구조를 설명한다.

    하지만 Nginx도 Docker 컨테이너라면 굳이 호스트의 published port를 돌아서 갈 필요가 없다.

    같은 network 안에서:

    Nginx container
           ↓
    backend:8000
           ↓
    FastAPI container
    

    로 직접 통신할 수 있다. Compose 네트워크에서 컨테이너 간 통신은 서비스 이름과 컨테이너 포트를 기준으로 하고, 호스트에서 접근할 때는 published host port를 사용한다는 차이가 있다.

    이 차이를 모르면 포트 번호가 굉장히 헷갈린다.


    특히 8080:8000 같은 설정을 처음 볼 때 착각하기 쉬웠다

    Compose에 이런 설정이 있다고 하자.

    ports:
      - "8080:8000"
    

    처음에는 8080이 FastAPI의 포트라고 생각하기 쉽다.

    하지만 두 숫자의 역할이 다르다.

    8080 = Host에서 접근하는 port
    
    8000 = Container 내부에서 애플리케이션이 듣는 port
    

    그래서 호스트에서는:

    localhost:8080
    

    으로 접근할 수 있다.

    그런데 같은 Docker network 안의 Nginx가 FastAPI로 직접 요청한다면 보통:

    backend:8000
    

    처럼 컨테이너가 실제로 듣고 있는 포트를 사용한다.

    Docker Compose 공식 문서에서도 호스트에서는 published host port를 이용하지만 같은 Compose 네트워크의 다른 컨테이너에서는 service name과 container port를 이용하는 예를 보여준다.

    이걸 모르고 Nginx에서:

    proxy_pass http://backend:8080;
    

    처럼 설정하면, 호스트에서는 8080으로 잘 열리는데 컨테이너끼리는 통신하지 못하는 상황을 만들 수 있다.

    그리고 다시 502가 나온다.

    이렇게 보면 같은 502라도 원인이 꽤 다양하다.

    localhost 문제
    
    서비스 이름 문제
    
    Docker network 문제
    
    container port 문제
    
    Uvicorn bind 문제
    

    전부 결과는 Nginx에서 비슷하게 보일 수 있다.

    그래서 502라는 결과만 보고 설정 파일을 계속 고치는 방식이 비효율적이라는 생각이 들었다.


    어느 순간부터는 Nginx 설정을 읽는 대신 Nginx 위치에서 직접 요청해보게 됐다

    이런 문제를 몇 번 생각하다 보니 가장 단순한 테스트가 보였다.

    Nginx가 FastAPI에 연결해야 한다면, Nginx와 같은 위치에서 FastAPI에 직접 요청해보면 된다.

    Nginx도 Docker 안에서 실행 중이라고 하자.

    그렇다면 Nginx 컨테이너에 들어간다.

    docker exec -it nginx_container sh
    

    그리고 FastAPI 서비스로 직접 요청한다.

    curl http://backend:8000/health
    

    여기에서 정상적으로 응답한다면 중요한 사실을 하나 알 수 있다.

    Nginx container
          ↓
    Docker network
          ↓
    backend:8000
    

    경로 자체는 동작한다.

    그렇다면 이제 Nginx proxy_pass나 URI 처리 같은 설정을 좀 더 집중해서 볼 이유가 생긴다.

    반대로 여기에서:

    Could not resolve host
    

    가 나온다면 서비스 이름이나 network부터 볼 수 있다.

    Connection refused
    

    라면 이름 해석은 됐지만 해당 주소/포트에서 서비스를 받을 수 없는 상태를 의심할 수 있다.

    200 OK
    

    가 나온다면 네트워크보다는 Nginx configuration을 더 볼 수 있다.

    나는 이 방식이 특히 좋다고 생각한다.

    왜냐하면 한 번의 테스트로 문제 범위를 확 줄일 수 있기 때문이다.


    Nginx error log를 먼저 보는 이유도 여기 있었다

    502가 발생했을 때 브라우저에는 보통 많은 정보가 나오지 않는다.

    502 Bad Gateway
    

    정도가 전부다.

    이 정보만 가지고는 Nginx가 hostname을 찾지 못했는지, Connection이 거부됐는지, timeout이 발생했는지 알 수 없다.

    그런데 Nginx는 error_log를 통해 오류를 기록할 수 있고, 공식 core module 문서에서도 파일과 로그 레벨을 지정하는 error_log directive를 제공한다.

    그래서 브라우저에서 502를 본 뒤 Nginx 설정을 무작정 바꾸기보다 error log에 어떤 메시지가 남았는지부터 확인하는 것이 훨씬 직접적이다.

    예를 들어 로그가 말해주는 방향은 서로 다를 수 있다.

    connection refused
    

    라면 upstream 주소는 찾았지만 Connection을 만들지 못했을 가능성을 본다.

    host not found
    

    계열이라면 이름 해석이나 network 문제를 본다.

    timeout 관련 메시지가 있다면 upstream이 응답하지 않는 이유를 본다.

    즉 Nginx error log는 단순히 오류 기록이 아니라 요청이 어느 단계까지 갔는지를 알려주는 단서가 된다.

    Access log는 요청 자체가 어떻게 처리되었는지를 기록할 수 있고, Nginx 공식 HTTP log module은 요청 로그를 access_log로 작성하는 기능을 제공한다.


    그런데 Connection이 된다고 502 문제가 모두 끝나는 것도 아니었다

    Nginx 컨테이너 안에서:

    curl http://backend:8000/health
    

    가 잘 된다.

    서비스 이름도 맞다.

    Docker network도 정상이다.

    Uvicorn도 0.0.0.0:8000에서 듣고 있다.

    그런데 특정 API만 Nginx를 통하면 문제가 생긴다면 이제 조금 다른 부분을 봐야 한다.

    이때부터는 연결 자체의 문제에서 HTTP proxy 동작 문제로 범위가 이동한다.

    예를 들어 Nginx의 locationproxy_pass에서 URI가 어떻게 전달되는지 생각해야 할 수 있다.

    다음 두 설정은 얼핏 매우 비슷하게 보인다.

    location /api/ {
        proxy_pass http://backend:8000;
    }
    

    그리고:

    location /api/ {
        proxy_pass http://backend:8000/;
    }
    

    하지만 Nginx proxy_pass는 URI를 포함하는지 여부와 location 구조에 따라 upstream으로 전달되는 요청 URI 처리 방식이 달라질 수 있다. Nginx 공식 proxy module 문서에서 proxy_pass에 URI를 지정한 경우와 지정하지 않은 경우 upstream에 전달되는 request URI가 어떻게 달라지는지를 별도로 설명한다.

    이 차이 때문에:

    사용자 요청
    /api/search
    

    을 보냈는데 FastAPI에서는 내가 예상한:

    /search
    

    가 아니라 다른 path를 받고 있을 수도 있다.

    이 경우에는 FastAPI까지 Connection 자체는 성공한다.

    따라서 network 문제와는 성격이 다르다.

    그래서 나는 지금 502나 proxy 문제를 볼 때 “연결할 수 있는가”와 “올바른 HTTP 요청을 전달하고 있는가”를 별도 단계로 생각한다.


    root_path를 사용하는 서비스라면 경로 문제를 더 조심해야 했다

    내 FastAPI 서비스에서는 애플리케이션이 항상 루트 /에서만 동작하는 형태가 아니라 특정 prefix 아래에서 서비스하는 구조도 사용했다.

    예를 들어 애플리케이션이 /bookq 같은 경로 구조를 가진다면 Nginx가 어떤 URI를 upstream으로 전달하는지가 훨씬 중요해진다.

    사용자가:

    https://example.com/bookq/search
    

    로 요청했는데 Nginx가 FastAPI에는:

    /search
    

    를 보내야 하는 구조인지,

    아니면:

    /bookq/search
    

    를 그대로 보내야 하는지는 FastAPI의 routing과 proxy 구성에 따라 달라진다.

    이걸 제대로 맞추지 않으면 네트워크는 정상이고 Uvicorn도 정상인데 결과가 원하는 API로 가지 않을 수 있다.

    그래서 reverse proxy를 구성할 때는 단순히:

    Nginx가 backend:8000에 연결되는가?
    

    만 보는 것이 아니라:

    사용자의 원래 URI가
    FastAPI에서는 어떤 URI로 보여야 하는가?
    

    까지 생각해야 한다.

    이 지점부터 Nginx는 단순한 포트 전달기가 아니라 요청의 경로와 헤더를 어떻게 upstream에 전달할지를 결정하는 HTTP proxy라는 점이 더 명확해졌다.


    결국 내가 502를 볼 때 가장 먼저 묻게 된 질문은 “어디까지 갔나?”였다

    처음에는 502가 뜨면 Nginx 설정 파일부터 열었다.

    proxy_pass를 바꿔보고,

    포트를 바꿔보고,

    컨테이너를 재시작하고,

    그래도 안 되면 Nginx를 다시 설치해볼 생각까지 할 수 있다.

    그런데 이런 방식에는 문제가 있다.

    현재 어느 단계까지 정상인지 확인하지 않고 여러 곳을 동시에 바꾸고 있다는 것이다.

    지금은 오히려 요청 하나를 머릿속으로 따라간다.

    브라우저
       ↓
    Nginx
       ↓
    Docker DNS
       ↓
    Docker Network
       ↓
    FastAPI Container
       ↓
    Uvicorn Socket
       ↓
    FastAPI Route
    

    그리고 앞에서부터 하나씩 확인한다.

    브라우저에서 502가 나왔다는 것은 Nginx까지는 왔다.

    그다음 Nginx error log를 본다.

    Nginx 컨테이너에서 backend:8000으로 직접 요청한다.

    이름이 해석되는지 본다.

    Connection이 되는지 본다.

    같은 Docker network인지 본다.

    Uvicorn이 0.0.0.0에 bind되어 있는지 본다.

    그리고 여기까지 정상이라면 Nginx의 location과 proxy_pass URI를 본다.

    이 방식으로 바꾸고 나니 502가 더 이상 하나의 큰 문제처럼 느껴지지 않았다.

    하나의 요청이 여러 단계를 지나가다가 어느 한 단계에서 다음으로 넘어가지 못한 결과로 보이기 시작했다.


    Docker에서 Nginx를 사용할 때 호스트 포트를 굳이 거쳐야 하는지도 다시 생각했다

    예전에는 호스트에서 FastAPI가:

    localhost:8000
    

    으로 열리니 Nginx도 그 주소를 사용하는 것이 자연스럽다고 생각하기 쉽다.

    하지만 둘 다 Docker 안에 있다면 네트워크 관점에서는 오히려 돌아가는 경로가 된다.

    Nginx container
       ↓
    Host published port
       ↓
    FastAPI container
    

    보다:

    Nginx container
       ↓
    Docker network
       ↓
    FastAPI service
    

    가 더 직접적이다.

    Compose는 같은 기본 네트워크의 서비스가 서비스 이름으로 서로 접근할 수 있도록 구성한다.

    이걸 이해하고 나니 Docker Compose에서 ports를 언제 사용하는지도 조금 달리 보게 됐다.

    FastAPI를 외부 호스트에서 직접 접근할 필요가 없다면 반드시 모든 backend port를 host에 publish해야 하는 것은 아니다.

    Nginx만 외부에 열고 FastAPI는 내부 Docker network에서만 접근하도록 구성하는 방식도 생각할 수 있다.

    이런 구조에서는 서비스 간 경로가 더 명확해지고 backend port를 외부에 직접 노출하지 않아도 된다.

    물론 실제 운영 요구사항에 따라 구성이 달라질 수 있지만, 적어도:

    Docker 컨테이너끼리 통신하려면 무조건 host port가 필요하다.

    고 생각했던 것은 틀린 접근이었다.


    지금 다시 502를 만난다면 가장 먼저 할 일

    지금 같은 상황을 다시 만나면 nginx.conf를 처음부터 다시 작성하지 않을 것 같다.

    먼저 Nginx error log를 본다.

    그다음 Nginx가 Docker 안에 있다면 컨테이너에 들어간다.

    docker exec -it nginx_container sh
    

    그리고 upstream에 직접 요청한다.

    curl http://backend:8000/health
    

    여기서 실패하면 Nginx configuration보다 앞의 문제다.

    서비스 이름을 확인한다.

    docker compose ps
    

    network를 확인한다.

    docker network inspect <network_name>
    

    FastAPI가 어떤 주소에 bind되어 있는지 확인한다.

    0.0.0.0:8000
    

    인지 본다. Uvicorn은 --host 0.0.0.0을 이용해 로컬 인터페이스 밖에서도 접근 가능한 형태로 bind할 수 있다.

    그리고 upstream 직접 요청이 성공한다면 그때 Nginx의 locationproxy_pass를 본다.

    이 순서가 좋은 이유는 단순하다.

    매 단계에서 문제의 절반 이상을 지울 수 있기 때문이다.


    502와 504도 같은 문제라고 생각하기 쉬웠다

    reverse proxy 오류를 보다 보면 502 Bad Gateway504 Gateway Timeout도 자주 함께 등장한다.

    둘 다 Nginx가 앞에 있고 backend에서 문제가 발생한 것처럼 보이기 때문에 같은 오류처럼 느껴질 수 있다.

    하지만 조사할 때는 의미를 구분하는 것이 도움이 된다.

    upstream에 정상적으로 연결할 수 없거나 유효한 upstream 처리가 되지 않는 경우에는 502 계열 문제를 볼 수 있다. Nginx upstream 문서도 upstream 서버를 선택하거나 도달하지 못하는 상황에서 502를 반환할 수 있음을 설명한다.

    반면 upstream에 요청은 갔지만 응답을 기다리는 과정에서 timeout이 문제가 된다면 조사 방향은 달라진다.

    예를 들어 FastAPI가 내부에서 LLM API 응답을 오래 기다리고 있거나 PostgreSQL Query가 오래 실행되고 있다면 Nginx와 FastAPI의 Connection 자체는 정상인데 backend 처리시간이 너무 긴 문제일 수 있다.

    이때 proxy_read_timeout을 무작정 크게 만드는 방법도 생각할 수 있지만, 이전에 max_connections를 보면서 느꼈던 것과 같은 질문을 다시 하게 된다.

    timeout을 늘리는 것이 원인을 해결하는 것인가, 아니면 느린 backend를 더 오래 기다리게 만드는 것인가?

    설정값을 늘리는 것이 필요한 경우도 있지만 먼저 왜 backend 응답이 그만큼 오래 걸렸는지 보는 것이 순서라고 생각한다.


    이전에는 설정값을 먼저 바꿨지만 이제는 요청의 이동 경로를 먼저 그린다

    Docker와 Nginx를 처음 다루면 설정 파일이 많다.

    nginx.conf.

    Compose 파일.

    Dockerfile.

    Uvicorn 실행 명령.

    FastAPI 설정.

    어디에서 문제가 났는지 모르면 모든 파일이 의심스럽다.

    그래서 하나씩 값을 바꾸게 된다.

    그런데 지금은 문제를 만나면 설정 파일보다 먼저 작은 그림을 그리는 편이 더 낫다고 생각한다.

    예를 들어:

    Internet
       │
       ▼
    Nginx :80
       │
       │ Docker Network
       ▼
    backend:8000
       │
       ▼
    Uvicorn 0.0.0.0:8000
       │
       ▼
    FastAPI
    

    그리고 각 선마다 질문 하나를 붙인다.

    Internet → Nginx
    정상인가?
    
    Nginx → backend
    이름을 찾을 수 있는가?
    
    backend:8000
    Connection이 되는가?
    
    Uvicorn
    외부 인터페이스에 bind되어 있는가?
    
    FastAPI
    요청한 route가 존재하는가?
    

    이렇게 만들어 놓으면 502라는 큰 오류가 여러 개의 작은 확인 작업으로 바뀐다.

    개인적으로 Docker와 reverse proxy 문제에서는 이 방식이 설정 예제를 외우는 것보다 훨씬 도움이 됐다.


    마무리

    처음 Nginx에서 502 Bad Gateway를 봤을 때는 Nginx 자체가 문제라고 생각하기 쉬웠다.

    화면에 Nginx가 표시되고 Nginx가 오류를 반환했기 때문이다.

    하지만 reverse proxy 구조를 다시 생각해보면 502는 오히려 한 가지 중요한 정보를 준다.

    사용자의 요청은 Nginx까지 도착했다.

    그렇다면 그 다음 경로를 보면 된다.

    Nginx가 upstream 주소를 찾을 수 있는가.

    Docker network가 연결되어 있는가.

    localhost를 잘못 사용하고 있지는 않은가.

    호스트 포트와 컨테이너 포트를 혼동하고 있지는 않은가.

    FastAPI가 실제로 0.0.0.0에 bind되어 있는가.

    그리고 이 모든 것이 정상이라면 Nginx가 요청 URI를 원하는 형태로 전달하고 있는지 확인한다.

    처음에는:

    502
    ↓
    Nginx 설정 오류
    

    라고 생각했다.

    지금은:

    502
    ↓
    Nginx는 요청을 받았다
    ↓
    upstream까지 어느 단계에서 막혔을까?
    

    라고 생각한다.

    이 작은 차이 때문에 확인 순서가 완전히 달라졌다.

    그리고 Docker 환경에서는 특히 localhost라는 익숙한 주소가 문제를 더 어렵게 만들 수 있었다.

    호스트의 localhost와 Nginx 컨테이너의 localhost와 FastAPI 컨테이너의 localhost는 서로 같은 공간이 아니다.

    그래서 컨테이너끼리 통신할 때는 Compose가 제공하는 network와 service name을 이용하는 구조를 먼저 생각하게 됐다. Docker Compose는 같은 네트워크의 서비스를 이름으로 발견할 수 있도록 하고, 컨테이너가 교체되어 IP가 변경되더라도 서비스 이름을 기준으로 연결하도록 설계되어 있다.

    결국 이 문제를 겪으면서 얻은 가장 큰 교훈은 Nginx 설정 문법 자체가 아니었다.

    502를 보면 설정값을 고치기 전에 요청이 실제로 어디까지 이동했는지를 먼저 확인하는 것.

    Nginx에서 FastAPI까지의 경로를 단계별로 확인할 수 있으면 502는 더 이상 막연한 서버 오류가 아니다.

    어느 선 하나가 끊어진 문제로 바뀐다.

    그리고 그 선이 어디인지 찾은 뒤에 설정을 수정하는 편이, 여러 설정을 동시에 바꾸면서 우연히 문제가 사라지기를 기다리는 것보다 훨씬 설명 가능한 해결 방법이라고 생각한다.