[Pro Django] 깡통장고 도커 컴포즈 + Nginx 배포: 디버깅 위주

Saemi An·2025년 5월 13일
post-thumbnail

⚠️ 환경변수 적용 오류: connection to server at "localhost" (127.0.0.1), port 5432 failed: Connection refused

app_1  | connection to server at "localhost" (127.0.0.1), port 5432 failed: Connection refused
app_1  |        Is the server running on that host and accepting TCP/IP connections?

🤡 뜻

app 컨테이너가 db 컨테이너 접속 시도시 호스트 주소를 localhost로 참조하여 연결에 실패함. 즉, localhost:5432 형식으로 PostgreSQL에 접속하려함.

  • 이때 localhost = 각 컨테이너 내부 = 현재 맥락에서 app 컨테이너
  • 따라서 앱컨테이너 내부에서 5432 포트로 db 연결을 시도하니 실패함
  • docker-compose.yml > services > app > environment: CORE_SETTINGS_DATABASES: '{"default":{"HOST":"db"}}' 에 따르면 db:5432가 되어야함.

🤡 원인

docker-compose.yml > services > app > environment: CORE_SETTINGS_DATABASES: '{"default":{"HOST":"db"}}' 에 따르면 db:5432가 되어야함.

즉, docker-compose.yml 파일의 환경변수가 제대로 적용되지 않고 있음.

🤡 디버깅 1차: envvar.py

docker-compose에 정의된 환경변수가 잘 들어오는지 확인을 위해
envvar.py에 print(f'envvars.py에서 결과: {get_settings_from_environment(ENVVAR_SETTINGS_PREFIX)}') 넣어보았다.

envvar.py에서는 글로벌 환경변수들을 모두 읽어온 뒤 -> arg1
prefix를 뗀 부분을 키로, 값은 파이썬 값으로 변환한 딕셔너리를 -> arg2
deep_update() 해주고 있다.

  • deep_update(arg1, arg2) - 원본 환경변수 딕셔너리인 arg1에 새로운 환경변수 딕셔너리 arg2를 업데이트 해주는 함수

출력 결과 '{"default":{"HOST":"db"}}' 가 잘 들어온 것을 확인했다;

(사실 여기 확인한 순간 디버깅 완료 였어야 하나 더 삽질하고 들어감 😗)

🤡 디버깅 2차: settings/__init__.py

settings/__init__.py에서는 외부 django-split-setting 패키지를 활용해 여기저기에 있는 세팅파일들을 다음과 같이 통합하고 있다.

# 다수의 설정파일을 종합 (순서 중요)
include('base.py', 'custom.py', optional(LOCAL_SETTINGS_PATH), 'envvars.py', 'docker.py')

이때 base.py에서 DATABASE 설정은 (개발환경 세팅을 염두해 두었기 때문에) {"default":{"HOST":"localhost"}}로 정의되어 있다.

이는 예상대로라면 include(..., 'envvars.py', ...)에서 DATABASES: '{"default":{"HOST":"db"}}' 로 변경되어야 한다.

하지만 예상대로 작동을 하지 않고 있으니..
일단 출력을 찍어보기로 했다.

settings/__init__.py 최하단에 print(f"[환경변수 디버깅] settings/__init__.py - DATABASES = {DATABASES}")를 추가하여 확인하니 base.py에서의 설정이 그대로 출력되는것을 확인 했다;

즉, envvars.py까지는 도커 컴포즈 파일에서의 환경변수 설정이 잘 들어왔으나,
이것이 envvars.py에서 deep_update()되는 과정에서 해당 함수가 예상과 같이 작동하지 않았다.

🤡 디버깅 3차: utils/collections_utils.py

deep_update() 함수가 정말로 예상대로 작동하지 않는 것인지 확실히 하기 위해 다양한 print()를 찍어봤다.

