Haru Utils

애플리케이션 · 구성 레시피

Python venv·Gunicorn·Uvicorn·NGINX 배포

Python 의존성을 재현 가능한 venv에 격리하고 WSGI 또는 ASGI 서버를 systemd로 실행한 뒤, 루프백 백엔드를 NGINX가 프록시하도록 검증 순서대로 구성합니다.
Python venv 배포Gunicorn systemdUvicorn WorkerNGINX reverse proxyFastAPI Django 배포
지원 환경Ubuntu Server 24.04 LTS와 systemd
예상 시간60
난이도고급
검토일2026-08-28

BEFORE YOU START

시작 전에 준비하세요

01

고정된 Python 주 버전과 해시가 포함된 requirements.lock 또는 동등한 잠금 파일

02

읽기 전용 애플리케이션 릴리스와 /healthz 상태 경로

03

sudo 권한, 기존 SSH 세션, NGINX 우회·복구 경로

04

WSGI와 ASGI 중 하나를 선택하고 모듈:호출 가능 객체를 확인한 결과

권장 대상 Django·Flask·FastAPI 같은 Python 웹 애플리케이션을 재현 가능하고 복구 가능한 서비스로 운영하려는 개발자

FOLLOW THE RECIPE

8단계 구성·점검 레시피

1
확인시스템을 변경하지 않는 점검 단계

Python 실행 모델과 OS 확인

애플리케이션이 WSGI인지 ASGI인지 먼저 구분합니다. ASGI에서는 Uvicorn 문서가 권장하는 별도 uvicorn-worker 패키지를 사용하고, 개발용 reload 옵션은 사용하지 않습니다.

운영체제
cat /etc/os-release
Python 후보
python3 --version
apt-cache policy python3-venv
초기화 시스템
ps -p 1 -o comm=
현재 NGINX
nginx -v
systemctl status nginx --no-pager
  • 애플리케이션의 WSGI 또는 ASGI 진입점을 확인했다.
  • 선택한 Python 주 버전을 모든 환경에서 지원한다.
  • 개발 서버가 아닌 프로세스 관리 서버를 사용하기로 했다.
결과 읽기

프레임워크와 Python 지원 범위가 맞지 않으면 서버 구성 전에 의존성 잠금부터 갱신합니다.

다음 판단

기존 사용자·포트·서비스·릴리스 권한을 확인합니다.

2
확인시스템을 변경하지 않는 점검 단계

기존 서비스와 릴리스 사전 점검

서비스 계정과 8000 포트, 기존 unit·NGINX 설정을 조사합니다. 실행 중인 서비스를 덮어쓰지 않습니다.

서비스 계정
getent passwd appsvc
getent group appsvc
백엔드 포트
sudo ss -ltnp | grep ':8000 '
기존 unit과 NGINX 파일
systemctl status <APP_SERVICE> nginx.service --no-pager
sudo find /etc/nginx/sites-enabled -maxdepth 1 -type l -o -type f
릴리스와 잠금 파일
sudo stat -c '%U:%G %a %n' /srv/myapp/current /srv/myapp/current/requirements.lock
  • 8000 포트를 다른 프로세스가 사용하지 않는다.
  • 릴리스는 배포 계정이 쓰고 런타임 계정은 읽기만 가능하다.
  • 잠금 파일의 출처와 검토 절차가 있다.
결과 읽기

기존 unit이 active라면 신규 설치가 아니라 변경 배포입니다. 현재 unit·환경 파일·릴리스의 복구본을 먼저 확보합니다.

다음 판단

OS 패키지와 venv 의존성을 무인 승인 없이 설치합니다.

3
변경패키지·설정·서비스 상태가 달라지는 단계

Python venv와 NGINX 설치

OS 패키지는 Ubuntu 저장소에서, Python 패키지는 잠금 파일의 해시를 검증하며 전용 venv에 설치합니다.

