Python Django 운영 가이드
Python 3.12 + Django ≥5.1 + gunicorn ≥23.0 + uv 0.5+ + systemd 환경에서 PostgreSQL 17 또는 MariaDB 11.4를 사용하는 운영 매뉴얼.
💡 요약 정리
- 앱은 127.0.0.1:8000에서 listen하며, nginx가 리버스 프록시 역할을 합니다
- 환경변수는
/etc/[프로젝트]/env에서 관리하며, 배포 시 절대 덮어쓰지 않아야 합니다 - 코드 배포 후 반드시
uv sync→migrate→collectstatic→restart순서를 따릅니다 - DB는 PostgreSQL 17 또는 MariaDB 11.4를 지원합니다
- Django 관리 명령 실행 시 env 로드가 필수입니다
1. 환경 매니페스트
| 항목 | 값 |
|---|---|
| OS | Ubuntu 24.04 LTS |
| 언어 / 런타임 | Python 3.12 |
| 프레임워크 | Django ≥5.1 + gunicorn ≥23.0 |
| 패키지 매니저 | uv 0.5+ |
| 프로세스 매니저 | systemd |
| 앱 listen | 127.0.0.1:8000 |
| 리버스 프록시 | nginx 1.24 (:80/:443, Let's Encrypt 자동) |
| 앱 디렉토리 | /opt/[프로젝트] |
| 환경변수 | /etc/[프로젝트]/env |
| 로그 | /var/log/[프로젝트] |
| 서비스 단위 | [프로젝트].service |
| Django 프로젝트 디렉토리 | /opt/[프로젝트]/[프로젝트]/ (settings.py, urls.py, wsgi.py) |
| 정적 파일 | /opt/[프로젝트]/staticfiles/ (collectstatic 결과) |
| 미디어 파일 | /opt/[프로젝트]/media/ |
DB 매니페스트
| 항목 | PostgreSQL 17 | MariaDB 11.4 |
|---|---|---|
| 인증 | peer auth (sudo psql) | unix_socket auth (sudo mariadb) |
| 포트 | 5432 | 3306 |
| 서비스명 | postgresql@17-main | mariadb |
| Django ENGINE | django.db.backends.postgresql | django.db.backends.mysql |
| 클라이언트 | psycopg[binary] | mysqlclient 또는 PyMySQL |
셸에 한 줄: PROJECT_NAME=myapp; export PROJECT_NAME
2. 서버 접속
2-1. SSH (root)
ssh root@[아이디].mycafe24.com
2-2. SFTP / rsync
rsync -avz --exclude='.venv/' --exclude='__pycache__/' --exclude='staticfiles/' --exclude='media/' \
./ root@[아이디].mycafe24.com:/opt/$PROJECT_NAME/
ssh root@[아이디].mycafe24.com "chown -R appuser:appuser /opt/$PROJECT_NAME"
2-3. appuser로 작업
sudo -u appuser bash
cd /opt/$PROJECT_NAME
Django 관리 명령은 env 로드 필수:
sudo -u appuser bash -c 'set -a; source /etc/$PROJECT_NAME/env; set +a
cd /opt/$PROJECT_NAME && uv run python manage.py shell'
3. 환경 확인
ls -la /opt/$PROJECT_NAME
sudo cat /etc/$PROJECT_NAME/env
sudo systemctl is-active $PROJECT_NAME
sudo journalctl -u $PROJECT_NAME -n 50
ss -tlnp | grep 8000
curl -sf http://127.0.0.1:8000/admin/login/
4. DB 직접 접속
4-1. PostgreSQL 17
sudo -u postgres psql
\l
\du
\c [DB명]
\dt
\q
4-1'. MariaDB 11.4
sudo mariadb
SHOW DATABASES;
USE [DB명];
SHOW TABLES;
EXIT;
4-2. 앱 사용자로 접속
# PG
psql "postgresql://[DB사용자]:[비밀번호]@127.0.0.1:5432/[DB명]"
# MariaDB
mariadb -u [DB사용자] -p [DB명]
4-3. 샘플 DDL
Django ORM이 자동 생성하므로 직접 DDL은 거의 불필요. python manage.py migrate로 처리. 수동 점검 시:
PG:
SELECT table_name FROM information_schema.tables WHERE table_schema = 'public';
MariaDB:
SHOW TABLES;
5. 코드 배포 워크플로우
표준 시퀀스
# 1. 업로드
sudo -u appuser bash -c "cd /opt/$PROJECT_NAME && git pull"
# 2. 의존성 동기화
sudo -u appuser uv sync --directory /opt/$PROJECT_NAME
# 3. 마이그레이션 + 정적 파일 수집
sudo -u appuser bash -c '
set -a; source /etc/$PROJECT_NAME/env; set +a
cd /opt/$PROJECT_NAME
uv run python manage.py migrate --noinput
uv run python manage.py collectstatic --noinput
'
# 4. 재시작
sudo systemctl restart $PROJECT_NAME
# 5. 검증
curl -sf http://127.0.0.1:8000/
sudo journalctl -u $PROJECT_NAME -n 30
5-A. 배포 시 위험 회피
⚠️ 자체 코드를 통째로 업로드하면 자동 구성된 DB·보안·systemd 설정이 사라져 서비스가 깨질 수 있습니다.위험 5종 매트릭스
| # | 위험 | 증상 | Django 안전 패턴 |
|---|---|---|---|
| 1 | DB 환경변수 참조 깨짐 | HTTP 200은 떠도 모든 DB 쿼리 실패 (KeyError, OperationalError) | settings.py의 DATABASES에서 os.environ["DB_NAME"] 등 사용 |
| 2 | 외부 IP에 listen | 외부에서 :8000 직접 접속 가능 → nginx 우회 → fail2ban·rate limit 무력화 | gunicorn --bind 127.0.0.1:8000 (systemd unit) |
| 3 | 엔트리 포인트 변경 | systemd가 시작 시 wsgi 모듈 못 찾음 ("ModuleNotFoundError") | [프로젝트]/wsgi.py 위치 유지, [프로젝트]/urls.py 라우팅 |
| 4 | 의존성 동기화 누락 | "ImportError" / "ModuleNotFoundError" | 업로드 후 uv sync |
| 5 | settings.py / env 덮어쓰기 | env 미반영 → DB 연결 실패, ALLOWED_HOSTS 누락 → 400 | [프로젝트].service/env/STATIC_ROOT 변경 시 절대 업로드 금지 |
안전한 settings.py 패턴
# settings.py
import os
SECRET_KEY = os.environ["SECRET_KEY"]
DEBUG = os.environ.get("DEBUG", "False") == "True"
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "").split(",")
DATABASES = {
"default": {
# PostgreSQL
"ENGINE": "django.db.backends.postgresql",
# MariaDB라면: "ENGINE": "django.db.backends.mysql",
"NAME": os.environ["DB_NAME"],
"USER": os.environ["DB_USER"],
"PASSWORD": os.environ["DB_PASSWORD"],
"HOST": os.environ.get("DB_HOST", "127.0.0.1"),
"PORT": os.environ.get("DB_PORT", "5432"), # MariaDB: "3306"
"OPTIONS": { # MariaDB만
# "charset": "utf8mb4",
},
}
}
STATIC_URL = "/static/"
STATIC_ROOT = "/opt/[프로젝트]/staticfiles/"
MEDIA_URL = "/media/"
MEDIA_ROOT = "/opt/[프로젝트]/media/"
절대 업로드 금지
다음 파일은 절대 업로드하지 마십시오:/etc/systemd/system/[프로젝트].service/etc/[프로젝트]/env
3가지 배포 방법 (안전 순서)
| 방법 | 설명 | 적합 |
|---|---|---|
| 방법 1 — 변경 파일만 업로드 | rsync로 .py 파일만 (가장 안전) | 일상 배포 |
| 방법 2 — 소스 통째 업로드 + 서버 빌드 | .venv/, __pycache__/, staticfiles/, media/ 제외 필수 | 큰 변경, 의존성 추가 |
| 방법 3 — 외부 설정 분리 | DB 등은 env 파일만, 코드는 자유 | 권장 베이스 |
배포 후 4가지 체크
sudo systemctl is-active $PROJECT_NAME # 1. active
ss -tlnp | grep 8000 | grep '127.0.0.1' # 2. 127.0.0.1 listen
curl -sf http://127.0.0.1:8000/ # 3. 헬스 OK
curl -sI https://[도메인]/ # 4. 외부 응답
5-B. Django 배포 7단계
# 1. 업로드 (rsync, .venv·staticfiles·media·__pycache__ 제외)
# 2. 권한 회복
ssh root@... "chown -R appuser:appuser /opt/$PROJECT_NAME"
# 3. 의존성
sudo -u appuser uv sync --directory /opt/$PROJECT_NAME
# 4. 마이그레이션
sudo -u appuser bash -c 'set -a; source /etc/$PROJECT_NAME/env; set +a; cd /opt/$PROJECT_NAME && uv run python manage.py migrate --noinput'
# 5. 정적 파일
sudo -u appuser bash -c 'set -a; source /etc/$PROJECT_NAME/env; set +a; cd /opt/$PROJECT_NAME && uv run python manage.py collectstatic --noinput'
# 6. 재시작
sudo systemctl restart $PROJECT_NAME
# 7. 검증
curl -sI https://[도메인]/
자주 만나는 실수
| 실수 | 결과 | 회복 |
|---|---|---|
DEBUG=True 운영 배포 | 에러 페이지에 SECRET 노출, 정적 파일 동작 변경 | env에 DEBUG=False |
ALLOWED_HOSTS 미등록 | 400 Bad Request | env ALLOWED_HOSTS=example.com,www.example.com |
collectstatic 누락 | 정적 파일 404 | manage.py collectstatic --noinput |
migrate 누락 | OperationalError: no such table | manage.py migrate |
5-C. 환영 페이지 끄고 내 코드 띄우기 (필독)
설치 직후 도메인으로 접속하면 "서버가 정상 동작 중입니다" 환영 페이지가 표시됩니다. 이는 Nginx가 /var/www/cafe24-welcome/index.html을 우선 서빙하기 때문입니다. 본인 코드의 / 라우트가 보이게 하려면 이 환영 파일 1개만 정리하면 됩니다.
5-C-1. 환영 페이지 끄기
| 방법 | 명령 |
|---|---|
| 방법 A — 삭제 | sudo rm /var/www/cafe24-welcome/index.html |
| 방법 B — 백업 후 비활성 (원복 가능) | sudo mv /var/www/cafe24-welcome/index.html /var/www/cafe24-welcome/index.html.bak |
| 현재 상태 확인 | ls /var/www/cafe24-welcome/ |
5-C-2. 확인
도메인 재접속 또는 curl -sI https://[도메인]/ 결과로 본인 앱이 응답하는지 확인합니다. 캐시가 남으면 시크릿 창 또는 강제 새로고침을 권장합니다.
Tip: 환영 파일을 백업해두면 트러블슈팅 시 "Nginx는 살아있나" 확인용으로 다시 활성화할 수 있습니다.
6. DB 연동 실전 코드
모델 (PG/Maria 공통)
# myapp/models.py
from django.db import models
class Item(models.Model):
name = models.CharField(max_length=255)
payload = models.JSONField(null=True, blank=True) # PG: JSONB / Maria: JSON
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "items"
indexes = [models.Index(fields=["-created_at"])]
View (CRUD)
# myapp/views.py
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
import json
from .models import Item
@require_http_methods(["GET"])
def list_items(request):
items = Item.objects.values("id", "name", "payload", "created_at")[:100]
return JsonResponse(list(items), safe=False)
@require_http_methods(["POST"])
def create_item(request):
data = json.loads(request.body)
item = Item.objects.create(name=data["name"], payload=data.get("payload"))
return JsonResponse({"id": item.id})
Connection Pool (gunicorn 워커)
gunicorn은 워커당 DB 커넥션 보유. CONN_MAX_AGE 설정으로 재사용:
# settings.py
DATABASES["default"]["CONN_MAX_AGE"] = 60 # 60초 재사용
DATABASES["default"]["CONN_HEALTH_CHECKS"] = True # Django 4.1+
7. 정적 파일 / 미디어
nginx에 정적·미디어 위임
location /static/ {
alias /opt/[프로젝트]/staticfiles/;
expires 7d;
add_header Cache-Control "public, immutable";
}
location /media/ {
alias /opt/[프로젝트]/media/;
expires 30d;
}
collectstatic (배포마다 필수)
sudo -u appuser bash -c '
set -a; source /etc/$PROJECT_NAME/env; set +a
cd /opt/$PROJECT_NAME && uv run python manage.py collectstatic --noinput
'
8. 로그 / 모니터링
sudo journalctl -u $PROJECT_NAME -f
sudo journalctl -u $PROJECT_NAME -n 100
sudo tail -f /var/log/nginx/[프로젝트]_access.log
sudo tail -f /var/log/nginx/[프로젝트]_error.log
Django 로깅 설정 권장
# settings.py
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"console": {"class": "logging.StreamHandler"},
},
"root": {
"handlers": ["console"],
"level": "INFO",
},
}
stdout으로 출력 → systemd journal이 자동 수집.
9. HTTPS / 도메인
# 보유 도메인 SSL 발급
sudo certbot --nginx -d example.com -d www.example.com
# Django ALLOWED_HOSTS에 도메인 추가 (env)
sudo nano /etc/$PROJECT_NAME/env
# ALLOWED_HOSTS=example.com,www.example.com,[아이디].mycafe24.com
sudo systemctl restart $PROJECT_NAME
도메인 연결 후 반드시 ALLOWED_HOSTS에 해당 도메인을 추가하고 서비스를 재시작해야 합니다. 미등록 시 400 Bad Request가 발생합니다.
10. 트러블슈팅 + FAQ
트러블슈팅 매트릭스
| 증상 | 원인 | 해결 |
|---|---|---|
| HTTP 502 | gunicorn 다운 | systemctl status $PROJECT_NAME |
| HTTP 400 Bad Request | ALLOWED_HOSTS 미등록 | env에 도메인 추가 → restart |
| 정적 파일 404 | collectstatic 미실행 | manage.py collectstatic |
KeyError: 'DB_NAME' | env 로드 안 함 (수동 실행) | set -a; source /etc/$PROJECT_NAME/env; set +a |
OperationalError: no such table | migrate 미실행 | manage.py migrate |
| Specified key too long (Maria) | utf8mb4 인덱스 | OPTIONS = {'charset': 'utf8mb4'} |
| 마이그레이션 충돌 | 다중 개발자 | python manage.py makemigrations --merge |
FAQ
Q. 관리자 계정 만들기
sudo -u appuser bash -c '
set -a; source /etc/$PROJECT_NAME/env; set +a
cd /opt/$PROJECT_NAME && uv run python manage.py createsuperuser
'
Q. shell 들어가기
sudo -u appuser bash -c '
set -a; source /etc/$PROJECT_NAME/env; set +a
cd /opt/$PROJECT_NAME && uv run python manage.py shell
'
Q. DEBUG=True로 했더니 에러 페이지에 SECRET 다 보여요
즉시 DEBUG=False로 변경하세요. 운영 환경에서 DEBUG=True는 보안 위반입니다.
Q. gunicorn 워커 수 늘리고 싶어요
[프로젝트].service의 --workers 옵션 변경 (예: --workers 4). 변경 후 daemon-reload + restart. 자세한 건 공통 운영 가이드 참조.
12. 추가 운영 / 커스터마이징
공통 운영·커스터마이징 가이드 및 FAQ·트러블슈팅 문서를 참조하세요.
추가 문의 사항이 있으시면 1:1 문의게시판을 이용해 주세요.