def deep_update(base_dict, updated_with):

    print(f'cokkections_utils.py에서 초기화 상태의 베이스 딕: {base_dict}')
    print(f'cokkections_utils.py에서 초기화 상태의 새로운 딕: {updated_with}')

    for key, value in updated_with.items():

        # 만일 value가 dict 타입 이라면
        if isinstance(value, dict):
            base_dict_value = base_dict.get(key)  # base_dict에서 같은 key를 갖는 애를 찾아 그 value를 변수에 담음
            # print(f'{cnt}번째 함수에서 베이스딕값(아마 이게 잘못된거 같은데? "D"라는 키가 없지 않나?): {base_dict_value}')

            # 만일 원본 value 또한 dict 타입 이라면 재귀함수 호출
            if isinstance(base_dict_value, dict):
                deep_update(base_dict_value, value, cnt)
            # 그렇지 않으면 값 업데이트
            else:
                base_dict[key] = value

        # 만일 value가 dict 타입이 아니라면 새로운 value로 업데이트
        else:
            base_dict[key] = value

    print(f'cokkections_utils.py에서 업데이트 이후의 베이스 딕: {base_dict}')

    return base_dict

정말로 deep_update() 함수의 리턴값에서 데이터베이스의 호스트 설정이 여전히 'localhost'인 것을 확인했다.

그런데 3차 디버깅 중 deep_update() 함수가 제대로 작동하지 않는 이유에 대한 힌트를 얻었다.
deep_update()는 결국 두개의 인자(base_dict와 update_dict)를 비교하여 base_dict의 키와 update_dict의 키가 같으면 업데이트를 해주는 함수인데,
두 번째 인자인 update_dict를 출력해 보면 키가 이상했다;

🤡 디버깅 4차: utils/settings.py

envvars.py에서 두 번째 인자인 update_with의 실제값인 get_settings_from_environment(ENVVAR_SETTINGS_PREFIX)를 추적하기 위해 utils/settings.py의 get_settings_from_environment(prefix) 함수를 살펴보니

인덱싱이 잘못된 것이 보였다.

def get_settings_from_environment(prefix):

    prefix_len = len(prefix)

    return {key[prefix_len]: yaml_coerce(value) for key, value in os.environ.items() if key.startswith(prefix)}

return시 key[prefix_len:] 에서 :를 빼먹었던 것이다..
그러니 당연히 updated_dict의 키가 한 자리의 알파벳으로 보였던 것..
많이 돌아왔지만 그래도 디버깅 성공..! 🎉

⚠️ 프로젝트 구조 변경으로 인한 경로 이슈: Gunicorn - ModuleNotFoundError: No module named 'project'

app_1  | Starting Gunicorn...
app_1  | [2025-05-12 10:06:42 +0000] [1] [INFO] Starting gunicorn 23.0.0
app_1  | [2025-05-12 10:06:42 +0000] [1] [INFO] Listening at: unix:/gunicorn/gunicorn.sock (1)
app_1  | [2025-05-12 10:06:42 +0000] [1] [INFO] Using worker: sync
app_1  | [2025-05-12 10:06:42 +0000] [24] [INFO] Booting worker with pid: 24
app_1  | [2025-05-12 10:06:42 +0000] [25] [INFO] Booting worker with pid: 25
app_1  | [2025-05-12 10:06:42 +0000] [26] [INFO] Booting worker with pid: 26
app_1  | [2025-05-12 10:06:42 +0000] [24] [ERROR] Exception in worker process
...
app_1  | ModuleNotFoundError: No module named 'project'
app_1  | [2025-05-12 10:06:42 +0000] [24] [INFO] Worker exiting (pid: 24)
app_1  | [2025-05-12 10:06:42 +0000] [25] [ERROR] Exception in worker process
...
app_1  | gunicorn.errors.HaltServer: <HaltServer 'Worker failed to boot.' 3>

🤡 뜻

Gunicorn이 워커를 시작하려 할 때 "project"라는 모듈을 찾지 못해서 죽고있다.

🤡 원인

entrypoint.sh를 보면 다음과 같이 구니콘 시작을 명령하고 있다;

exec poetry run gunicorn core.project.wsgi:application \
    --bind unix:/gunicorn/gunicorn.sock \
    --workers 3