변경 단계입니다. 대상 서버, 백업 파일, 서비스 중단 영향과 바로 이전 상태로 돌아가는 방법을 다시 확인하세요.
APT 인덱스
sudo apt update
패키지 변경 미리 보기
apt-get --simulate install python3-venv python3-pip nginx
신규 NGINX 자동 시작 차단
dpkg-query -W nginx >/dev/null 2>&1 || sudo systemctl mask nginx.service
NGINX 패키지가 아직 없는 신규 호스트에서만 mask합니다. 기존 NGINX가 있으면 서비스·기본 사이트를 보존하고 변경 배포 분기로 진행합니다.
필수 패키지 설치
sudo apt-get install python3-venv python3-pip nginx
신규 설치 비노출 확인
systemctl is-enabled nginx.service
sudo ss -ltnp | grep -E ':(80|443)[[:space:]]'
신규 설치 분기에서는 nginx가 masked이고 새 리스너가 없어야 합니다. 기존 NGINX 분기에서는 기존 기준값과 비교합니다.
서비스 계정 생성
sudo adduser --system --group --home /srv/myapp --no-create-home appsvc
계정이 없을 때만 실행합니다. 로그인 셸이 없는 시스템 계정을 사용합니다.
런타임 디렉터리 준비
sudo install -d -m 0750 -o root -g appsvc /srv/myapp/venv
sudo install -d -m 0750 -o appsvc -g appsvc /srv/myapp/var
sudo install -d -m 0750 -o root -g appsvc /etc/myapp
venv 생성
sudo python3 -m venv /srv/myapp/venv
해시 검증 잠금 파일 설치
sudo /srv/myapp/venv/bin/python -m pip install --require-hashes -r /srv/myapp/current/requirements.lock
sudo chown -R root:appsvc /srv/myapp/venv
sudo chmod -R u=rwX,g=rX,o= /srv/myapp/venv
Gunicorn을 포함하고 ASGI를 선택했다면 uvicorn과 uvicorn-worker도 잠금 파일에 직접 버전·해시로 고정합니다. appsvc는 venv를 읽고 실행할 수 있지만 수정할 수 없습니다.
  • venv는 소스 저장소에 커밋하지 않고 같은 잠금 파일로 재생성할 수 있다.
  • pip 설치가 해시 불일치 없이 완료됐다.
  • appsvc는 로그인 불가이고 애플리케이션 릴리스를 수정할 수 없다.
결과 읽기

해시 불일치는 우회하지 말고 잠금 파일의 생성 주체와 패키지 변경을 검토합니다.

다음 판단

환경 파일, 선택한 app server unit, NGINX 설정을 백업 후 작성합니다.

4
주의권한·연결·중단 영향을 확인할 단계

systemd와 NGINX 구성

WSGI와 ASGI unit 중 하나만 선택합니다. 환경 비밀은 unit 명령행이 아닌 권한 제한 파일에 저장하고 예시 화면에는 값을 넣지 않습니다.

백업 접미사
date -u +%Y%m%dT%H%M%SZ
기존 unit 백업
sudo cp --preserve=all --no-clobber /etc/systemd/system/<APP_SERVICE> /etc/systemd/system/<APP_SERVICE>.<BACKUP_SUFFIX>.bak
기존 파일이 있을 때만 실행합니다.
기존 NGINX 설정 백업
sudo cp --preserve=all --no-clobber /etc/nginx/sites-available/myapp.conf /etc/nginx/sites-available/myapp.conf.<BACKUP_SUFFIX>.bak
기존 파일이 있을 때만 실행합니다.
패키지 기본 사이트 확인
sudo readlink -f /etc/nginx/sites-enabled/default
sudo nginx -T | grep -E 'listen .*default_server|server_name'
신규 설치 분기에서만 패키지 기본 링크인지 확인합니다. 기존 운영 사이트라면 이동하지 않습니다.
신규 패키지 기본 링크 보존 비활성화
sudo install -d -m 0755 -o root -g root /etc/nginx/disabled-site-links
sudo mv --no-clobber /etc/nginx/sites-enabled/default /etc/nginx/disabled-site-links/default.<BACKUP_SUFFIX>
신규 설치이고 위 확인 결과가 Ubuntu 패키지 기본 링크인 경우에만 실행합니다. sites-enabled 안에서 이름만 바꾸면 계속 로드되므로 디렉터리 밖으로 이동합니다.
환경 파일 준비
sudo install -m 0640 -o root -g appsvc /dev/null /etc/myapp/myapp.env
sudoedit /etc/myapp/myapp.env
비밀값은 서버에서 직접 입력하고 화면·명령 인자·Git에 넣지 않습니다.
선택한 app unit 편집
sudoedit /etc/systemd/system/<APP_SERVICE>
NGINX 설정 편집
sudoedit /etc/nginx/sites-available/myapp.conf
NGINX 사이트 링크 생성
sudo ln -s /etc/nginx/sites-available/myapp.conf /etc/nginx/sites-enabled/myapp.conf
링크가 없을 때만 실행합니다. 기존 파일을 덮어쓰지 않습니다.
설정 예시 · /etc/systemd/system/<APP_SERVICE>
WSGI 선택 시 Gunicorn unit
[Unit]
Description=My Python WSGI application
After=network.target

