본문으로 건너뛰기

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 syncmigratecollectstaticrestart 순서를 따릅니다
  • DB는 PostgreSQL 17 또는 MariaDB 11.4를 지원합니다
  • Django 관리 명령 실행 시 env 로드가 필수입니다

1. 환경 매니페스트

항목
OSUbuntu 24.04 LTS
언어 / 런타임Python 3.12
프레임워크Django ≥5.1 + gunicorn ≥23.0
패키지 매니저uv 0.5+
프로세스 매니저systemd
앱 listen127.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 17MariaDB 11.4
인증peer auth (sudo psql)unix_socket auth (sudo mariadb)
포트54323306
서비스명postgresql@17-mainmariadb
Django ENGINEdjango.db.backends.postgresqldjango.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 안전 패턴
1DB 환경변수 참조 깨짐HTTP 200은 떠도 모든 DB 쿼리 실패
(KeyError, OperationalError)
settings.pyDATABASES에서 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
5settings.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 Requestenv ALLOWED_HOSTS=example.com,www.example.com
collectstatic 누락정적 파일 404manage.py collectstatic --noinput
migrate 누락OperationalError: no such tablemanage.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 502gunicorn 다운systemctl status $PROJECT_NAME
HTTP 400 Bad RequestALLOWED_HOSTS 미등록env에 도메인 추가 → restart
정적 파일 404collectstatic 미실행manage.py collectstatic
KeyError: 'DB_NAME'env 로드 안 함 (수동 실행)set -a; source /etc/$PROJECT_NAME/env; set +a
OperationalError: no such tablemigrate 미실행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 문의게시판을 이용해 주세요.