즉 파이썬이 core/project/wsgi.py 파일을 core.project.wsgi라는 이름으로 import한다는 뜻인데,

로그에서는 ModuleNotFoundError: No module named 'project'과 같이 앞부분이 잘린 채 구니콘이 project만 찾고 있다.

🤡 디버깅 1차: 전반적인 흐름 이해하기

장고 app 컨테이너를 대기 상태로 띄운 후
컨테이너 터미널에 접속해서 디렉토리 구조를 살펴봐도 Dockerfile대로 프로젝트가 잘 복사되어 있었다.

문제 해결의 실마리가 보이지 않아 현재 프로젝트에서 통신 구조를 따라가며 하나하나씩 짚어보기로 했다.

1. Client --> Nginx

  • 클라이언트가 'saeminister.store'으로 요청을 보낸다.
  • DNS를 통해 서버의 IP로 요청이 전달된다.
  • 공유기 포트포워딩 설정에 의해 80포트로 수신된 요청을 우분투 서버컴에 토스해 준다.
  • 우분투 서버컴에서 Nginx가 요청을 받는다.

2. Nginx(로컬머신) --> Gunicorn(컨테이너)

  • Nginx 설정파일에 정의된 proxy_pass에 따라 요청을 소켓을 통해 구니콘 (장고 app 컨테이너)에 전달한다.
  • 여기서 프록시란?
  • 구니콘은 WSGI를 통해 장고 앱을 실행한다.
  • 서버컴이 어떻게 컨테이너 안에 구동중인 장고 앱에 전달하는지?
  • WGSI가 뭐야?

3. Gunicorn --> Django

  • 구니콘이 HTTP 요청을 받아 장고 애플리케이션에 전달한다.
  • 장고는 URL, 뷰, 미들웨어 등을 통해 요청을 처리한다.
  • 여기서 미들웨어란? 이건 어떻게 요청을 처리 하는가? 예시?

4. Django --> PostgreSQL(컨테이너)

  • 요청에 따라 DB 조회/수정이 필요하면 장고 ORM으로 PostgreSQL 접속한다.
  • db 컨테이너에 실행되는 PostgreSQL은 Docker Compose의 네트워크로 장고 앱 접근한다.
  • Docker Compose로 컨테이너 띄우면 그 안의 서비스들은 자동으로 네트워크 구성 및 소통 가능? 네트워크에서 컨테이너가 통신하는 방식?
  • 장고는 orm으로 postgresql에 접속하고 / postgresql은 네트워크로 장고앱 접근?

5. Response --> Client

  • 장고 앱의 비즈니스 로직 처리 혹은 PostgreSQL로부터 데이터를 받아 로직 처리 후 그 응답을 Django -> Gunicorn -> Nginx -> 클라이언트 순으로 응답을 전달한다.

👩🏻‍💻 Proxy란?

'대리'라는 뜻.
프록시 서버는 사용자의 요청을 대신 다른 서버에 전달하고, 그 응답도 다시 사용자에게 전달해주는 중간 서버이다.

프록시는 다음 두 가지 종류가 있다;
A. 포워드 프록시 : 클라이언트 -> 프록시 -> 인터넷 (외부 서버)
B. 리버스 프록시 : 인터넷 (외부 서버) -> 프록시 -> 내부 서버(웹앱 등)
+) 포워드 프록시와 리버스 프록시 차이 참고자료

클라이언트가 saeminister.store 요청시
실제 장고앱은 도커 컨테이너 내부에서 동작하고 있기 때문에
클라이언트는 장고앱 컨테이너의 IP나 포트를 직접 알지 못한다.

이때 '리버스 프록시의 역할'을 하는 Nginx(web과 was가 물리적으로 한 서버에 존재하기 때문에 실제로 리버스 프록시는 아님)가 클라이언트의 요청(GET /)을 먼저 받고
해당 요청을 서버컴 내부 도커 컨테이너(장고(구니콘) 서버)로 '대신' 전달해 준다.

그리고 장고앱으로부터 받은 응답을 다시 클라이언트에 전달해 준다.

리버스 프록시를 두는 이유는 다음과 같다;