[Service]
Type=notify
User=appsvc
Group=appsvc
WorkingDirectory=/srv/myapp/current
EnvironmentFile=/etc/myapp/myapp.env
RuntimeDirectory=myapp
ExecStart=/srv/myapp/venv/bin/gunicorn --workers <WORKERS> --bind 127.0.0.1:8000 --access-logfile - --error-logfile - <WSGI_MODULE>:<WSGI_CALLABLE>
ExecReload=/bin/kill -s HUP $MAINPID
Restart=on-failure
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/srv/myapp/var

[Install]
WantedBy=multi-user.target
Django·Flask 같은 WSGI를 선택한 경우의 예입니다. ASGI unit과 동시에 사용하지 않습니다.
설정 예시 · /etc/systemd/system/<APP_SERVICE>
ASGI 선택 시 Gunicorn·uvicorn-worker unit
[Unit]
Description=My Python ASGI application
After=network.target

[Service]
Type=notify
User=appsvc
Group=appsvc
WorkingDirectory=/srv/myapp/current
EnvironmentFile=/etc/myapp/myapp.env
Environment=FORWARDED_ALLOW_IPS=127.0.0.1
RuntimeDirectory=myapp
ExecStart=/srv/myapp/venv/bin/gunicorn --workers <WORKERS> --worker-class uvicorn_worker.UvicornWorker --bind 127.0.0.1:8000 --access-logfile - --error-logfile - <ASGI_MODULE>:<ASGI_CALLABLE>
ExecReload=/bin/kill -s HUP $MAINPID
Restart=on-failure
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/srv/myapp/var

[Install]
WantedBy=multi-user.target
FastAPI 등 ASGI를 선택한 경우입니다. 폐기 예정인 uvicorn.workers 모듈 대신 별도 uvicorn-worker 패키지를 잠금 파일에 고정합니다.
설정 예시 · /etc/nginx/sites-available/myapp.conf
루프백 Python 백엔드 프록시
server {
  listen <NGINX_PRIVATE_IP>:<NGINX_PORT>;
  server_name <APP_HOST>;

  location / {
    proxy_pass http://127.0.0.1:8000;
    proxy_http_version 1.1;
    proxy_set_header Host $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_connect_timeout 5s;
    proxy_read_timeout 60s;
  }
}
특정 사설 주소부터 검증합니다. 공개 서비스의 TLS와 업로드 크기·timeout은 애플리케이션 요구사항에 따라 별도 검토합니다.
  • WSGI 또는 ASGI unit 하나만 선택했다.
  • Python 서버는 127.0.0.1:8000만 수신한다.
  • 환경 파일은 root:appsvc 0640이고 비밀값이 명령 인자에 없다.
  • 릴리스는 appsvc가 수정할 수 없고 쓰기 경로만 별도 허용됐다.
결과 읽기

ProtectSystem으로 쓰기 오류가 나면 전체 격리를 끄지 말고 필요한 데이터 경로만 ReadWritePaths에 추가합니다.

다음 판단

unit·NGINX 문법을 각각 통과시킨 뒤 순서대로 시작합니다.

5
주의권한·연결·중단 영향을 확인할 단계

unit 검증과 앱 선기동 후 NGINX 반영

애플리케이션을 먼저 루프백에서 검증하고, NGINX 문법이 맞을 때만 reload합니다. 기존 관리 세션을 계속 유지합니다.

