Django 배포 (feat.Gunicorn서버)

강정우·2025년 6월 19일
0

Dev_Ops

목록 보기
22/25
post-thumbnail

Gunicorn에 대하여

우선 Gunicorn 에 대해 간단히 알아보자.
Gunicorn은 무엇일까?

Gunicorn은 주로 Django나 Flask와 같은 파이썬 웹 애플리케이션을 배포할 때 사용되며, 웹 서버(예: Nginx)와 파이썬 애플리케이션 사이에서 중간 역할을 수행한다.
웹 서버로부터 요청을 받아 파이썬 애플리케이션으로 전달하고, 애플리케이션의 응답을 다시 웹 서버로 보내는 역할을 한다.

Gunicorn은 개발 환경보다는 실제 서비스 운영 환경에서 높은 트래픽을 효율적으로 처리하기 위해 사용되는 도구이다.

  • 참고로 Gunicorn 과 uWSGI 는 넓은 의미의 WAS 이다.

그럼 Django 프로젝트를 Gunicorn 와 이어보자.

가상환경

우선 gunicorn 을 사용할 때 프로젝트의 가상환경을 활성화해야한다.
그럼 이 가상환경은 무엇이고 왜 활성화 해야할까?

이유

프로젝트마다 독립된 실행 환경을 만들어 의존성 충돌을 막고, 버전의 일관성을 유지하기 위해서이다.

1. 의존성 격리 (Dependency Isolation)

문제 상황: 한 서버에 여러 개의 Django 프로젝트를 운영한다고 가정할 때

프로젝트 A: Django 3.2, requests 라이브러리 2.25 버전 필요
프로젝트 B: Django 4.1, requests 라이브러리 2.28 버전 필요

만약 가상 환경 없이 시스템 전체(Global)에 패키지를 설치한다면, requests 라이브러리는 하나의 버전만 존재할 수 있다.
프로젝트 B를 위해 2.28 버전으로 업그레이드하면, 2.25 버전에 의존하던 프로젝트 A는 예기치 않은 오류를 일으킬 수 있다.
이것을 "의존성 지옥 (Dependency Hell)"이라고 부른다.

따라서 우리는 가상 환경을 만들어 각 프로젝트를 위한 독립된 공간을 만든다.
그러면
프로젝트 A의 가상 환경에는 Django 3.2와 requests 2.25가 설치되고
프로젝트 B의 가상 환경에는 Django 4.1과 requests 2.28이 설치된다.

Gunicorn이 특정 프로젝트의 가상 환경 내에서 실행되면, 정확히 그 환경에 설치된 패키지들(올바른 버전의 Django 포함)을 사용하여 프로젝트를 구동하므로 충돌이 발생하지 않는다.

2. 버전의 일관성 및 재현성 (Consistency & Reproducibility)

개발 환경과 서버 환경의 동기화: 개발자 PC의 가상 환경에서 pip freeze > requirements.txt 명령으로 현재 프로젝트가 사용하는 모든 패키지와 그 버전을 기록한다.

서버에서는 새로운 가상 환경을 만들고

pip install -r requirements.txt

를 실행하여 개발 환경과 100% 동일한 환경을 복제한다.

만약 가상 환경을 사용하지 않는다면, Gunicorn이 시스템에 설치된 다른 버전의 라이브러리를 사용하게 되어 "제 PC에서는 잘 됐는데, 서버에서는 안 돼요"라는 흔한 문제를 겪게 된다. 가상 환경은 이 문제를 원천적으로 방지한다.

3. 시스템 파이썬 환경 보호 (Protecting System Python)

Ubuntu와 같은 운영체제는 시스템 작동을 위해 자체적으로 파이썬을 사용한다.
sudo pip install ... 과 같이 시스템 레벨에 패키지를 무분별하게 설치하다가 운영체제가 사용하는 패키지의 버전을 변경하면, 최악의 경우 시스템 유틸리티가 먹통이 되는 등 심각한 문제를 일으킬 수 있다.
가상 환경은 시스템 파이썬 환경을 건드리지 않고, 프로젝트에 필요한 패키지들을 안전하게 관리하게 해준다.