👩🏻‍💻 현재 구조에서 WAS?

WAS(Web Application Server)는 동적인 웹 콘텐츠를 처리하는 서버이다. 로그인, 데이터 조회 등의 사용자의 요청을 받아 내부로직을 실행하고 결과를 돌려주는 역할을 한다.

현재 내 프로젝트 구조에서 WASDjango + Gunicorn이다.

👩🏻‍💻 Nginx는 어떻게 컨테이너에서 구동중인 장고앱에 요청을 대신 전달해 주는가?

(현재 프젝 구조에서는 유닉스 소켓을 통해 Nginx와 Gunicorn이 소통하지만, 일반적인 경우를 설명하자면(?))

Nginx 설정파일의 proxy_pass 설정 부분을 보면
Nginx로 하여금 클라이언트의 요청을 ~~로 대신 전달하라는 설정이 있다.

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

하지만 장고는 도커 컨테이너에서 구동 중이다.
이때 Nginx가 Django 컨테이너에 접근하기 위해서는 다음 방법이 필요하다;

👩🏻‍💻 UNIX 도메인 소켓이란?

UNIX DOMAIN SOCKET이란 같은 컴퓨터 내에서 두개의 프로그램이 파일을 통해 직접 통신할 수 있게 해주는 방법이다.

일반적인 네트워크 통신 방식(TCP/IP)
보통 Nginx와 Gunicorn이 통신을 할 때에는
Nginx가 localhost:8080 같은 주소로 요청시
Gunicorn이 0.0.0.0:8080 포트에서 기다리는

방식으로 통신이 이루어진다. 이는 네트워크를 사용하는 방식으로, 느리지는 않지만 약간의 네트워크 오버헤드가 있을 수 있다.
+) 네트워크 오버헤드?

반면
UNIX 도메인 소켓 방식
proxy_pass http://unix:/some/path/gunicorn.sock; 과 같이 Nginx 설정파일을 정의하게 되면,
Nginx는 네트워크 포트로 요청을 보내지 않고 '파일(파이프)'을 통해 Gunicorn에게 직접 요청을 전달한다.
즉, 동일 컴퓨터 안에서만 가능하지만 조금 더 빠르고 가볍고 보안상 안전하다.

이를 위해서는 다음과 같은 설정 과정이 필요하다;

1️⃣
현재 내 Nginx 설정 파일에는 proxy_pass http://unix:/home/saemi/src/project_1/shared/gunicorn/gunicorn.sock; 가 있는데
해당 소켓 파일(파이프)을 통해 구니콘에게 요청을 전달한다.
(소켓파일은 실제 파일처럼 보이지만 사실은 네트워크 대신 통신을 도와주는 특수 '파이프'와 같다.)

2️⃣
이를 위해서는 구니콘 실행시 gunicorn --bind unix:/home/saemi/src/project_1/shared/gunicorn/gunicorn.sock ... 등과 같은 소켓 바인딩이 필요하다.

3️⃣
현재 내 프로젝트 아키텍처와 같이 장고앱과 구니콘이 도커 안에서 같이 실행되고 있다면, 컨테이너 안에서 생성된 소켓 파일을 호스트 머신에 마운트 해서 접근 가능하게 하도록 docker-compose.yml 파일에서 다음과 같은 설정이 필요하다;

도커 컴포즈에서 소켓을 공유하는 방식:

services:
  app:
    build: .
    volumes:
      - ./shared/gunicorn:/gunicorn

🤡 디버깅 2차: 전반적인 흐름 속에서 문제 원인 찾기

현재 내가 마주한 ModuleNotFoundError: No module named 'project' 이슈는 소켓 바인딩으로 가기도 전에 구니콘을 실행할 wsgi.py 파일에서 문제가 발생했다는 의미이다.

즉, 파이썬 경로에 문제가 있어 보인다. 가능한 원인은 다음과 같다;

1. exec 커멘드의 경로가 잘못 되었거나 --> 구니콘 실행 없이 대기상태로 컨테이너 띄워서 접속한 뒤 카피된 프로젝트 구조를 살펴봤을 때 문제 예상대로 잘 카피되어 있었음.
2. PYTHONPATH에 문제가 있을 수 있다.

