Django + Docker 개발/운영 환경 분리 및 NAS 배포 정리
Django 애플리케이션을 개발 환경에서 운영 환경으로 배포하면서 Docker 컨테이너, DB, SECRET_KEY, 관리자 계정 등을 분리한 과정 기록
1. 배포 전 상황
처음에는 하나의 Django 프로젝트를 NAS의 Docker 환경에서 개발하고 있었다.
개략적인 구조는 다음과 같았다.
NAS
└── docker/
└── django/
└── <project>/
├── apps/
├── config/
├── media/
├── static/
├── templates/
├── manage.py
└── requirements.txt
개발용 Django 컨테이너는 특정 포트에서 실행되고 있었으며, 개발 DB를 사용하고 있었다.
앱이 실제 사용 가능한 수준까지 완성되면서 다음과 같은 문제가 생겼다.
- 개발 중인 코드가 운영 서비스에 영향을 주면 안 됨
- 개발 DB와 운영 DB를 분리해야 함
- Django SECRET_KEY를 개발/운영 간 분리해야 함
- 운영 중에는 개발 작업을 독립적으로 진행할 수 있어야 함
- NAS에서는 외부 CLI 접근을 제한하고 있어 Container Manager UI를 중심으로 관리해야 함
따라서 개발 환경과 운영 환경을 분리하기로 했다.
1-1. 왜 GitHub 대신 자체 Git 서버를 쓰기로 했나
버전 관리는 처음엔 GitHub를 썼다. 원래는 GitHub 조직(Organization) 계정을 만들어서 private 저장소로 관리하려는 계획도 있었다.
그런데 다시 생각해보니, private으로 해도 어차피 외부 웹 서비스에 코드를 올리는 건 마찬가지고, 혼자 개발하는 프로젝트라 애초에 "코드를 다른 사람과 공유"할 필요 자체가 없었다. 공유가 목적이 아니라면 굳이 외부 서비스에 코드를 올릴 이유가 없다고 판단해서, Git 서버 자체를 회사 NAS에 직접 두는 쪽으로 방향을 바꿨다.
이 결정을 굳히게 된 계기가 하나 더 있었는데, 바로 아래의 사고다.
1-2. .env가 이미 GitHub에 노출됐던 사고
GitHub를 쓰던 초기에, .env(SECRET_KEY 등 민감정보 포함) 파일이 .gitignore에 등록되어 있었는데도 실제로는 계속 GitHub에 올라가고 있었다.
원인: .gitignore는 "새로 추적될 파일"만 무시하고, 이미 Git이 추적 중인(tracked) 파일에는 영향을 주지 않는다. .gitignore에 .env를 추가 했으나 경로 문제였는지, Git은 여전히 그 파일을 추적하고 있었고 이후 변경사항도 계속 push되고 있었던 것이다. VS Code를 쓰는지 여부와는 무관한, Git 동작 자체의 특징이었다.
확인해야 할 것:
- .gitignore에 .env(또는 /.env)가 정확히 한 줄로 들어가 있는지 (오타·경로 문제 여부)
- .env가 이전 커밋에 이미 포함되어 있었는지 git log --stat이나 커밋 기록에서 확인
추적 해제 절차:
# 1. .gitignore에 .env 추가 (이미 있다면 확인만)
echo ".env" >> .gitignore
# 2. Git 추적 목록에서만 제거
git rm --cached .env
# 3. 커밋 후 푸시
git commit -m "Stop tracking .env"
git push
여기서 끝이 아니었다. git rm --cached는 "앞으로의 추적"만 막을 뿐, 이미 과거 커밋 히스토리에 남아있는 .env 내용(SECRET_KEY 원문)은 그대로 남아있다. 즉 저장소 히스토리를 뒤져보면 여전히 예전 SECRET_KEY 값을 볼 수 있는 상태였다.
그래서 실제로는:
- 노출됐던 SECRET_KEY는 재발급(새로 생성)해서 교체
- 히스토리 자체도 정리(과거 커밋에서 해당 파일 내용 제거)하는 작업까지 함께 진행
"이미 원격 히스토리에 노출된 민감정보는 rm --cached만으로는 안심할 수 없고, 값 자체를 무효화(재발급)하는 게 안전하다"는 걸 이때 체감했다.
이 사고를 겪으면서 "애초에 외부 서비스에 코드를 올리지 않으면 이런 리스크 자체가 줄어들지 않을까"라는 생각이 들었고, 이게 자체 Git 서버로 옮기기로 한 결정에 힘을 실어줬다.
2. 개발 환경과 운영 환경의 기본 원칙
이번 배포에서 가장 중요한 원칙은 다음과 같이 정리할 수 있다.
개발과 운영은 실행 환경을 분리하고, 운영은 개발 환경의 변경에 직접 영향을 받지 않도록 한다.
따라서 다음 항목들을 분리했다.
| 항목 | 개발 | 운영 |
| Docker 컨테이너 | 별도 | 별도 |
| 포트 | 개발용 | 운영용 |
| DB | 개발 DB | 운영 DB |
| DB 계정 | 개발 전용 | 운영 전용 |
| SECRET_KEY | 개발용 | 운영용 |
| 코드 작업 공간 | 개발용 | 운영용 |
| Media/업로드 파일 | 개발용 | 운영용 |
| Django 설정 | development | production |
3. Docker 컨테이너를 분리
개발 컨테이너와 운영 컨테이너를 하나로 사용하는 것은 피하는 것이 좋다.
예를 들어 다음과 같이 분리한다.
Django 개발 컨테이너
↓
개발 DB
↓
개발용 코드/파일
Django 운영 컨테이너
↓
운영 DB
↓
운영용 코드/파일
이렇게 하면 개발 중인 코드가 운영 서비스에 바로 영향을 주는 문제를 막을 수 있다.
특히 개발 컨테이너에서 코드를 수정했는데 운영 컨테이너가 같은 NAS 폴더를 바라보고 있다면, 개발자가 의도하지 않았더라도 운영 서비스의 코드가 변경될 수 있다.
따라서 컨테이너뿐 아니라 컨테이너가 사용하는 작업 공간도 분리했다.
4. NAS의 Django 작업 폴더 분리
기존에는 다음과 같이 하나의 프로젝트 폴더를 사용했다.
docker/
└── django/
└── <project>/
이를 다음과 같이 변경했다.
docker/
└── django/
├── <project>-dev/
│ └── <project>/
│
└── <project>-prod/
└── <project>/
여기서 중요한 점이 있다.
Django 프로젝트 이름과 NAS 작업 폴더 이름은 별개의 개념이다.
NAS에서는
<project>-dev
<project>-prod
처럼 구분할 수 있지만, Django 프로젝트 내부의 <project>라는 이름까지 개발/운영에 따라 변경할 필요는 없다.
즉,
<project>-dev/
└── <project>/
와
<project>-prod/
└── <project>/
가 가능하다.
두 환경에서 Django 프로젝트의 구조와 프로젝트 이름은 동일하게 유지하고, 실행 환경만 분리하는 방식이다.
5. Django 프로젝트 자체를 dev/prod로 새로 만드는 것은 피한다
처음에는 다음과 같은 구조도 생각할 수 있다.
<project>-dev/
├── apps/
├── config/
└── manage.py
<project>-prod/
├── apps/
├── config/
└── manage.py
하지만 이렇게 되면 개발 프로젝트와 운영 프로젝트가 별개의 코드베이스처럼 관리될 가능성이 높다.
그 결과:
- 코드 변경 사항을 양쪽에 각각 반영해야 함
- migration 관리가 복잡해짐
- 버전 차이가 발생할 가능성이 높음
- 어떤 코드가 기준인지 불명확해짐
따라서 이번 프로젝트에서는 다음 원칙을 사용한다.
Django 프로젝트는 하나의 프로젝트로 유지하고, 개발/운영 실행 환경만 분리한다.
6. Django settings는 개발/운영으로 분리
Django 설정은 다음과 같이 구성했다.
config/
└── settings/
├── base.py
├── development.py
└── production.py
각 파일의 역할은 다음과 같다.
base.py
개발/운영에서 공통으로 사용하는 설정을 담당한다.
development.py
개발 환경에서 필요한 설정을 담당한다.
예:
DEBUG = True
production.py
운영 환경에서 필요한 설정을 담당한다.
예:
DEBUG = False
이 구조에서 중요한 점은 운영 폴더에 development.py가 존재한다고 해서 개발 설정이 자동으로 실행되는 것은 아니라는 것이다.
Django는 지정된 settings module만 사용한다.
예를 들어 운영 컨테이너에서는:
config.settings.production
을 사용하도록 설정한다.
따라서 development.py가 프로젝트 안에 존재하더라도 운영 환경에서 자동으로 로드되는 것은 아니다.
7. SECRET_KEY는 개발/운영을 반드시 분리
Django의 SECRET_KEY는 개발과 운영에서 동일하게 사용하지 않는 것이 좋다.
이번 프로젝트에서는 개발용 키와 운영용 키를 별도로 생성했다.
개발
SECRET_KEY = 개발용 키
운영
SECRET_KEY = 운영용 키
특히 운영용 SECRET_KEY는 개발 환경에서 사용하던 값을 그대로 복사하지 않고 새로 생성했다.
8. SECRET_KEY를 코드에 직접 넣지 않는다
SECRET_KEY를 다음처럼 코드에 직접 넣는 것은 피한다.
SECRET_KEY = "실제 비밀키"
대신 별도의 파일로 관리한다.
예:
SECRET_KEY.env
그리고 Django 설정에서 해당 파일을 읽도록 한다.
중요한 것은 파일 이름보다 실제 파일 위치와 Django 코드가 참조하는 경로가 일치하는 것이다.
예를 들어 Django가 다음 경로를 찾고 있다면:
/app/<project>/SECRET_KEY.env
실제 컨테이너 내부에도 정확히 그 위치에 파일이 있어야 한다.
실제로 배포 과정에서 다음과 같은 오류를 경험했다.
FileNotFoundError:
[Errno 2] No such file or directory:
'/app/<project>/SECRET_KEY.env'
이 오류는 SECRET_KEY 자체의 문제가 아니라 Django가 지정된 위치에서 SECRET_KEY 파일을 찾지 못한 것이었다.
따라서 비밀키 파일을 다른 위치로 옮기는 것 자체는 문제가 없으며, settings에서 참조하는 위치와 실제 파일 위치를 일치시키면 된다.
9. SECRET_KEY는 Docker 환경변수와 파일 중 하나로 일관되게 관리
Django 설정이 다음과 같은 방식이라면:
secret_key = os.getenv("DJANGO_SECRET_KEY")
if secret_key:
SECRET_KEY = secret_key
else:
# SECRET_KEY.env에서 읽기
컨테이너 환경변수에 SECRET_KEY를 넣지 않고 별도의 SECRET_KEY.env 파일을 사용하는 것도 가능하다.
이번 환경에서는 Container Manager UI를 사용해 NAS에서 컨테이너를 관리하고 있기 때문에, 비밀키를 코드에 넣지 않고 별도의 파일로 관리하는 방식으로 구성했다.
10. 운영 컨테이너 포트 분리
개발 컨테이너가 사용하는 포트와 운영 컨테이너의 포트를 다르게 설정했다.
예:
개발 → 1000
운영 → 1001
중요한 것은 반드시 1001을 사용해야 한다는 것이 아니라 두 컨테이너가 서로 다른 포트를 사용해야 한다는 것이다.
11. 운영 컨테이너의 볼륨 구성
운영 컨테이너는 운영용 작업 공간을 사용하도록 설정했다.
개념적으로:
NAS
│
├── docker/django/<project>-prod
│ ↓
│ /app
│
└── 운영용 파일 저장 폴더
↓
컨테이너 내부의 해당 경로
Django 프로젝트는 컨테이너 내부에서 다음과 같은 구조로 접근한다.
/app/
└── <project>/
├── apps/
├── config/
├── manage.py
└── ...
이때 작업 디렉토리 역시 Django 프로젝트 위치에 맞게 설정해야 한다.
예:
/app/<project>
12. 개발/운영 DB 분리
운영 배포에서 가장 중요한 부분 중 하나가 DB 분리였다.
기존 개발 환경에서 사용하던 DB와 운영 DB를 분리하고, 각각 별도의 DB 계정을 사용하도록 구성했다.
개념적으로:
개발
Django Dev Container
↓
개발 DB
↑
개발 전용 DB 계정
운영
Django Prod Container
↓
운영 DB
↑
운영 전용 DB 계정
이렇게 하면 개발 컨테이너가 운영 DB에 접근할 수 있는 가능성을 줄일 수 있다.
13. DB 계정도 환경별로 분리
DB 이름만 분리하는 것보다 DB 사용자도 분리하는 것이 좋다.
예:
개발 DB
<project>db_dev
↑
<project>_dev_user
운영 DB
<project>db
↑
<project>_prod_user
운영 컨테이너에서는 운영용 DB 계정 정보를 사용하고, 개발 컨테이너에서는 개발용 DB 계정 정보를 사용한다.
이렇게 하면 컨테이너 설정에서 DB 연결 정보를 명확하게 분리할 수 있다.
14. 운영 DB에 처음 Django를 연결할 때
운영 DB를 새로 만들었다면 Django 테이블도 새로 생성해야 한다.
운영 컨테이너에서:
python manage.py migrate
를 실행한다.
이 과정을 하지 않고 운영 Django에 접속하면 다음과 같은 오류가 발생할 수 있다.
ProgrammingError:
(1146, "Table '<project>db.django_session' doesn't exist")
처음에는 관리자 계정이 없어서 발생한 오류로 생각하기 쉽지만, 실제 원인은 Django 기본 테이블이 아직 생성되지 않았기 때문이다.
migrate를 실행하면 Django가 필요한 기본 테이블들을 생성한다.
대표적으로:
django_migrations
django_session
auth_user
auth_group
django_content_type
django_admin_log
등이 생성된다.
15. 운영 DB의 Django 관리자 계정 생성
DB migration이 완료된 다음 운영 환경에서 Django 관리자 계정을 생성할 수 있다.
python manage.py createsuperuser
중요한 점은 개발 DB에서 사용하던 관리자 계정을 그대로 가져오는 것이 아니라 운영 DB에서 별도로 생성하는 것이다.
개발과 운영 DB가 분리되어 있다면 각각 독립적으로 Django 사용자 정보를 관리할 수 있다.
16. Superuser와 실제 업무용 계정은 분리
운영 환경에서는 모든 업무를 superuser 계정으로 수행하는 것보다 실제 업무용 계정을 별도로 만드는 것이 좋다.
구조는 다음과 같이 가져갈 수 있다.
Superuser
↓
시스템 설정 / 권한 관리 / 비상용
일반 Staff 사용자
↓
실제 업무 수행
예를 들어 실제 담당자 계정은 Django shell에서 생성할 수 있다.
python manage.py shell
from django.contrib.auth import get_user_model
User = get_user_model()
user = User.objects.create_user(
username="담당자ID",
password="강력한비밀번호",
email="담당자 이메일"
)
user.is_staff = True
user.is_superuser = False
user.save()
실제 운영에서는 담당자에게 필요한 권한만 부여하는 방향으로 관리하는 것이 좋다.
17. Django 사용자 목록 확인
현재 DB에 어떤 Django 계정이 존재하는지 확인하려면:
python manage.py shell
이후:
from django.contrib.auth import get_user_model
User = get_user_model()
User.objects.all()
관리자 계정만 확인하려면:
User.objects.filter(is_superuser=True)
좀 더 자세하게 확인하려면:
for user in User.objects.all():
print(
user.username,
user.is_staff,
user.is_superuser,
user.is_active
)
DB에서 직접 확인할 경우에도 Django 기본 사용자 테이블을 조회할 수 있지만, 사용자 생성/수정은 Django의 createsuperuser 또는 ORM을 사용하는 것이 안전하다.
18. 최종 운영 구조
이번 배포를 완료한 후의 개념적인 구조는 다음과 같다.
NAS
│
└── docker/
└── django/
│
├── <project>-dev/
│ └── <project>/
│ ├── apps/
│ ├── config/
│ │ └── settings/
│ │ ├── base.py
│ │ ├── development.py
│ │ └── production.py
│ ├── manage.py
│ └── ...
│
└── <project>-prod/
├── SECRET_KEY.env
│
└── <project>/
├── apps/
├── config/
│ └── settings/
│ ├── base.py
│ ├── development.py
│ └── production.py
├── manage.py
└── ...
실행 환경은:
┌── 개발 컨테이너
│ ↓
개발 작업 공간 ─────┤ 개발 DB
│
└── 개발용 SECRET_KEY
┌── 운영 컨테이너
│ ↓
운영 작업 공간 ─────┤ 운영 DB
│
└── 운영용 SECRET_KEY
19. 앞으로의 개발 → 운영 배포 방식
이 구조에서 앞으로는 다음 흐름으로 관리한다.
평소 개발
개발 코드 수정
↓
개발 컨테이너
↓
개발 DB에서 테스트
기능 추가가 완료되면
개발 완료
↓
테스트
↓
운영 반영할 버전 확정
↓
운영 작업 공간에 코드 반영
↓
운영 컨테이너 재시작/업데이트
↓
필요한 경우 migrate
핵심은:
운영 환경에서 직접 코드를 수정하지 않고, 개발 환경에서 검증한 버전을 운영에 반영한다.
단순한 데이터 수정이나 Django Admin을 통한 업무 데이터 관리는 운영에서 직접 수행할 수 있지만, 코드 변경과 기능 개발은 개발 환경에서 진행하는 것이 안전하다.
20. 이번 작업에서 얻은 핵심 원칙
이번 배포 과정에서 가장 중요한 내용만 압축하면 다음과 같다.
⓪ 코드는 애초에 필요 이상으로 외부에 공유하지 않는다
혼자 개발하는 프로젝트라면 GitHub 같은 외부 서비스보다 자체 Git 서버가 리스크를 줄인다. 그리고 .env처럼 민감정보가 든 파일은 커밋하기 전에 .gitignore에 등록해야 한다 — 한 번이라도 커밋되면 .gitignore로는 못 막는다.
① 컨테이너는 개발/운영을 분리한다
Dev Container
Prod Container
② 작업 폴더도 분리한다
<project>-dev
<project>-prod
③ Django 프로젝트 이름까지 개발/운영으로 나누지는 않는다
<project>-dev/<project>
<project>-prod/<project>
④ settings는 환경별로 구분한다
base.py
development.py
production.py
⑤ SECRET_KEY는 개발/운영을 분리한다
개발용 SECRET_KEY
운영용 SECRET_KEY
그리고 실제 비밀값은 코드에 직접 넣지 않는다.
⑥ DB를 분리한다
개발 DB
운영 DB
⑦ DB 계정도 분리한다
개발 DB → 개발 전용 계정
운영 DB → 운영 전용 계정
⑧ 새로운 운영 DB에는 반드시 migration을 수행한다
python manage.py migrate
⑨ superuser와 실제 업무용 계정을 분리한다
superuser → 시스템 관리
staff → 실제 업무
21. 한눈에 보는 최종 체크리스트
운영 배포 시 아래만 확인해도 된다.
[ ] .env 등 민감정보 파일이 첫 커밋 전부터 .gitignore에 등록되어 있는가?
[ ] 개발 컨테이너와 운영 컨테이너가 분리되어 있는가?
[ ] 개발/운영 포트가 다른가?
[ ] 개발/운영 작업 폴더가 다른가?
[ ] 운영 컨테이너가 운영 작업 폴더를 바라보는가?
[ ] 운영 DB가 개발 DB와 분리되어 있는가?
[ ] 운영 DB 전용 계정이 있는가?
[ ] 개발 DB 전용 계정이 있는가?
[ ] 운영 SECRET_KEY가 개발 SECRET_KEY와 다른가?
[ ] SECRET_KEY가 코드에 직접 들어있지 않은가?
[ ] SECRET_KEY 파일의 실제 위치와 Django가 읽는 위치가 일치하는가?
[ ] 운영에서 production settings를 사용하고 있는가?
[ ] 운영 DB에서 migrate를 실행했는가?
[ ] 운영 superuser를 별도로 생성했는가?
[ ] 실제 업무용 staff 계정을 별도로 생성했는가?
[ ] 운영/개발의 media 및 고객 파일 저장 영역이 의도대로 분리되어 있는가?
핵심만 다시 한 문장으로
“같은 Django 프로젝트를 개발과 운영이라는 두 개의 실행 환경으로 분리하고, 컨테이너·작업 공간·DB·DB 계정·SECRET_KEY를 각각 격리한 뒤, 개발에서 검증된 버전을 운영에 배포한다.”
이 문서를 기준으로 앞으로 배포할 때는 “Git 서버/비밀정보 노출 여부 → 컨테이너 → 폴더 → DB → DB 계정 → SECRET_KEY → settings → migrate → 사용자 계정” 순서로 확인하면 된다.