4. systemd 서비스 파일과의 관계

추후 나오겠지만 gunicorn.service 파일을 보면

[Service]
...
ExecStart=/home/ubuntu/myproject/myvenv/bin/gunicorn \
          ...

여기서 ExecStart가 gunicorn을 바로 실행하지 않고 /home/ubuntu/myproject/myvenv/bin/gunicorn 을 실행하는 것을 볼 수 있다.

/home/ubuntu/myproject/myvenv/bin/gunicorn 은 바로 myvenv라는 가상 환경 내에 설치된 Gunicorn 실행 파일이다.
이 파일을 실행하면, Gunicorn은 이 가상 환경에 설치된 파이썬 인터프리터와 라이브러리들을 사용하여 Django 프로젝트를 구동하게 된다.
즉, source activate 명령을 실행한 것과 동일한 효과를 낸다.

그럼 이제 본격적으로 시작해보자.

Gunicorn 실행

1. Gunicorn 설치

우선 가상환경을 실행 보통 가상환경 이름도 venv 로 많이 설정한다.
그게 convention 하기에 다른 개발자가 봐도 한눈에 이게 가상환경이라는 것을 알수 있기 때문이다.

python3 -m venv [가상환경 이름]
myproject/
├── manage.py
├── myproject/
│   ├── settings.py
│   └── ...
└── test/              <-- 여기에 가상 환경이 생성.
    ├── bin/           <-- 활성화 스크립트가 들어있는 폴더
    │   ├── activate   <-- 해당 스크립트를 실행해야 함
    │   ├── python
    │   └── pip
    ├── include/
    ├── lib/
    └── ...

현재 경로가 프로젝트 root 일 때, 아래 명령어로 해당 스크립트를 실행할 수 있다.

source [가상환경 이름]/bin/activate

프로젝트 루트에 [가상환경 이름] 를 만들었다면, .gitignore 파일에 [가상환경 이름]/를 추가하여 Git이 가상 환경 폴더를 추적하지 않도록 반드시 설정해야 한다.

가상환경을 실행했다면 gunicorn 과 의존성을 모두 설치해준다.

pip install gunicorn
pip install -r requirements.txt

터미널에서 source venv/bin/activate 명령으로 가상 환경을 활성화하면, 쉘(shell)은 앞으로 실행되는 모든 파이썬 관련 명령어를 시스템 전체가 아닌 현재 활성화된 가상 환경 내부에서 먼저 찾아 실행한다.
동작 원리:
1. 가상 환경을 활성화하면, 쉘의 PATH 환경 변수 맨 앞에 현재 가상 환경의 bin 디렉토리 경로가 추가된다.
2. 이 상태에서 gunicorn 명령을 입력하면, 쉘은 PATH를 순서대로 탐색하다가 가장 먼저 발견되는, 즉 가상 환경 내부의 bin/gunicorn을 실행한다.
3. 가상 환경 내의 Gunicorn은 자연스럽게 같은 환경에 설치된 Django, 그리고 requirements.txt로 설치한 모든 라이브러리를 사용하여 프로젝트를 구동한다.

2. Gunicorn 실행

가상환경에 구니콘까지 설치를 완료했다면 이제 실행하면 된다.
cmd 앞 부분에 가상환경 이름이 (가상환경 이름) 이 표시되는 것을 확인하고

gunicorn --workers 1 [프로젝트_이름].wsgi:application

[프로젝트_이름].wsgi: 아래 사진과 같은 파일을 가르키도록 한다.