Python 서버 실행 파일
/srv/myapp/venv/bin/gunicorn --version
/srv/myapp/venv/bin/python -c 'import sys; print(sys.prefix)'
unit 검증과 반영
sudo systemd-analyze verify /etc/systemd/system/<APP_SERVICE> && sudo systemctl daemon-reload
앱 서비스 시작
sudo systemctl enable --now <APP_SERVICE>
루프백 상태 확인
curl --fail --show-error --silent --max-time 5 http://127.0.0.1:8000/healthz
A · 신규 NGINX 검증 후 시작
sudo nginx -t && sudo systemctl unmask nginx.service && sudo systemctl enable --now nginx.service
신규 설치 분기입니다. 패키지 기본 사이트를 보존 비활성화한 뒤 실행합니다.
B · 기존 NGINX 검증 후 reload
sudo nginx -t && sudo systemctl reload nginx.service
기존 NGINX 운영 분기입니다. 기존 default/site 파일을 삭제하거나 이동하지 않습니다.
NGINX 최종 리스너
sudo ss -ltnp | grep -E ':(80|443|<NGINX_PORT>)[[:space:]]'
  • unit 검증 후 daemon-reload가 실행됐다.
  • 루프백 healthz가 성공한 뒤 NGINX를 반영했다.
  • 신규 또는 기존 NGINX 한 분기만 선택했고 nginx -t 성공 뒤에만 start·reload했다.
  • 신규 분기에서는 패키지 default_server가 전 인터페이스에 남지 않았다.
결과 읽기

루프백이 실패하면 NGINX를 수정하지 말고 app unit·환경 파일·진입점 로그를 먼저 해결합니다.

다음 판단

루프백과 NGINX 경유 실제 요청을 모두 확인합니다.

6
확인시스템을 변경하지 않는 점검 단계

백엔드와 프록시 경유 요청 검증

두 경로의 상태 응답과 로그를 비교해 문제 계층을 분리합니다. 앱 포트가 외부 인터페이스에 열리지 않았는지도 확인합니다.

직접 백엔드
curl --fail --show-error --silent --max-time 5 http://127.0.0.1:8000/healthz
NGINX 경유
curl --fail --show-error --silent --max-time 5 -H 'Host: <APP_HOST>' http://<NGINX_PRIVATE_IP>:<NGINX_PORT>/healthz
리스닝 범위
sudo ss -ltnp | grep -E ':(8000|<NGINX_PORT>) '
서비스와 로그
systemctl is-active <APP_SERVICE> nginx.service
sudo journalctl -u <APP_SERVICE> -n 80 --no-pager
  • 8000은 127.0.0.1에만 바인딩된다.
  • NGINX 경유 healthz와 실제 대표 기능이 정상이다.
  • 재부팅 또는 유지보수 재기동 뒤에도 두 서비스가 정상이다.
결과 읽기

직접 요청은 성공하고 프록시만 실패하면 NGINX·Host·방화벽을, 둘 다 실패하면 app unit을 확인합니다.

다음 판단

비밀·권한·전달 헤더·업데이트 정책을 최종 점검합니다.

7
주의권한·연결·중단 영향을 확인할 단계

런타임 권한과 프록시 신뢰 점검

프로세스 계정은 코드·venv를 수정하지 못해야 하며, 전달 헤더는 루프백 NGINX만 신뢰합니다. 디버그 모드는 운영에서 끕니다.

중요 파일 권한
sudo stat -c '%U:%G %a %n' /etc/myapp/myapp.env /srv/myapp/current /srv/myapp/venv
unit 보안 분석
systemd-analyze security <APP_SERVICE>
외부 리스닝 재확인
sudo ss -ltnp
NGINX 문법 재확인
sudo nginx -t
  • 환경 비밀을 Git·로그·unit ExecStart에 넣지 않았다.
  • appsvc가 코드와 venv를 변경할 수 없다.
  • Uvicorn의 전달 헤더 신뢰 대상은 127.0.0.1뿐이다.
  • DEBUG·자동 reload·개발 오류 페이지를 운영에서 사용하지 않는다.
  • 공개 포트는 NGINX만 사용하고 TLS·보안 헤더를 별도 검증한다.
결과 읽기

보안 분석 점수는 절대값이 아니라 변경 근거를 찾는 도구입니다. 필요한 쓰기·네트워크 권한만 예외로 둡니다.

다음 판단

문제가 있으면 앱 unit과 NGINX 설정을 독립적으로 되돌립니다.

8
주의권한·연결·중단 영향을 확인할 단계

unit·NGINX·릴리스 단계별 복원

실패 지점을 앱과 프록시로 나눠 복원합니다. 현재 파일은 삭제하지 않고 .failed로 보존하며, 이전 설정도 검증 후에만 반영합니다.