WORKDIR /opt/project
...
ENV PYTHONPATH .
...
COPY core core

흠냐리.. 이렇게 적고 보니 왜 문제가 일어나는지 알겠다!
현재 Dockerfile에서 /opt/project 경로에 core(장고앱 전체 소스코드 파일)를 복사한 뒤, poetry run gunicorn core.project.wsgi:application ... 를 실행시키고 있으니

내가 예상한 opt/project/core/project/ 경로에서 wsgi.py 파일을 찾는게 아니라
장고앱 컨테이너 내부의 프로젝트 최상위 루트 opt/project/ 에서 wsgi.py 파일이 없다고 하는 것이다!

예상대로 Dockerfile에서 WORKDIR /opt/Core 로 수정 후 컨테이너를 다시 띄워보니 이제 컨테이너가 마구 재시작을 멈추기는 했다!!!! 😭😭😭🎉

🤡 디버깅 3차: 에러 메세지 다시 살펴보기

..라고 생각했는데 아니었다.
브라우저로 도메인 접속시 502 Bad Gateway 에러가 떠서 설정파일을 다시 살펴보니, docker-compose.yml 파일에서 내가 디버깅 목적으로 대기 상태의 컨테이너를 띄우던 설정을 주석처리하지 않아서 컨테이너가 잘 실행되고 있다고 착각했다...

일단 wsgi.py 파일에 파이썬이 도달은 하는지 테스트 하기 위해 간단한 print()문을 넣어보니 print() 메세지가 잘 출력되는 것을 확인했다.

그렇다면 wsgi.py 파일 속 설정 자체가 문제되고 있는 것 같다.

🤡 디버깅 4차: wsgi.py

현재 내 wsgi.py 파일 내용 하나하나 뜯어보자;

import os

from django.core.wsgi import get_wsgi_application

print('wsgi.py 파일에 도착함!!')

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings')

application = get_wsgi_application()

os.environ 이란 파이썬을 통해 운영체제에 속한 모든 환경변수를 딕셔너리로 접근 가능하게 한다.
아래와 같은 방식으로 내 OS의 환경변수를 확인할 수 있다.
(관련 설명 참조)

setdefault(key, default_val) 는 딕셔너리 함수로, 해당 key가 없으면 default_val을 설정하고 이미 key가 존재하면 기존값을 유지한다.

알고리즘 공부할 때 딕셔너리 타입 활용시 자주 쎴던 기억이 있다.

setdefault()의 첫 번째 인자 DJANGO_SETTINGS_MODULE 는 장고가 어떤 settings.py 파일을 참조해야할지 지정하는 환경변수 이름이다.

DJANGO_SETTINGS_MODULE라는 환경변수가 등록되어 있지 않을테니
project.settings 즉, proejct/settings.py 파일을 기본값으로 읽어오도록 설정해주고 있다.

이때 setdefault()의 두 번째 인자를 적는 기준 경로는 PYTHONPATH 기준이다!

이후 os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.project.settings') 와 같이 wsgi.py 파일을 수정한 후 다시 컨테이너를 띄워보니 해당 에러는 해결! 🥳 🎉

그런데 무한로딩에 빠진 것처럼 보였다.

🤡 디버깅 5차: 무한로딩인가? 정상 대기중인가?

위에서 말한대로 (데몬모드 X)컨테이너를 띄우니 (entrypoint.sh 파일에 직접 설정한 '구니콘 실행중...' 메세지 이후) 아무런 응답을 하지 않았다.

이게 도대체 제대로 돌아가고 있는건지 아닌지 모르겠다.

그와중에 로컬머신의 .shared/gunicorn/gunicorn.sock 파일은 생성이 되어 더 의아했다.


👩🏻‍💻 마운트란?

(.shared/gunicorn/gunicorn.sock 파일이 생성된 이유 설명)

