우선 Gunicorn 에 대해 간단히 알아보자.
Gunicorn은 무엇일까?
Gunicorn은 주로 Django나 Flask와 같은 파이썬 웹 애플리케이션을 배포할 때 사용되며, 웹 서버(예: Nginx)와 파이썬 애플리케이션 사이에서 중간 역할을 수행한다.
웹 서버로부터 요청을 받아 파이썬 애플리케이션으로 전달하고, 애플리케이션의 응답을 다시 웹 서버로 보내는 역할을 한다.
Gunicorn은 개발 환경보다는 실제 서비스 운영 환경에서 높은 트래픽을 효율적으로 처리하기 위해 사용되는 도구이다.
그럼 Django 프로젝트를 Gunicorn 와 이어보자.
우선 gunicorn 을 사용할 때 프로젝트의 가상환경을 활성화해야한다.
그럼 이 가상환경은 무엇이고 왜 활성화 해야할까?
프로젝트마다 독립된 실행 환경을 만들어 의존성 충돌을 막고, 버전의 일관성을 유지하기 위해서이다.
문제 상황: 한 서버에 여러 개의 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 포함)을 사용하여 프로젝트를 구동하므로 충돌이 발생하지 않는다.
개발 환경과 서버 환경의 동기화: 개발자 PC의 가상 환경에서 pip freeze > requirements.txt 명령으로 현재 프로젝트가 사용하는 모든 패키지와 그 버전을 기록한다.
서버에서는 새로운 가상 환경을 만들고
pip install -r requirements.txt
를 실행하여 개발 환경과 100% 동일한 환경을 복제한다.
만약 가상 환경을 사용하지 않는다면, Gunicorn이 시스템에 설치된 다른 버전의 라이브러리를 사용하게 되어 "제 PC에서는 잘 됐는데, 서버에서는 안 돼요"라는 흔한 문제를 겪게 된다. 가상 환경은 이 문제를 원천적으로 방지한다.
Ubuntu와 같은 운영체제는 시스템 작동을 위해 자체적으로 파이썬을 사용한다.
sudo pip install ...
과 같이 시스템 레벨에 패키지를 무분별하게 설치하다가 운영체제가 사용하는 패키지의 버전을 변경하면, 최악의 경우 시스템 유틸리티가 먹통이 되는 등 심각한 문제를 일으킬 수 있다.
가상 환경은 시스템 파이썬 환경을 건드리지 않고, 프로젝트에 필요한 패키지들을 안전하게 관리하게 해준다.
추후 나오겠지만 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 명령을 실행한 것과 동일한 효과를 낸다.
그럼 이제 본격적으로 시작해보자.
우선 가상환경을 실행 보통 가상환경 이름도 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
로 설치한 모든 라이브러리를 사용하여 프로젝트를 구동한다.
가상환경에 구니콘까지 설치를 완료했다면 이제 실행하면 된다.
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 조합으로 프로덕션으로 서비스화까지 진행해보자.
Nginx를 Gunicorn 앞단에 둬서 Reverse Proxy로 사용하고, systemd를 이용해 Gunicorn 프로세스를 안정적으로 관리하는 것이 표준적인 방법이다.
요청 라우팅: 클라이언트 요청을 적절한 WAS로 전달
예: /api/ → WAS, /static/ → 정적 파일
보안: 실제 서버 IP/구조를 외부에 숨김
로드 밸런싱: 여러 WAS로 트래픽 분산
SSL 종료: HTTPS 요청을 처리하고 내부는 HTTP로 전달
캐싱: 정적 자원 등을 캐싱해 성능 향상
대표적인거: Nginx, Apache
항목 | 내용 |
---|---|
Reverse Proxy 위치 | Web Server (Nginx 등) 위치 |
주요 역할 | 요청 라우팅, 보안, 로드밸런싱, SSL 종료 |
OSI 계층 | 주로 4~7계층에서 동작하며, HTTP 처리와 보안 기능은 7계층에 해당 |
# 가상환경 활성화 후
python manage.py collectstatic
이에 나온 결과 경로를 기억해준 다음
/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 실행
- 서비스가 실제로 필요할 때만 시작
- 메모리 절약 및 빠른 부팅
- Zero Downtime Restart
# 서비스 재시작 중에도 소켓은 살아있음 sudo systemctl restart gunicorn.service
- 소켓이 연결 요청을 버퍼링
- 서비스 재시작 완료 후 요청 처리 재개
- 의존성 관리
PartOf=gunicorn.service
- 서비스와 소켓의 생명주기 연결
- 권한 및 보안 관리
SocketUser=www-data # 소켓 파일 소유자 SocketGroup=www-data # 소켓 파일 그룹 SocketMode=0660 # 읽기/쓰기 권한 (소유자, 그룹만)
실제 동작 흐름
1. Nginx가 클라이언트 요청 받음
2. Unix Socket (/run/gunicorn.sock)으로 요청 전달
3. systemd가 소켓 활동 감지하여 Gunicorn 서비스 시작
4. Gunicorn이 Django 애플리케이션 실행하여 응답 생성
5. 응답이 소켓 → Nginx → 클라이언트로 전달
/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 5 | HTTP keep-alive 유지 시간 |
--bind unix:/run/gunicorn.sock | 소켓을 통해 바인딩 (위 소켓 파일과 일치해야 함) |
--preload | 앱을 preload해서 worker 간 메모리 공유 가능 |
--capture-output | print() 등의 stdout/stderr도 로그에 출력 |
--enable-stdio-inheritance | stdout/stderr을 systemd에 전달 |
--log-level info | 로그 레벨 설정 (debug , info , warning , error ) |
Restart=on-failure | Gunicorn이 실패했을 경우 자동 재시작 |
LimitNOFILE=4096 | 열린 파일 디스크립터 수 제한 증가 (디폴트는 너무 낮을 수 있음) |
참고로 gunicorn config 파일을 작성할 수도 있는데 그것보단 위 .service 파일에서 처리하는 방법을 더 추천한다.
왜냐하면 설정을 한곳에 모아서 관리하기 편하고, 설정 변경 후 systemctl daemon-reload
와 systemctl restart
만 하면 되기 때문이다.
# 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가 /run/gunicorn.sock
소켓을 통해 Gunicorn으로 요청을 전달하도록 설정한다.
/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으로 전달되는 것이다.
# 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 애플리케이션의 응답을 다시 사용자에게 보여주게 된다.