--workers 3: 3개의 워커 프로세스를 생성하여 요청을 처리하겠다는 의미이다.
참고로 워커 수는 보통 (CPU 코어 수 * 2) + 1 공식을 따르는 것을 권장한다. 그런데 테스트 할땐 로그가 x3 개씩 나오니까 그냥 1로 설정 후 진행하자.

실행하면 터미널에 Gunicorn이 127.0.0.1:8000에서 리스닝하고 있다는 로그가 나타나는데, 이제 웹 브라우저나 curl 명령어로 접속하여 Django 프로젝트가 정상적으로 동작하는지 확인한다.

curl http://127.0.0.1:8000

만약 외부에서 접속할 수 있도록 하려면 --bind 옵션을 사용하면 된다.

gunicorn --workers 3 --bind 0.0.0.0:8000 myproject.wsgi:application

이렇게 보이면 성공적으로 동작하고 있는 것이다.
이제 중지하고 nginx + systemd 조합으로 프로덕션으로 서비스화까지 진행해보자.

systemd + nginx + gunicorn 조합으로

개념

Nginx를 Gunicorn 앞단에 둬서 Reverse Proxy로 사용하고, systemd를 이용해 Gunicorn 프로세스를 안정적으로 관리하는 것이 표준적인 방법이다.

✅ Reverse Proxy의 역할

요청 라우팅: 클라이언트 요청을 적절한 WAS로 전달
예: /api/ → WAS, /static/ → 정적 파일
보안: 실제 서버 IP/구조를 외부에 숨김
로드 밸런싱: 여러 WAS로 트래픽 분산
SSL 종료: HTTPS 요청을 처리하고 내부는 HTTP로 전달
캐싱: 정적 자원 등을 캐싱해 성능 향상
대표적인거: Nginx, Apache

항목내용
Reverse Proxy 위치Web Server (Nginx 등) 위치
주요 역할요청 라우팅, 보안, 로드밸런싱, SSL 종료
OSI 계층주로 4~7계층에서 동작하며, HTTP 처리와 보안 기능은 7계층에 해당
  • 왜 Nginx를 사용할까?
    정적 파일(Static Files) 서빙: Gunicorn은 동적 요청 처리에 특화되어 있다. CSS, JS, 이미지 같은 정적 파일은 Nginx가 훨씬 빠르고 효율적으로 처리하기 때문이다.
    보안 및 로드 밸런싱: Nginx가 맨 앞에서 요청을 받아 필요한 요청만 Gunicorn으로 전달하므로 보안성이 향상된다. 또한 로드 밸런싱, SSL/TLS(HTTPS) 적용 등 다양한 고급 기능을 제공하기 때문이다.

📒 Django Static 파일 생성

# 가상환경 활성화 후
python manage.py collectstatic

이에 나온 결과 경로를 기억해준 다음

⚙️ Gunicorn을 systemd 서비스 등록

Gunicorn Socket 파일 생성

/etc/systemd/system/gunicorn.socket 파일을 생성하고 아래 내용을 추가한다.
Unix 소켓을 사용하면 Nginx와 Gunicorn이 TCP/IP 통신보다 더 효율적으로 통신할 수 있다.

왜냐하면 Unix Domain Socket은 같은 서버 내 프로세스 간 통신을 뜻한다.
따라서 1. 네트워크 스택을 거치지 않음 2. 커널 내부에서 직접 데이터 전송 3. TCP/IP 오버헤드 없음 와 같은 이유로 사용한다.

[Unit]
Description=gunicorn socket for [프로젝트 이름]
PartOf=gunicorn.service

[Socket]
ListenStream=/run/gunicorn.sock
SocketUser=www-data
SocketGroup=www-data
SocketMode=0660

[Install]
WantedBy=sockets.target

ListenStream=/run/gunicorn.sock: Gunicorn이 HTTP 대신 유닉스 도메인 소켓으로 통신합니다.
SocketUser & SocketGroup: 소켓 파일의 소유자/그룹을 설정 (보통 www-data, nginx에서 접근 가능하도록).
SocketMode: 퍼미션 설정 (660은 소유자와 그룹만 접근 가능).

