[태그:] localhost

  • Docker 컨테이너끼리 통신할 때 localhost가 안 되는 이유를 뒤늦게 이해했다

    Docker를 처음 사용할 때 가장 헷갈렸던 것 중 하나가 네트워크였다.

    이미지와 컨테이너의 차이는 어느 정도 이해했고, docker run, docker ps, docker logs 같은 명령어도 사용하고 있었지만 여러 컨테이너를 서로 연결하기 시작하면 갑자기 이해하기 어려운 문제가 생겼다.

    특히 FastAPI, Nginx, PostgreSQL처럼 여러 서비스를 각각 컨테이너로 분리해서 실행할 때 그랬다.

    처음에는 모든 컨테이너가 결국 같은 서버 안에서 실행되고 있으니 서로 연결할 때도 평소처럼 localhost를 사용하면 될 것이라고 생각했다.

    예를 들어 FastAPI에서 PostgreSQL에 연결한다고 하면 별다른 의심 없이 이런 주소를 떠올리기 쉽다.

    postgresql://user:password@localhost:5432/database
    

    PostgreSQL도 같은 서버에 있고 FastAPI도 같은 서버에 있으니, localhost:5432로 연결하는 것이 너무 자연스러워 보였다.

    그런데 FastAPI와 PostgreSQL을 각각 Docker 컨테이너로 실행하자 연결이 되지 않았다.

    처음에는 PostgreSQL 설정을 의심했다. 계정이나 비밀번호를 잘못 입력했는지 확인했고, PostgreSQL이 정말 5432 포트에서 실행되고 있는지도 확인했다.

    호스트에서 접속해보면 PostgreSQL은 정상적으로 동작하고 있었다.

    그러면 더 이상해진다.

    같은 서버에서 PostgreSQL은 잘 열리는데 왜 FastAPI 컨테이너에서는 localhost:5432로 연결할 수 없는 것일까?

    이 질문을 따라가면서 Docker에서 localhost라는 주소를 내가 잘못 이해하고 있었다는 것을 알게 됐다.


    같은 서버에서 실행된다는 사실과 같은 네트워크 공간에 있다는 것은 달랐다

    Docker 공식 문서를 보면 컨테이너는 애플리케이션의 각 구성요소를 서로 격리된 환경에서 실행한다. 포트를 publish하는 것도 이 격리된 환경의 서비스를 호스트에서 접근할 수 있도록 연결해주는 과정이다.

    처음에는 이 격리를 주로 파일이나 프로세스 관점에서 생각했다.

    컨테이너 A에서 설치한 패키지가 컨테이너 B에 자동으로 생기지 않고, 각각 독립적인 파일 시스템을 가진다는 정도로 이해했다.

    그런데 네트워크도 마찬가지였다.

    컨테이너마다 자신의 네트워크 환경이 있기 때문에 컨테이너 안에서 localhost라고 하면 그 컨테이너 자기 자신을 가리킨다.

    구조를 머릿속으로 그려보면 이해하기 쉬웠다.

    Docker Host
    
     ├─ FastAPI Container
     │      └─ localhost
     │
     ├─ PostgreSQL Container
     │      └─ localhost
     │
     └─ Nginx Container
            └─ localhost
    

    세 컨테이너 모두 같은 물리 서버 안에서 실행되고 있지만 각각의 localhost가 바라보는 대상은 다르다.

    FastAPI 컨테이너에서:

    localhost:5432
    

    라고 요청하면 PostgreSQL 컨테이너를 찾는 것이 아니다.

    FastAPI 컨테이너 자기 자신의 5432 포트를 찾는다.

    FastAPI 컨테이너 안에서 PostgreSQL이 실행되고 있지 않다면 당연히 연결되지 않는다.

    여기까지 이해하고 나니 이전에 Nginx에서 localhost:8000으로 FastAPI를 연결하려다가 실패하는 상황도 같은 문제라는 것이 보였다.

    Nginx 컨테이너의 localhost:8000 역시 FastAPI 컨테이너가 아니라 Nginx 컨테이너 자신이었다.

    처음에는 PostgreSQL 연결 문제와 Nginx 502 문제가 서로 다른 문제처럼 보였는데, 네트워크 구조를 알고 나니 둘 다 같은 착각에서 시작할 수 있었다.


    그렇다면 다른 컨테이너를 어떻게 찾아야 할까

    localhost가 자기 자신이라면 다음 질문이 생긴다.

    FastAPI 컨테이너는 PostgreSQL 컨테이너의 주소를 어떻게 알아낼까?

    처음에는 컨테이너 IP를 직접 찾아서 사용하면 된다고 생각했다.

    docker inspect postgres
    

    같은 명령으로 IP를 확인한 뒤:

    172.18.0.3:5432
    

    처럼 연결하면 실제로 동작할 수도 있다.

    그러면 문제가 해결된 것처럼 느껴진다.

    하지만 이 방법을 계속 사용하기 어려운 이유가 금방 나타난다.

    컨테이너는 영구적으로 유지되는 서버 한 대가 아니다.

    컨테이너를 지우고 다시 만들면 새로운 컨테이너가 생성되고 네트워크 주소가 달라질 수 있다.

    Docker Compose 공식 문서에서도 컨테이너가 업데이트되어 새 컨테이너로 교체되면 IP 주소는 달라질 수 있지만 서비스 이름은 그대로 유지되며, 다른 컨테이너가 서비스 이름으로 다시 연결하도록 설명한다.

    이걸 보고 나니 IP를 직접 저장하는 방식은 Docker가 제공하는 환경을 제대로 활용하지 않는 방법이라는 생각이 들었다.

    내가 필요한 것은:

    PostgreSQL의 현재 IP가 무엇인가?

    가 아니라,

    PostgreSQL이라는 서비스를 항상 같은 이름으로 찾을 수 있는가?

    였다.

    Docker Compose에서는 바로 이 역할을 서비스 이름이 해준다.


    Compose의 서비스 이름을 주소처럼 사용할 수 있다는 것이 처음에는 낯설었다

    예를 들어 Compose 파일이 다음과 같다고 생각해보자.

    services:
      api:
        build: ./api
    
      postgres:
        image: postgres:16
        environment:
          POSTGRES_USER: app
          POSTGRES_PASSWORD: password
          POSTGRES_DB: appdb
    

    Compose는 기본적으로 애플리케이션용 네트워크 하나를 만들고 각 서비스를 그 네트워크에 연결한다. 같은 네트워크에 속한 서비스는 서비스 이름으로 서로를 찾을 수 있다. Docker 문서에서는 각 서비스가 기본 네트워크에 참여하고 Docker 내부 DNS에 자신의 서비스 이름을 등록하기 때문에 다른 컨테이너가 서비스 이름으로 접근할 수 있다고 설명한다.

    그래서 FastAPI에서 PostgreSQL을 찾을 때:

    localhost
    

    대신:

    postgres
    

    를 사용할 수 있다.

    연결 문자열도 다음처럼 달라진다.

    postgresql://app:password@postgres:5432/appdb
    

    처음에는 postgres라는 문자가 어떻게 실제 서버 주소가 될 수 있는지 낯설었다.

    DNS라고 하면 보통:

    google.com
    naver.com
    

    같은 인터넷 도메인부터 생각했기 때문이다.

    그런데 Docker도 사용자 정의 네트워크에서 자체 DNS를 제공한다. Docker 네트워킹 문서에 따르면 custom network에 연결된 컨테이너는 Docker의 embedded DNS를 사용하며, 같은 네트워크에 있는 컨테이너를 이름으로 찾을 수 있다.

    결국 구조는 이런 식이었다.

    FastAPI Container
    
    postgres:5432
          ↓
    Docker DNS
          ↓
    PostgreSQL Container IP
          ↓
    5432
    

    FastAPI는 PostgreSQL 컨테이너의 실제 IP를 알 필요가 없다.

    postgres라는 이름만 알고 있으면 Docker가 현재 주소를 찾아준다.

    이걸 이해하면서부터 컨테이너 IP를 직접 확인해서 설정 파일에 넣는 방식은 거의 필요하지 않다는 생각이 들었다.


    그런데 서비스 이름을 사용했는데도 연결이 안 되는 경우가 있었다

    여기까지 이해하면 Docker 네트워크가 굉장히 단순해진 것처럼 느껴진다.

    localhost 대신 service name을 사용하면 된다.

    하지만 실제로는 한 가지 조건이 더 있다.

    두 컨테이너가 서로 통신할 수 있는 같은 네트워크에 있어야 한다.

    Docker Compose에서 별도의 네트워크 설정을 하지 않으면 보통 프로젝트용 기본 네트워크를 생성하고 서비스들을 여기에 연결한다. 그래서 하나의 Compose 파일에서 단순한 구조를 사용한다면 의식하지 않아도 서비스 이름 통신이 동작하는 경우가 많다.

    문제는 프로젝트가 조금 복잡해지면서 네트워크를 직접 나누기 시작할 때다.

    예를 들어 이렇게 구성할 수 있다.

    services:
      nginx:
        image: nginx
        networks:
          - frontend
    
      api:
        build: ./api
        networks:
          - frontend
          - backend
    
      postgres:
        image: postgres:16
        networks:
          - backend
    
    networks:
      frontend:
      backend:
    

    여기에서는 의도가 분명하다.

    Nginx는 FastAPI에는 접근할 수 있다.

    nginx
      ↓
    frontend network
      ↓
    api
    

    FastAPI는 PostgreSQL에도 접근할 수 있다.

    api
     ↓
    backend network
     ↓
    postgres
    

    하지만 Nginx와 PostgreSQL은 같은 네트워크를 공유하지 않는다.

    nginx
      X
    postgres
    

    Docker Compose 문서에서도 여러 네트워크를 정의한 경우 서비스가 실제로 해당 네트워크에 접근하려면 각 서비스의 networks 설정을 통해 연결해야 한다고 설명한다.

    이 구조를 보고 나니 네트워크 분리가 단순한 설정 복잡도가 아니라 어떤 서비스끼리 직접 대화할 수 있도록 허용할 것인가를 결정하는 구조라는 생각이 들었다.

    DB는 API와 통신하면 된다.

    굳이 Nginx가 PostgreSQL에 직접 접근할 이유는 없다.

    그렇다면 애초에 같은 네트워크에 넣지 않는 편이 구조를 더 명확하게 만들 수 있다.


    내가 처음 헷갈렸던 것은 ports였다

    Docker Compose에서 다음 설정을 자주 사용한다.

    ports:
      - "5433:5432"
    

    처음 보면 숫자가 두 개라서 헷갈린다.

    그리고 어느 순간부터 5433이 PostgreSQL 포트인지 5432가 PostgreSQL 포트인지 머릿속에서 섞이기 시작한다.

    Docker에서 port publishing은 다음 구조를 가진다.

    HOST_PORT:CONTAINER_PORT
    

    Docker 공식 문서에서도 -p HOST_PORT:CONTAINER_PORT 형태로 호스트 포트를 컨테이너 내부 포트로 전달한다고 설명한다.

    따라서:

    ports:
      - "5433:5432"
    

    라면 의미는:

    Host 5433
        ↓
    Container 5432
    

    다.

    호스트 PC에서 PostgreSQL에 접속한다면:

    localhost:5433
    

    을 사용할 수 있다.

    그런데 FastAPI와 PostgreSQL이 같은 Docker 네트워크 안에 있다면 FastAPI가 사용할 주소는 보통 다르다.

    postgres:5432
    

    다.

    여기서 나는 처음으로 호스트가 접근하는 방법과 컨테이너끼리 접근하는 방법을 분리해서 생각해야 한다는 것을 제대로 이해했다.


    localhost:5433이 호스트에서는 되는데 컨테이너에서는 안 되는 이유도 여기 있었다

    호스트에서:

    psql -h localhost -p 5433
    

    을 실행하니 PostgreSQL에 잘 연결된다.

    그러면 FastAPI 환경변수에도 다음처럼 넣고 싶어진다.

    DB_HOST=localhost
    DB_PORT=5433
    

    호스트에서 확인까지 했으니 당연히 될 것 같다.

    그런데 FastAPI가 Docker 컨테이너 안에 있다면 이야기가 달라진다.

    여기에서 localhost는 다시 FastAPI 컨테이너 자신이고, 5433은 호스트에 공개하기 위해 만든 포트다.

    FastAPI와 PostgreSQL이 같은 Docker network에 있다면 굳이 호스트를 한번 거쳐서 PostgreSQL에 들어갈 이유가 없다.

    잘못 생각하기 쉬운 경로
    
    FastAPI Container
          ↓
    localhost:5433
          ↓
    ?
    
    실제 원하는 경로
    
    FastAPI Container
          ↓
    postgres:5432
          ↓
    PostgreSQL Container
    

    Docker Compose 공식 문서에서도 같은 network의 서비스 간 통신은 service name과 container port를 사용하고, host machine에서 접근할 때는 published host port를 사용한다고 구분해서 설명한다.

    이 구분을 알고 나니 이전에는 서로 다른 포트를 마구 섞어 사용하고 있었다는 것을 알 수 있었다.


    여기서 중요한 생각 하나가 바뀌었다

    처음에는 ports가 있어야 컨테이너끼리 서로 통신할 수 있다고 생각하기 쉬웠다.

    예를 들어 PostgreSQL에:

    ports:
      - "5432:5432"
    

    가 없다면 FastAPI도 PostgreSQL에 접근하지 못할 것처럼 느껴진다.

    그런데 같은 Docker 네트워크에서 서비스끼리 통신하는 것과 호스트에 포트를 publish하는 것은 별개의 문제였다.

    ports는 주로 호스트 또는 외부에서 컨테이너 서비스로 들어올 수 있도록 포트를 공개하는 역할을 한다. 같은 Compose 네트워크 안에서는 서비스가 서비스 이름과 컨테이너 포트를 이용해 직접 통신할 수 있다.

    그렇다면 PostgreSQL을 외부에서 직접 접근할 필요가 없는 운영 구조에서는 DB 포트를 무조건 호스트에 공개해야 할 이유도 줄어든다.

    예를 들어:

    services:
      api:
        build: ./api
    
      postgres:
        image: postgres:16
    

    처럼 동일한 Compose network에 있다면 API는:

    postgres:5432
    

    로 접근할 수 있다.

    이 구조를 이해하면서 ports는 컨테이너 간 통신을 위한 필수 설정이 아니라 네트워크 경계를 넘어 호스트 쪽으로 서비스를 공개할 필요가 있을 때 사용하는 설정이라고 생각하게 됐다.

    Docker도 published port는 기본적으로 호스트의 모든 네트워크 인터페이스에 공개될 수 있으므로 민감한 서비스를 publish할 때 주의하라고 안내하고 있다.

    PostgreSQL처럼 외부에 직접 공개할 필요가 없는 서비스라면 이 차이를 알고 있는 것이 보안 측면에서도 의미가 있었다.


    문제를 만났을 때는 주소를 바꾸기 전에 먼저 “어디에서 실행 중인가”를 보게 됐다

    예전에는 Connection refused가 나타나면 주소와 포트부터 이것저것 바꿔봤다.

    localhost:5432
    
    127.0.0.1:5432
    
    localhost:5433
    
    172.18.0.3:5432
    

    그중 하나가 우연히 연결되면 그대로 사용하는 식이었다.

    하지만 이렇게 해결하면 다음에 환경이 바뀌었을 때 왜 그 주소여야 했는지를 설명하지 못한다.

    지금은 Connection 문제가 생기면 주소를 보기 전에 먼저 요청하는 프로그램이 어디에서 실행되고 있는지부터 생각한다.

    FastAPI가 호스트에서 직접 실행되는가?

    아니면 Docker 컨테이너 안에서 실행되는가?

    PostgreSQL은 호스트에 설치되어 있는가?

    별도의 Docker 컨테이너인가?

    두 컨테이너가 같은 network인가?

    이 질문을 먼저 정리하면 사용할 주소도 자연스럽게 결정된다.

    예를 들어 둘 다 같은 Compose network에 있다면:

    FastAPI Container
           ↓
    postgres:5432
           ↓
    PostgreSQL Container
    

    이라고 생각한다.

    FastAPI는 호스트에서 실행되고 PostgreSQL만 Docker라면:

    Host FastAPI
           ↓
    localhost:<published port>
           ↓
    PostgreSQL Container
    

    형태가 될 수 있다.

    즉 주소 자체를 외우는 것이 아니라 요청이 어느 네트워크 경계를 넘어가는지를 먼저 보는 것이다.

    이 방식으로 생각하니 Docker 네트워크 문제가 이전보다 훨씬 덜 막연해졌다.


    네트워크가 맞는지 확인할 때도 docker ps만으로는 부족했다

    컨테이너가 둘 다 실행 중이라는 것은 서로 통신할 수 있다는 뜻이 아니다.

    docker ps
    

    에서는 다음처럼 모두 정상으로 보일 수 있다.

    nginx      Up
    api        Up
    postgres   Up
    

    그런데 서로 다른 network에 들어 있다면 통신이 되지 않을 수 있다.

    그래서 실제 network 구성을 봐야 한다.

    docker network ls
    

    그리고 특정 network를 확인한다.

    docker network inspect <network_name>
    

    Docker는 docker network inspect를 네트워크의 상세 정보를 확인하는 공식 명령으로 제공한다.

    이 결과를 보면 내가 예상한 컨테이너들이 실제로 같은 network에 연결되어 있는지 확인할 수 있다.

    예전에는 Compose YAML을 보고:

    설정상 같은 network니까 당연히 연결됐겠지.

    라고 생각했지만, 장애 상황에서는 실행 중인 실제 상태를 보는 편이 더 확실했다.

    설정 파일은 내가 의도한 구조를 보여준다.

    docker network inspect현재 실제로 만들어져 있는 구조를 보여준다.

    이 차이는 문제를 추적할 때 꽤 중요했다.


    이제는 DNS 확인과 포트 확인도 별개로 본다

    FastAPI 컨테이너에서 다음과 같이 실행한다고 하자.

    getent hosts postgres
    

    정상적으로 IP가 나온다면 적어도 Docker DNS가 postgres라는 이름을 해석하고 있다는 뜻이다.

    하지만 이것만으로 PostgreSQL 연결까지 성공한다는 뜻은 아니다.

    다음 단계가 남아 있다.

    해당 IP의 5432 포트에서 PostgreSQL이 실제로 연결을 받고 있어야 한다.

    즉:

    postgres라는 이름을 찾을 수 있다
    

    와:

    postgres:5432로 연결할 수 있다
    

    는 다른 검사다.

    이 구분도 중요했다.

    이름을 찾지 못한다면 network나 service discovery를 본다.

    이름은 찾는데 Connection refused라면 PostgreSQL 프로세스나 포트, listen 설정 등을 확인할 수 있다.

    연결은 되는데 인증 오류가 난다면 이제 네트워크를 벗어나 DB 사용자와 비밀번호, 권한을 본다.

    결국 동일한 “DB 연결 실패”에서도 어느 단계에서 실패했는지에 따라 조사 위치가 달라진다.


    Docker 네트워크 문제를 가장 빨리 줄이는 방법은 실제 요청을 보내는 것이었다

    설정 파일을 오래 들여다보는 것보다 내가 지금 더 선호하는 방식은 요청을 보내는 컨테이너 안에서 실제 목적지에 접속해보는 것이다.

    예를 들어 FastAPI가 PostgreSQL에 연결하지 못한다고 하자.

    FastAPI 컨테이너에 들어간다.

    docker exec -it api_container sh
    

    그리고 그 위치에서 postgres라는 이름이 해석되는지 본다.

    필요한 클라이언트가 있다면 실제 DB 연결도 시도할 수 있다.

    Nginx가 FastAPI에 접근하지 못한다면 Nginx 컨테이너에 들어가:

    curl http://api:8000/health
    

    를 실행한다.

    이 방식이 좋은 이유는 실제 애플리케이션과 거의 같은 네트워크 관점에서 테스트하고 있기 때문이다.

    호스트에서 curl localhost:8000이 성공한다는 것보다 Nginx 컨테이너에서 curl api:8000이 성공한다는 사실이 Nginx → FastAPI 경로를 확인하는 데 훨씬 직접적인 증거다.

    이걸 알고 나니 Docker 네트워크 문제에서 호스트 테스트만 반복하는 일이 줄었다.


    처음에는 네트워크 이름이 귀찮은 설정이라고 생각했다

    Docker Compose에서는 필요하면 네트워크를 직접 이름 붙여 구성할 수 있다.

    처음에는 이것이 Compose 파일만 복잡하게 만든다고 생각하기 쉬웠다.

    컨테이너가 서로 통신만 하면 되는데 굳이:

    networks:
      backend:
    

    같은 것을 따로 적을 이유가 없어 보인다.

    하지만 여러 애플리케이션이나 Nginx가 함께 동작하는 환경에서는 네트워크가 서비스 구조를 표현하는 방법이 될 수 있었다.

    예를 들어:

    public network
        │
      Nginx
        │
        ├──────── FastAPI
                   │
              backend network
                   │
              PostgreSQL
    

    라고 구성하면 요청 흐름이 더 명확해진다.

    Nginx는 외부 요청을 받을 수 있고 FastAPI와 통신한다.

    PostgreSQL은 FastAPI와 통신하지만 굳이 Nginx나 외부와 직접 통신할 필요가 없다.

    Docker Compose의 networks 설정도 여러 서비스를 특정 네트워크에 명시적으로 연결해 서비스 간 통신 범위를 구성할 수 있도록 제공된다.

    이 관점으로 보면 network는 단순히 컨테이너가 서로 보이게 만드는 기능이 아니라 서비스 사이의 통신 관계를 표현하는 경계가 된다.


    그리고 이 구조를 이해하니 Nginx 502도 훨씬 설명하기 쉬워졌다

    앞 글에서 Nginx 502를 다루면서 Nginx가 FastAPI upstream에 연결하지 못하는 여러 상황을 봤다.

    그때 가장 헷갈리기 쉬웠던 부분 중 하나도 Docker network였다.

    Nginx 설정이:

    proxy_pass http://api:8000;
    

    으로 정확해도 Nginx와 api가 같은 network에 없다면 원하는 경로가 만들어지지 않는다.

    반대로 network가 같고 Docker DNS에서 api라는 이름을 찾을 수 있고 api:8000에서 Uvicorn이 정상적으로 요청을 받고 있다면 Nginx 설정 문제의 범위를 훨씬 좁힐 수 있다.

    결국 Nginx 502와 PostgreSQL 연결 실패는 겉으로는 완전히 다른 장애다.

    하지만 Docker 관점에서는 이런 질문으로 연결된다.

    요청을 보내는 컨테이너가 목적지 컨테이너를 실제로 찾고 접근할 수 있는가?

    이 질문 하나가 해결되면 그다음부터는 애플리케이션별 문제로 들어갈 수 있다.


    컨테이너가 재생성돼도 설정을 바꾸지 않아도 되는 구조가 결국 더 편했다

    IP를 직접 사용하는 방식과 서비스 이름을 사용하는 방식의 차이는 처음에는 별것 아닌 것처럼 느껴졌다.

    IP를 알고 있으면 그냥 연결하면 된다.

    하지만 Docker의 장점 중 하나는 컨테이너를 쉽게 교체하고 다시 만들 수 있다는 점이다.

    그런 환경에서 컨테이너 주소를 IP에 의존하면 컨테이너 교체가 곧 설정 변경으로 이어질 수 있다.

    반면:

    postgres:5432
    
    api:8000
    

    처럼 서비스 이름을 사용하면 컨테이너가 교체되어 내부 IP가 달라져도 애플리케이션 설정은 그대로 유지할 수 있다. Compose 문서에서도 컨테이너 업데이트 시 기존 컨테이너가 제거되고 다른 IP를 가진 새 컨테이너가 네트워크에 참여하지만 이름은 유지되므로 서비스 이름으로 다시 연결하도록 설명한다.

    이 부분을 이해하고 나서야 service name을 사용하는 것이 단순히 주소를 예쁘게 만드는 방식이 아니라 동적으로 교체되는 컨테이너를 대상으로 안정적으로 연결할 수 있도록 하는 추상화라는 생각이 들었다.


    그래서 지금은 localhost를 보면 먼저 “누구의 localhost인가?”라고 묻게 된다

    Docker를 사용하기 전에는 localhost라는 주소에 별다른 의문이 없었다.

    현재 내가 사용하는 컴퓨터를 의미한다고 생각하면 대부분 맞았다.

    하지만 여러 컨테이너로 서비스를 나누고 나서는 localhost를 보는 순간 한 가지 질문을 먼저 한다.

    이 코드를 실행하는 프로세스는 지금 어디에 있는가?

    FastAPI 컨테이너 안이라면 FastAPI 컨테이너의 localhost다.

    Nginx 컨테이너 안이라면 Nginx 컨테이너의 localhost다.

    PostgreSQL 컨테이너 안이라면 PostgreSQL 컨테이너의 localhost다.

    호스트 터미널에서 실행한다면 그제야 호스트의 localhost다.

    이 질문 하나를 먼저 하면 Docker 연결 문제 상당수를 이해하기 쉬워진다.

    그리고 그다음 질문도 자연스럽게 이어진다.

    목적지가 다른 컨테이너라면 같은 network인가?

    그 서비스 이름은 무엇인가?

    그 서비스가 실제로 듣고 있는 container port는 무엇인가?

    호스트에 publish된 port와 혼동하고 있지는 않은가?

    예전에는 이것들을 각각 외워야 하는 Docker 설정이라고 생각했다.

    지금은 모두 한 요청이 어떤 네트워크 공간에서 어디로 이동하는지를 설명하기 위한 정보라고 생각한다.


    결국 Docker 네트워크를 작은 사설 네트워크처럼 생각하니 이해하기 쉬웠다

    내가 Docker network를 가장 편하게 이해하게 된 방법은 여러 컨테이너가 들어 있는 작은 내부 네트워크를 상상하는 것이었다.

    예를 들면 다음과 같다.

    Docker Backend Network
    
     ┌─────────────────────────────┐
     │                             │
     │  api:8000                   │
     │      │                      │
     │      │                      │
     │      ├──── postgres:5432    │
     │      │                      │
     │      └──── elasticsearch    │
     │                             │
     └─────────────────────────────┘
    

    각 서비스에는 이 네트워크 안에서 사용할 이름이 있다.

    Docker가 그 이름을 실제 주소로 찾아준다.

    그리고 호스트나 인터넷에서 이 내부 네트워크로 들어와야 할 때만 port publishing이나 Nginx 같은 경계를 사용한다.

    이 그림으로 생각하기 시작하니:

    왜 localhost가 안 되는가?
    

    라는 질문뿐 아니라:

    왜 service name을 사용하는가?
    
    왜 container IP를 직접 쓰지 않는가?
    
    왜 host port와 container port가 다른가?
    
    왜 network가 다르면 이름이 맞아도 연결되지 않는가?
    

    까지 하나의 원리로 설명할 수 있었다.


    마무리

    Docker 컨테이너끼리 연결이 되지 않았을 때 처음에는 주소와 포트를 계속 바꿔보는 방식으로 해결하려고 했다.

    localhost.

    127.0.0.1.

    호스트 IP.

    컨테이너 IP.

    published port.

    container port.

    그중 하나가 동작하면 해결됐다고 생각했다.

    하지만 그렇게 해결하면 왜 그 주소가 맞았는지를 이해하지 못한다.

    그리고 컨테이너가 다시 만들어지거나 배포 구조가 달라졌을 때 같은 문제를 다시 만난다.

    지금은 연결 문제를 만나면 주소보다 먼저 요청을 보내는 프로세스가 어디에서 실행되고 있는지를 확인한다.

    컨테이너 내부에서 localhost는 그 컨테이너 자신이다.

    다른 컨테이너를 찾으려면 서로 통신할 수 있는 network가 필요하다.

    Docker Compose에서는 같은 네트워크에 참여한 서비스를 service name으로 발견할 수 있다.

    그리고 같은 Docker network 안의 컨테이너끼리 통신할 때는 일반적으로 host에 publish한 port가 아니라 상대 컨테이너가 실제로 듣고 있는 container port를 사용한다. Host port는 호스트에서 격리된 컨테이너 서비스로 접근하기 위한 별도의 경계다.

    이 구조를 알고 난 뒤에는 localhost가 안 된다는 현상도 더 이상 이상하게 느껴지지 않았다.

    오히려 Docker가 컨테이너를 제대로 격리하고 있기 때문에 당연히 그렇게 동작하는 것이었다.

    결국 내가 잘못 알고 있었던 것은 Docker 명령어 하나가 아니었다.

    “같은 서버에 있다”와 “같은 네트워크 주소 공간에 있다”를 같은 뜻으로 생각하고 있었던 것이 문제였다.

    이 둘을 구분하고 나니 Docker 네트워크는 외워야 하는 복잡한 설정에서, 요청의 출발지와 목적지를 그려보면 이해할 수 있는 구조로 바뀌었다.

    그래서 지금은 컨테이너 연결 오류를 볼 때 localhost부터 바꾸지 않는다.

    먼저 이렇게 묻는다.

    지금 이 localhost는 누구인가?

    그 질문에 답하고 나면 다음에 확인할 네트워크와 주소도 자연스럽게 보이기 시작한다.