서비스와 로그 보존
systemctl status <APP_SERVICE> nginx.service --no-pager
sudo journalctl -u <APP_SERVICE> -n 120 --no-pager
현재 unit 보존
sudo mv /etc/systemd/system/<APP_SERVICE> /etc/systemd/system/<APP_SERVICE>.failed.<ROLLBACK_SUFFIX>
이전 unit 복원·검증
sudo cp --preserve=all /etc/systemd/system/<APP_SERVICE>.<BACKUP_SUFFIX>.bak /etc/systemd/system/<APP_SERVICE> && sudo systemd-analyze verify /etc/systemd/system/<APP_SERVICE> && sudo systemctl daemon-reload && sudo systemctl restart <APP_SERVICE>
현재 NGINX 설정 보존
sudo mv /etc/nginx/sites-available/myapp.conf /etc/nginx/sites-available/myapp.conf.failed.<ROLLBACK_SUFFIX>
이전 NGINX 복원·검증
sudo cp --preserve=all /etc/nginx/sites-available/myapp.conf.<BACKUP_SUFFIX>.bak /etc/nginx/sites-available/myapp.conf && sudo nginx -t && sudo systemctl reload nginx.service
복원 경로 실제 확인
curl --fail --show-error --silent --max-time 5 -H 'Host: <APP_HOST>' http://<NGINX_PRIVATE_IP>:<NGINX_PORT>/healthz
  • 작업 전 실제로 존재한 백업만 복원했다.
  • unit과 NGINX를 각각 문법 검사한 뒤 반영했다.
  • 이전 릴리스의 DB 마이그레이션 호환성을 별도 확인했다.
  • 환경 파일과 실패 파일의 비밀 권한을 유지했다.
결과 읽기

설정 복원 뒤에도 실패하면 코드·의존성·DB 스키마 문제를 분리하고 검증된 이전 릴리스로 전환합니다.

다음 판단

원인을 의존성·unit·프록시·애플리케이션으로 분류해 수정합니다.

SECURITY CHECK

운영 전 마지막 보안 점검

  • Python 의존성은 버전과 해시가 고정된 잠금 파일로 재현한다.
  • 서비스 계정은 로그인 불가이고 코드·venv를 수정할 수 없다.
  • 비밀은 0640 환경 파일에 두고 명령행·Git·로그에 남기지 않는다.
  • Gunicorn/Uvicorn은 루프백만 수신하고 공개 요청은 NGINX가 처리한다.
  • NGINX는 문법 검사와 실제 healthz 검증 후에만 반영한다.

COMMON ERRORS

자주 막히는 지점

ModuleNotFoundError 또는 worker 부팅 실패

증상
systemd가 반복 재시작하고 Python import 오류가 기록됩니다.
가능한 원인
잘못된 venv, 누락된 잠금 의존성, 틀린 모듈 진입점일 수 있습니다.
확인 순서
unit의 절대 실행 경로·WorkingDirectory·잠금 파일 설치 결과를 확인합니다.
트러블슈팅으로 이어보기

NGINX 502 Bad Gateway

증상
루프백 요청은 실패하거나 NGINX만 502를 반환합니다.
가능한 원인
앱 미기동, 8000 바인딩 불일치, 권한·timeout 문제일 수 있습니다.
확인 순서
직접 healthz, ss, app journal, NGINX error log 순서로 계층을 분리합니다.
트러블슈팅으로 이어보기

프록시 뒤 URL 스킴 또는 클라이언트 IP가 틀림

증상
리다이렉트가 HTTP로 향하거나 로그 IP가 프록시 주소입니다.
가능한 원인
전달 헤더 누락 또는 신뢰 프록시 범위 오류일 수 있습니다.
확인 순서
NGINX 헤더와 애플리케이션의 신뢰 프록시를 127.0.0.1 경로로 맞춥니다.
트러블슈팅으로 이어보기

PRIMARY REFERENCES

공식 문서

설치 저장소와 지원 버전은 바뀔 수 있습니다. 검토일 이후에는 링크된 공식 문서와 현재 서버의 패키지 후보 버전을 함께 확인하세요.

도구 빠른 검색

최근 사용한 도구를 다시 열거나, 이름과 기능으로 검색하세요.

검색어와 도구의 입력·결과는 저장하지 않습니다.

↑↓ 이동 · Enter 열기 · Esc 닫기