마운트란 한 디렉토리를 다른 위치에 연결해서, 그 안의 내용을 접근 가능하게 만드는 행위이다.
즉, 호스트 머신의 디렉토리를 컨테이너 안에 심어서
컨테이너에서 해당 디렉토리를 자신의 디렉토리처럼 쓰게 해주는 것.

현재 docker-compose.yml 파일의 app 부분에 다음과 같은 볼륨 설정이 있다;

volumes:
  - ./shared/gunicorn:/gunicorn

./shared/gunicorn 는 로컬머신에 실제하는 디렉토리로, docker-compose.yml 파일이 있는 위치를 기준으로 작성해 준 것이다.

/gunicorn 은 컨테이너 내부 경로로, 이 디렉토리는 실제로 로컬머신의 디렉토리와 연결되어 있다.

즉, 컨테이너는 /gunicorn/gunicorn.sock 파일을 만든다고 생각하고 gunicorn.sock 파일을 생성하지만,
실제로는 호스트의 ./shared/gunicorn/gunicorn.sock 파일을 만들게 된다.


👩🏻‍💻 소켓파일 열어보기?

위에서 .sock 파일은 네트워크 동신을 대체하는 특수 파이프라고 설명 했었다. 이 파이프는 운영체제에서 사용하는 통신용 파일로서, 프로세스간 통신(IPC)을 위한 파이프를 의미한다.

이후 Gunicorn이 이 파일을 열고 Listening 하며, Nginx가 여기에 요청을 전달하게 된다.

이는 텍스트 파일도, 일반 파일도 아닌 "소켓(UNIX Domain Socket)"이기 때문에 cat이나 VSCode로는 열 수 없다.


일단 소켓이 정상작동을 하는지 확인을 위해 curl을 날려본다;

curl --unix-socket /home/saemi/src/project_1/Core/shared/gunicorn/gunicorn.sock http://saeminister.store

응답은 다음과 같이 정상적이다(깡통 장고이기 때문에 / 처리를 안해줬다).

<h1>Not Found</h1><p>The requested resource was not found on this server.</p>

일단 소켓 연결은 정상! 🥳 🎉

⚠️ 502 Bad Gateway

curl을 날렸을 때 Not Found 응답이 왔는데 실제로 브라우저에서 도메인으로 접속을 해보면 502 에러가 뜬다.

🤡 뜻

위에서 curl을 날렸을 예상한 응답(404)이 왔는데
크롬에서 saeminister.store 접속 시 502 에러가 뜬다면
Nginx가 Gunicorn 소켓에 연결을 못 하고 있다는 의미
.

즉, 리버스 프록시 역할을 하는 Nginx가 Clinet -> Nginx -> Gunicorn 구조와 같이 중계서버 역할을 하는 도중
Nginx -> Gunicorn 대리 요청에 대한 응답이 없거나 연결을 못해서 뜨는 에러이다.

🤡 왜 아까 curl은 되고 Nginx는 안되지?

curl --unix-socket /home/saemi/src/project_1/Core/shared/gunicorn/gunicorn.sock http://saeminister.store 에서는 구니콘 소켓을 타고 직접 서버에 요청을 보냈다. 즉, Nginx를 우회해서 구니콘이 정상작동하는지 테스트를 위해 진행했던 것이다.

🤡 가능한 원인들?

  1. Nginx 설정파일의 proxy_pass에 적힌 소켓 경로가 안맞거나
  2. Nginx가 소켓파일에 대한 접근 권한이 없거나
  3. gunicorn.sock가 생성되기 전에 nginx가 먼저 뜨는 경우

🤡 디버깅 1차

  1. proxy_pass 재확인 완료
  2. gunicorn.sock 권한 확인 완료
  3. sudo systemctl restart nginx 실행 후 status 확인 완료

이후 시크릿모드로 브라우저에서 도메인에 접속하려 했으나 여전히 502에러가 떴다.

🤡 디버깅 2차: 컨테이너 안에서 구니콘이 살아있는지 확인

docker exec -it (컨테이너아이디) /bin/bash 로 컨테이너 접속 후
ps aux | grep gunicorn

+) 출력 결과 의미?