그럼 이 .socket 파일을 왜 systemd 에 등록하는가?
1. Socket Activation (소켓 활성화)

클라이언트 요청 → 소켓 → systemd가 서비스 자동 시작 → Gunicorn 실행
  • 서비스가 실제로 필요할 때만 시작
  • 메모리 절약 및 빠른 부팅
  1. Zero Downtime Restart
# 서비스 재시작 중에도 소켓은 살아있음
sudo systemctl restart gunicorn.service
  • 소켓이 연결 요청을 버퍼링
  • 서비스 재시작 완료 후 요청 처리 재개
  1. 의존성 관리
PartOf=gunicorn.service
  • 서비스와 소켓의 생명주기 연결
  1. 권한 및 보안 관리
SocketUser=www-data      # 소켓 파일 소유자
SocketGroup=www-data     # 소켓 파일 그룹
SocketMode=0660          # 읽기/쓰기 권한 (소유자, 그룹만)

실제 동작 흐름
1. Nginx가 클라이언트 요청 받음
2. Unix Socket (/run/gunicorn.sock)으로 요청 전달
3. systemd가 소켓 활동 감지하여 Gunicorn 서비스 시작
4. Gunicorn이 Django 애플리케이션 실행하여 응답 생성
5. 응답이 소켓 → Nginx → 클라이언트로 전달

Gunicorn Service 파일 생성

/etc/systemd/system/gunicorn.service 파일을 생성하고 아래 내용을 프로젝트 환경에 맞게 수정하여 추가한다.

[Unit]
Description=gunicorn daemon for [프로젝트 이름]
Requires=gunicorn.socket
After=network.target

[Service]
User=ubuntu
Group=www-data
WorkingDirectory=[back-end root path]
ExecStart=[back-end root path]/[가상환경 이름]/bin/gunicorn \
          --access-logfile - \
          --error-logfile - \
          --workers 4 \
          --threads 2 \
          --worker-class gthread \
          --timeout 60 \
          --graceful-timeout 30 \
          --max-requests 1000 \
          --max-requests-jitter 100 \
          --keep-alive 5 \
          --bind unix:/run/gunicorn.sock \
          --preload \
          --capture-output \
          --enable-stdio-inheritance \
          --log-level info \
          [back-end 프로젝트이름].wsgi:application

Restart=on-failure
RestartSec=5
LimitNOFILE=4096

[Install]
WantedBy=multi-user.target
옵션설명
--workers 4프로세스 수, CPU 수 x 2 + 1 정도가 일반적
--threads 2각 워커당 스레드 수 (동시성 향상)
--worker-class gthread비동기 대신 스레드 기반 워커 사용 (sync, gthread, gevent, uvicorn.workers.UvicornWorker 등 가능)
--timeout 60요청 처리 제한 시간
--graceful-timeout 30워커 종료 전 최대 대기 시간
--max-requests 1000일정 요청 수 처리 후 워커 재시작 (메모리 누수 방지용)
--max-requests-jitter 100위와 함께 사용, 요청 수에 약간의 무작위성 추가
--keep-alive 5HTTP keep-alive 유지 시간
--bind unix:/run/gunicorn.sock소켓을 통해 바인딩 (위 소켓 파일과 일치해야 함)
--preload앱을 preload해서 worker 간 메모리 공유 가능
--capture-outputprint() 등의 stdout/stderr도 로그에 출력
--enable-stdio-inheritancestdout/stderr을 systemd에 전달
--log-level info로그 레벨 설정 (debug, info, warning, error)
Restart=on-failureGunicorn이 실패했을 경우 자동 재시작
LimitNOFILE=4096열린 파일 디스크립터 수 제한 증가 (디폴트는 너무 낮을 수 있음)

