고정된 Python 주 버전과 해시가 포함된 requirements.lock 또는 동등한 잠금 파일
애플리케이션 · 구성 레시피
Python venv·Gunicorn·Uvicorn·NGINX 배포
Python 의존성을 재현 가능한 venv에 격리하고 WSGI 또는 ASGI 서버를 systemd로 실행한 뒤, 루프백 백엔드를 NGINX가 프록시하도록 검증 순서대로 구성합니다.BEFORE YOU START
시작 전에 준비하세요
읽기 전용 애플리케이션 릴리스와 /healthz 상태 경로
sudo 권한, 기존 SSH 세션, NGINX 우회·복구 경로
WSGI와 ASGI 중 하나를 선택하고 모듈:호출 가능 객체를 확인한 결과
권장 대상 Django·Flask·FastAPI 같은 Python 웹 애플리케이션을 재현 가능하고 복구 가능한 서비스로 운영하려는 개발자
FOLLOW THE RECIPE
8단계 구성·점검 레시피
Python 실행 모델과 OS 확인
애플리케이션이 WSGI인지 ASGI인지 먼저 구분합니다. ASGI에서는 Uvicorn 문서가 권장하는 별도 uvicorn-worker 패키지를 사용하고, 개발용 reload 옵션은 사용하지 않습니다.
cat /etc/os-releasepython3 --version
apt-cache policy python3-venvps -p 1 -o comm=nginx -v
systemctl status nginx --no-pager- 애플리케이션의 WSGI 또는 ASGI 진입점을 확인했다.
- 선택한 Python 주 버전을 모든 환경에서 지원한다.
- 개발 서버가 아닌 프로세스 관리 서버를 사용하기로 했다.
프레임워크와 Python 지원 범위가 맞지 않으면 서버 구성 전에 의존성 잠금부터 갱신합니다.
기존 사용자·포트·서비스·릴리스 권한을 확인합니다.
기존 서비스와 릴리스 사전 점검
서비스 계정과 8000 포트, 기존 unit·NGINX 설정을 조사합니다. 실행 중인 서비스를 덮어쓰지 않습니다.
getent passwd appsvc
getent group appsvcsudo ss -ltnp | grep ':8000 'systemctl status <APP_SERVICE> nginx.service --no-pager
sudo find /etc/nginx/sites-enabled -maxdepth 1 -type l -o -type fsudo stat -c '%U:%G %a %n' /srv/myapp/current /srv/myapp/current/requirements.lock- 8000 포트를 다른 프로세스가 사용하지 않는다.
- 릴리스는 배포 계정이 쓰고 런타임 계정은 읽기만 가능하다.
- 잠금 파일의 출처와 검토 절차가 있다.
기존 unit이 active라면 신규 설치가 아니라 변경 배포입니다. 현재 unit·환경 파일·릴리스의 복구본을 먼저 확보합니다.
OS 패키지와 venv 의존성을 무인 승인 없이 설치합니다.
Python venv와 NGINX 설치
OS 패키지는 Ubuntu 저장소에서, Python 패키지는 잠금 파일의 해시를 검증하며 전용 venv에 설치합니다.
sudo apt updateapt-get --simulate install python3-venv python3-pip nginxdpkg-query -W nginx >/dev/null 2>&1 || sudo systemctl mask nginx.serviceNGINX 패키지가 아직 없는 신규 호스트에서만 mask합니다. 기존 NGINX가 있으면 서비스·기본 사이트를 보존하고 변경 배포 분기로 진행합니다.sudo apt-get install python3-venv python3-pip nginxsystemctl 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/myappsudo python3 -m venv /srv/myapp/venvsudo /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/venvGunicorn을 포함하고 ASGI를 선택했다면 uvicorn과 uvicorn-worker도 잠금 파일에 직접 버전·해시로 고정합니다. appsvc는 venv를 읽고 실행할 수 있지만 수정할 수 없습니다.- venv는 소스 저장소에 커밋하지 않고 같은 잠금 파일로 재생성할 수 있다.
- pip 설치가 해시 불일치 없이 완료됐다.
- appsvc는 로그인 불가이고 애플리케이션 릴리스를 수정할 수 없다.
해시 불일치는 우회하지 말고 잠금 파일의 생성 주체와 패키지 변경을 검토합니다.
환경 파일, 선택한 app server unit, NGINX 설정을 백업 후 작성합니다.
systemd와 NGINX 구성
WSGI와 ASGI unit 중 하나만 선택합니다. 환경 비밀은 unit 명령행이 아닌 권한 제한 파일에 저장하고 예시 화면에는 값을 넣지 않습니다.
date -u +%Y%m%dT%H%M%SZsudo cp --preserve=all --no-clobber /etc/systemd/system/<APP_SERVICE> /etc/systemd/system/<APP_SERVICE>.<BACKUP_SUFFIX>.bak기존 파일이 있을 때만 실행합니다.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에 넣지 않습니다.sudoedit /etc/systemd/system/<APP_SERVICE>sudoedit /etc/nginx/sites-available/myapp.confsudo ln -s /etc/nginx/sites-available/myapp.conf /etc/nginx/sites-enabled/myapp.conf링크가 없을 때만 실행합니다. 기존 파일을 덮어쓰지 않습니다.[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.targetDjango·Flask 같은 WSGI를 선택한 경우의 예입니다. ASGI 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.targetFastAPI 등 ASGI를 선택한 경우입니다. 폐기 예정인 uvicorn.workers 모듈 대신 별도 uvicorn-worker 패키지를 잠금 파일에 고정합니다.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 문법을 각각 통과시킨 뒤 순서대로 시작합니다.
unit 검증과 앱 선기동 후 NGINX 반영
애플리케이션을 먼저 루프백에서 검증하고, NGINX 문법이 맞을 때만 reload합니다. 기존 관리 세션을 계속 유지합니다.
/srv/myapp/venv/bin/gunicorn --version
/srv/myapp/venv/bin/python -c 'import sys; print(sys.prefix)'sudo systemd-analyze verify /etc/systemd/system/<APP_SERVICE> && sudo systemctl daemon-reloadsudo systemctl enable --now <APP_SERVICE>curl --fail --show-error --silent --max-time 5 http://127.0.0.1:8000/healthzsudo nginx -t && sudo systemctl unmask nginx.service && sudo systemctl enable --now nginx.service신규 설치 분기입니다. 패키지 기본 사이트를 보존 비활성화한 뒤 실행합니다.sudo nginx -t && sudo systemctl reload nginx.service기존 NGINX 운영 분기입니다. 기존 default/site 파일을 삭제하거나 이동하지 않습니다.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 경유 실제 요청을 모두 확인합니다.
백엔드와 프록시 경유 요청 검증
두 경로의 상태 응답과 로그를 비교해 문제 계층을 분리합니다. 앱 포트가 외부 인터페이스에 열리지 않았는지도 확인합니다.
curl --fail --show-error --silent --max-time 5 http://127.0.0.1:8000/healthzcurl --fail --show-error --silent --max-time 5 -H 'Host: <APP_HOST>' http://<NGINX_PRIVATE_IP>:<NGINX_PORT>/healthzsudo 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을 확인합니다.
비밀·권한·전달 헤더·업데이트 정책을 최종 점검합니다.
런타임 권한과 프록시 신뢰 점검
프로세스 계정은 코드·venv를 수정하지 못해야 하며, 전달 헤더는 루프백 NGINX만 신뢰합니다. 디버그 모드는 운영에서 끕니다.
sudo stat -c '%U:%G %a %n' /etc/myapp/myapp.env /srv/myapp/current /srv/myapp/venvsystemd-analyze security <APP_SERVICE>sudo ss -ltnpsudo nginx -t- 환경 비밀을 Git·로그·unit ExecStart에 넣지 않았다.
- appsvc가 코드와 venv를 변경할 수 없다.
- Uvicorn의 전달 헤더 신뢰 대상은 127.0.0.1뿐이다.
- DEBUG·자동 reload·개발 오류 페이지를 운영에서 사용하지 않는다.
- 공개 포트는 NGINX만 사용하고 TLS·보안 헤더를 별도 검증한다.
보안 분석 점수는 절대값이 아니라 변경 근거를 찾는 도구입니다. 필요한 쓰기·네트워크 권한만 예외로 둡니다.
문제가 있으면 앱 unit과 NGINX 설정을 독립적으로 되돌립니다.
unit·NGINX·릴리스 단계별 복원
실패 지점을 앱과 프록시로 나눠 복원합니다. 현재 파일은 삭제하지 않고 .failed로 보존하며, 이전 설정도 검증 후에만 반영합니다.
systemctl status <APP_SERVICE> nginx.service --no-pager
sudo journalctl -u <APP_SERVICE> -n 120 --no-pagersudo mv /etc/systemd/system/<APP_SERVICE> /etc/systemd/system/<APP_SERVICE>.failed.<ROLLBACK_SUFFIX>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>sudo mv /etc/nginx/sites-available/myapp.conf /etc/nginx/sites-available/myapp.conf.failed.<ROLLBACK_SUFFIX>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.servicecurl --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
공식 문서
설치 저장소와 지원 버전은 바뀔 수 있습니다. 검토일 이후에는 링크된 공식 문서와 현재 서버의 패키지 후보 버전을 함께 확인하세요.