🤡 디버깅 3차: Nginx 에러로그 확인

sudo tail -n 50 /var/log/nginx/error.log 확인 결과 권한 이슈가 있는 것이 확인 되었다;

2025/05/13 18:41:57 [crit] 476276#476276: *15 connect() to unix:/home/saemi/src/project_1/Core/shared/gunicorn/gunicorn.sock failed (13: Permission denied) while connecting to upstream, client: (클라이언트 아이피주소), server: saeminister.store, request: "GET / HTTP/1.1", upstream: "http://unix:/home/saemi/src/project_1/Core/shared/gunicorn/gunicorn.sock:/", host: "(서버컴 공인 아이피 주소)"

기존에 gunicorn.sock 파일의 권한을 확인 했을 때 srwxrwxrwx 였기 때문에 Nginx도 해당 파일에 접근이 가능하다고 생각 했었는데 아니었다.
srwxrwxrwx 1 root root 0 May 13 17:46 gunicorn.sock
+) 권한 그룹과 오너 의미 공부 필요

./shared 파일에는 static과 gunicorn 파일이 있어 추후에도 Nginx가 접근해야할 파일이기 때문에 권한을 주려고 한다.
* Nginx 프로세스는 일반적으로 www-data 사용자로 실행됨

🤡 디버깅 4차: shared 디렉토리 권한 고민

docker-compose에서 volumes: ./shared:/gunicorn 같이 마운트를 하고 있기 때문에, 컨테이너 내부 앱(Gunicorn, Django)이 해당 디렉토리에 쓰기 권한이 있어야 한다.
* 기본적으로 Docker 컨테이너 안의 프로세스는 root 사용자로 실행됨.

예를들어 컨테이너 내부에서 Gunicorn이 소켓을 /gunicorn/gunicorn.sock에 생성하는 경우,
해당 디렉토리가 www-data 소유고, 컨테이너 내 앱 프로세스는 root 또는 다른 사용자로 실행하는 경우 권한 이슈가 날 수 있다.

즉, 컨테이너 내부의 앱(Gunicorn 등)이 /gunicorn 디렉토리에 gunicorn.sock 파일을 생성할 수 있어야 함(쓰기 권한 필요).

기존 권한:

ls -al
total 16
drwxr-xr-x 4 root  root  4096 May 12 17:03 .
drwxrwxr-x 7 saemi saemi 4096 May 12 17:00 ..
drwxr-xr-x 2 root  root  4096 May 13 17:46 gunicorn
drwxr-xr-x 2 root  root  4096 May 12 17:03 static

일단 권한 관련해서 어떻게 해야 야무지게 www-data에게 gunicorn.sock 파일에 대한 실행 권한을 줘야할 지 모르겠어서
아묻따 풀권한을 부여했다.
sudo chown -R www-data:www-data shared
sudo chmod -R 777 shared
+) 추후 그룹 나눠서 도커컴포즈 파일이랑 호환되도록 바꾸기
가능한 방법 1: Dockerfile이나 entrypoint.sh에서 실행 사용자를 명시
가능한 방법 2:볼륨 마운트 시 권한이 안 맞을 경우 docker-compose에서 user: 설정을 추가

🤡 디버깅 5차:

프로젝트 루트 디렉토리에 위치한 /shared 디렉토리 및 그 하위 폴더 및 파일의 소유자와 소유 그룹을 www-data로 변경 했음에도 불구하고 502 에러가 떴다.

찾아보니 www-data 사용자가 gunicorn.sock 파일이 있는 디렉터리 체인 전부에 대해 x(실행) 권한을 가져야 한다고 한다.
즉, 디렉토리 트리에서 소켓파일에 도달할 수 있어야 한다는 뜻이다.
+) 왜 루트디렉토리서부터 줘야함?

여기서 잠깐 멘붕이 왔다.
현재 .sock 파일까지의 경로는 /home/saemi/src/project_1/Core/shared/gunicorn/gunicorn.sock 와 같은데, 이 모든 디렉토리들에 www-data 사용자가 실행 권한을 가져야 한다고?! 😱
+) www-data 사용자란? www-data 그룹 멤버들 ?