참고로 gunicorn config 파일을 작성할 수도 있는데 그것보단 위 .service 파일에서 처리하는 방법을 더 추천한다.
왜냐하면 설정을 한곳에 모아서 관리하기 편하고, 설정 변경 후 systemctl daemon-reloadsystemctl restart만 하면 되기 때문이다.

systemd 서비스 시작 및 활성화

# systemd에 서비스 파일 변경사항 알리기
sudo systemctl daemon-reload

# Gunicorn 소켓과 서비스 시작
sudo systemctl start gunicorn.socket
sudo systemctl start gunicorn.service

# 부팅 시 자동으로 실행되도록 활성화
sudo systemctl enable gunicorn.socket
sudo systemctl enable gunicorn.service

# 상태 확인
sudo systemctl status gunicorn.socket
sudo systemctl status gunicorn.service

🔧 Nginx 설정

이제 Nginx가 /run/gunicorn.sock 소켓을 통해 Gunicorn으로 요청을 전달하도록 설정한다.

Nginx 설정 파일 생성

/etc/nginx/sites-available/myproject 파일을 생성하고 아래 내용을 추가한다.

server {
  listen 443 ssl http2;
  server_name tkd.nocturnal.monster;

  # ssl 인증서
  ssl_certificate /etc/letsencrypt/live/tkd.nocturnal.monster/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/tkd.nocturnal.monster/privkey.pem;

  # React
  location / {
    root [React dist 파일];
    try_files $uri $uri/ /index.html;
    
    # 정적 파일 캐싱 설정
    expires 1y;
    add_header Cache-Control "public, immutable";
  }

  # Django API
  location /api/ {
    proxy_pass http://unix:/run/gunicorn.sock;
    proxy_set_header Host $http_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;
    proxy_set_header X-Forwarded-Host $server_name;
    proxy_redirect off;
  }

  # Django admin
  location /admin/ {
    proxy_pass http://unix:/run/gunicorn.sock;
    proxy_set_header Host $http_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;
    proxy_set_header X-Forwarded-Host $server_name;
    proxy_redirect off;
  }

  # Django static
  location /static/ {
    alias [Django static 파일 경로];
    expires 1y;
    add_header Cache-Control "public, immutable";
  }

  # Django media
  location /media/ {
    alias [Django 미디어 파일 경로];
    expires 1y;
    add_header Cache-Control "public";
  }

  # Gzip 압축 설정 (선택사항 없어도 댐)
  gzip on;
  gzip_vary on;
  gzip_min_length 1024;
  gzip_proxied any;
  gzip_comp_level 6;
  gzip_types
    text/plain
    text/css
    text/xml
    text/javascript
    application/json
    application/javascript
    application/xml+rss
    application/atom+xml
    image/svg+xml;
}

# HTTP to HTTPS 리다이렉트
server {
    if ($host = tkd.nocturnal.monster) {
        return 301 https://$host$request_uri;
    } # managed by Certbot

  listen 80;
  server_name tkd.nocturnal.monster;
    return 404; # managed by Certbot
}

브라우저에서 요청을 받으면 nginx가 location /api/ 블록과 매칭, proxy_pass http://unix:/run/gunicorn.sock; 에 의해 Unix socket으로 전달되는 것이다.

Nginx 설정 활성화 및 재시작

# sites-enabled에 심볼릭 링크 생성 (최초 1회)
sudo ln -s /etc/nginx/sites-available/myproject /etc/nginx/sites-enabled/

# Nginx 설정 파일 문법 검사
sudo nginx -t

# Nginx 재시작
sudo systemctl restart nginx

자, 이제 도메인이나 IP로 접속하면 Nginx가 요청을 받아 Gunicorn으로 전달하고, Gunicorn이 처리한 Django 애플리케이션의 응답을 다시 사용자에게 보여주게 된다.

profile
智(지)! 德(덕)! 體(체)!

0개의 댓글