하지만 언제나 그렇듯 차근차근 하나씩 문제를 들여다보니 그렇게 큰 문제는 아니었다!

루트 디렉토리서부터 .sock 파일 까지
other의 x 권한을 살펴보니
/home/saemi를 제외한 모든 디렉토리에 이미 x가 있었다.

sudo chmod o+x /home/saemi 를 하자니 모든 기타 사용자가 디렉토리에 진입할 수 있도록 하용하는 거라 찜찜했다. 이럴거면 굳이 사용자를 나눌 필요도 없기도 하고..

그래서 다음과 같은 방법을 채택했다;
sudo chgrp www-data /home/saemi/home/saemi 디렉토리의 소유 그룹을 www-data로 변경한다. 즉, www-data 그룹에 속한 사용자들이 해당 디렉토리에 대해 그룹 권한을 따르게 한다.

이후 sudo chmod 710 /home/saemi소유 그룹에 w(실행) 권한을 부여한다.
+) r(읽기) w(생성, 수정, 삭제) x(실행? 진입?) 의 자세한 의미?

그 결.과.

드디어 깡통 장고가 내 도메인으로 배포 되었다 🥳 🎉
넘모 기쁘다...
한 2주간 유툽 강의도 듣고, Chatgpt랑 대화도 많이 하고, 그간 많이 마주쳤었지만 깊이 파고들지 않았던 부분까지 야금야금 공부한 보람이 있다!🥹


본 과정은 앞으로 개인 프젝 '포레포레' 리팩토링을 위해 CI/CD 파이프라인 구축을 위한 첫 배포였다.
앞으로 할 일을 다시 상기하며 오늘의 포스팅을 마무리 한다;

    코드 정리 후 맥북에서 깃 커밋
    이후 서버컴에서 컨테이너만 띄워도 잘 작동 되는지 확인
    Nginx에 SSL 적용
    Pro-Django Tutorial 7, 8, 9 수강 후 CI/CD 완성
    udemy 깃헙 액션 결제?
    서버컴에서 git commit 오류 해결

끝!


✅ 코드 정리 후 맥북에서 깃 커밋 & 이후 서버컴에서 컨테이너 재구동 테스트

(클론 이후)

  • mkdir -p local
  • 502 --> sudo chgrp www-data /home/saemi
  • 여전히 502 --> Nginx 에러 로그 확인(connect() to unix:/home/saemi/src/project_1/Core/shared/gunicorn/gunicorn.sock failed (2: No such file or directory)) --> Nginx conf 파일에서 경로 재지정
  • 정상 작동! 🥳 🎉
    +) 도커 새로 띄울 때마다 chgrp를 해야하나? 왜 기존 설정이 안 먹었지?

✅ 서버컴 깃 커밋 오류

글로벌로 설정된 사용자 이름과 이메일 확인;

git config --global user.name
git config --global user.email

혹은 전체 설정 목록 확인; git config --list

추가적으로,
만약 위 값들이 제대로 설정되어 있는데도 여전히 "Author identity unknown" 에러가 난다면, 해당 리포지토리 로컬에서 user.name과 user.email이 따로 설정되지 않았거나, git이 이를 찾지 못하고 있는 상황일 수 있음.

이때는 로컬에서 사용자 이름과 이메일 설정;

git config user.name "Your Name"
git config user.email "your.email@example.com"

혹은 글로벌 설정을 따르도록 함;

git config --local user.name "$(git config --global user.name)"
git config --local user.email "$(git config --global user.email)"

추가적으로,
설정 내용 반영 우선도는 다음과 같으며; 환경변수 > 로컬 설정 > 글로벌 설정
글로벌 설정 파일이 사용자 계정의 .gitconfig에 없을 경우(홈디렉토리를 인지하지 못하는 특수 환경 - 도커 혹은 CI 환경) 혹은 루트권한 혹은 다른 사용자로 커밋을 실행한 경우 동일한 에러 발생 가능함.

profile
하나씩 차근차근 천천히

0개의 댓글