Docker - Django, Nginx, Gunicorn 연동
in DEV on DevOps, Docker, Kubernetes, Django, Nginx, Gunicorn, Python, Pyenv, Yaml, Container-infrastructure, Web-service-deployment, DevOps, Port-forwarding, Dockerfile
이 포스트에서는 도커의 기초 명령어와 핵심 기능들을 활용하여 실제 작동하는 애플리케이션을 구축해본다.
파이썬의 대표적인 웹 프레임워크인 Django를 활용하여 웹 서비스를 컨테이너 환경에서 실행해본다.
아래와 같은 실무적인 인프라 구성을 목표로 한다.
- Pyenv를 활용한 독립적인 파이썬 가상 환경 구축
- YAML 파일의 문법과 개념 이해(설정 파일 관리)
- Docker Compose를 활용한 다중 컨테이너 오케스트레이션
목차
개발 환경
- Ubuntu 24.04.2 LTS
- Mac Apple M3 Max
- Memory 48 GB
1. 추가 환경 구축
Django와 Docker를 연동하기에 앞서, 개발 편의성과 버전 관리를 위한 기본 환경을 구축한다.
1.1. pyenv 설치
가장 먼저 파이썬 버전 관리 도구인 pyenv 를 설치한다.
OS에 기본 내장된 파이썬을 직접 사용하다 보면 권한 문제나 패키지 의존성 충돌이 발생하기 쉽다.
pyenv를 사용하면 프로젝트별로 독립된 파이썬 버전을 쉽게 설치하고 전환할 수 있다.
1) 사전 필수 패키지 설치
pyenv는 파이썬을 소스 코드로부터 컴파일하여 설치하는 방식을 사용한다. 따라서 빌드에 필요한 의존성 라이브러리들을 먼저 설치해야 한다.
$ ssh assu@127.0.0.1
# 패키지 목록 업데이트 및 필수 빌드 도구 설치
assu@myserver01:~$ sudo apt-get update; sudo apt-get install make build-essential libssl-dev \
zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncursesw5-dev xz-utils \
tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev
–
2) pyenv 자동 설치 스크립트 실행
공식적으로 제공되는 pyenv-installer 스크립트를 사용하여 설치를 진행한다. 이 스크립트는 pyenv 뿐만 아니라 가상 환경 관리를 위한 pyenv-virtualenv 플러그인까지 함께 설치해준다.
# `pyenv` 자동 설치 스크립트 실행
assu@myserver01:~$ curl https://pyenv.run | bash
설치가 완료되면 홈 디렉터리에 .pyenv 폴더가 생성된 것을 확인할 수 있다.
# 숨겨진 파일까지 조회
assu@myserver01:~$ ls -al
total 56
drwxr-x--- 6 assu assu 4096 Nov 22 05:25 .
drwxr-xr-x 3 root root 4096 May 12 2025 ..
-rw------- 1 assu assu 9199 Nov 22 05:25 .bash_history
-rw-r--r-- 1 assu assu 220 Mar 31 2024 .bash_logout
-rw-r--r-- 1 assu assu 3931 Nov 22 05:25 .bashrc
drwx------ 2 assu assu 4096 May 12 2025 .cache
-rw------- 1 assu assu 20 Sep 13 08:52 .lesshst
-rw-r--r-- 1 assu assu 807 Mar 31 2024 .profile
drwxrwxr-x 14 assu assu 4096 Nov 22 05:25 .pyenv
drwx------ 2 assu assu 4096 May 12 2025 .ssh
-rw-r--r-- 1 assu assu 0 May 12 2025 .sudo_as_admin_successful
-rw------- 1 assu assu 2464 Nov 22 05:25 .viminfo
drwxrwxr-x 3 assu assu 4096 Nov 17 05:19 work
3) 환경 변수 설정 (.bashrc)
설치된 pyenv 명령어를 셸 어디서든 사용할 수 있도록 환경 변수(PATH)를 설정해야 한다.
사용하는 셸의 설정 파일(여기서는 .bashrc)을 열어 아래 내용을 하단에 추가한다.
이는 pyenv의 실행 경로를 잡아주고, 셸이 시작될 때 pyenv와 virtualenv가 자동으로 초기화되도록 한다.
assu@myserver01:~$ vim ./bashrc
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - bash)"
eval "$(pyenv virtualenv-init -)"
4) 설정 적용 및 설치 확인
설정 파일을 수정했다면 현재 실행 중인 셸에 변경 사항을 적용해야 한다.
셸을 재시작(exec $SHELL)하거나, source ~/.bashrc를 실행한 뒤, 버전 확인 명령어로 설치가 정상적으로 되었는지 확인한다.
# 셸 재시작
assu@myserver01:~$ exec $SHELL
# 설치 확인
assu@myserver01:~$ pyenv --version
pyenv 2.6.13
1.2. pyenv를 활용한 파이썬 가상 환경 구축
pyenv 설치가 완료되었으니, 이제 실제 프로젝트를 위한 독립적인 파이썬 가상 환경을 구축해보자.
가상 환경을 사용하면 프로젝트마다 라이브러리 버전을 다르게 관리할 수 있어 의존성 충돌을 방지할 수 있다.
1) 파이썬 버전 설치
pyenv install --list 명령어로 설치 가능한 파이썬 버전을 확인한 후, 설치할 버전을 설치한다.
# 설치 가능한 파이썬 버전 리스트 확인
assu@myserver01:~$ pyenv install --list
Available versions:
2.1.3
// ...
3.13.9
# 파이썬 3.13.9 설치
assu@myserver01:~$ pyenv install 3.13.9
Downloading Python-3.13.9.tar.xz...
-> https://www.python.org/ftp/python/3.13.9/Python-3.13.9.tar.xz
Installing Python-3.13.9...
Installed Python-3.13.9 to /home/assu/.pyenv/versions/3.13.9
설치가 완료되면 pyenv versions 명령어로 현재 시스템에 설치된 파이썬 버전들을 확인할 수 있다.
# 버전 확인
assu@myserver01:~$ pyenv versions
* system (set by /home/assu/.pyenv/version)
3.13.9
2) 가상 환경 생성
설치한 파이썬 3.13.9를 기반으로 py3_13_9 라는 이름의 가상 환경을 생성한다.
# 가상 환경 생성: pyenv virtualenv [파이썬 버전] [가상환경이름]
assu@myserver01:~$ pyenv virtualenv 3.13.9 py3_13_9
# 생성 결과 확인
assu@myserver01:~$ pyenv versions
* system (set by /home/assu/.pyenv/version)
3.13.9
3.13.9/envs/py3_13_9
py3_13_9 --> /home/assu/.pyenv/versions/3.13.9/envs/py3_13_9
3) 가상 환경 활성화 및 필수 라이브러리 설치
가상 환경을 활성화(activate)하면 쉘 프롬프트 앞에 py3_13_9 가 표시된다. 이 상태에서 설치하는 모든 라이브러리는 해당 가상 환경에만 저장된다.
django- 파이썬 기반의 대표적인 웹 프레임워크
gunicorn- 웹 서버(Nginx)와 Django 애플리케이션을 연결해주는 WSGI 인터페이스
psycopg2-binary- 파이썬에서 PostgreSQL 데이터베이스를 사용하기 위한 어댑터
# 가상 환경 활성화
assu@myserver01:~$ pyenv activate py3_13_9
(py3_13_9) assu@myserver01:~$
# 웹 프레임워크인 django 설치
(py3_13_9) assu@myserver01:~$ pip install django
# 웹서버와 통신하기 위한 라이브러리인 gunicorn 설치
(py3_13_9) assu@myserver01:~$ pip install gunicorn
# 파이썬에서 PostgreSQL을 활용하기 위해 사용하는 라이브러리인 psycopg2 설치
(py3_13_9) assu@myserver01:~$ pip install psycopg2-binary
4) 설치 검증
파이썬 인터프리터를 실행하여 설치된 라이브러리들이 정상적으로 로드되는지, 버전은 맞는지 확인한다.
# 가상 환경에서 파이썬 실행
(py3_13_9) assu@myserver01:~$ python
Python 3.13.9 (main, Nov 22 2025, 06:17:02) [GCC 13.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
# 앞서 설치한 라이브러리 임포트
>>> import django
>>> import gunicorn
>>> import psycopg2
# 각 라이브러리 버전 확인
>>> django.__version__
'5.2.8'
>>> gunicorn.__version__
'23.0.0'
>>> psycopg2.__version__
'2.9.11 (dt dec pq3 ext lo64)'
# 파이썬 종료
>>> quit()
작업이 끝나면 source deactivate 명령어로 가상 환경을 종료한다.
# 가상 환경 비활성화
(py3_13_9) assu@myserver01:~$ source deactivate
pyenv-virtualenv: deactivate 3.13.9/envs/py3_13_9
assu@myserver01:~$
1.3. tree 설치
이후 진행될 도커 빌드 과정이나 프로젝트 구성을 설명할 때, 디렉터리 구조를 텍스트로 한 눈에 파악하는 매우 중요하다.
이를 위해 리눅스의 tree 유틸리티를 설치한다.
# tree 설치
assu@myserver01:~$ sudo apt install tree
[sudo] password for assu:
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
// ...
Running kernel seems to be up-to-date.
The processor microcode seems to be up-to-date.
No services need to be restarted.
No containers need to be restarted.
No user sessions are running outdated binaries.
No VM guests are running outdated hypervisor (qemu) binaries on this host.
설치 후 tree 명령어를 사용하면 하위 디렉터리 구조를 시각적으로 확인할 수 있다.
# 현재 디렉터리 구조 확인
assu@myserver01:~$ tree ./
./
└── work
└── ch04
└── ex01
├── test01.txt
└── test02.txt
1.4. django를 실행하기 위한 네트워크 설정
우리는 현재 로컬 호스트(내 컴퓨터) 위에 가상 머신(Ubuntu Server)를 띄우고, 그 안에서 도커와 Django를 실행할 예정이다. 하지만 가상 머신은 기본적으로 외부와 격리된 네트워크 환경을 가진다.
따라서 호스트(Host OS)의 브라우저에서 가상 머신(Guest OS) 내부의 웹 서비스에 접속하기 위해서는 포트 포워딩 설정이 필수적이다.
가상 머신 관리 도구(예: VirtualBox)의 네트워크 설정 메뉴를 통해 포트 포워딩 규칙을 정의한다.

위 설정의 핵심은 아래와 같다.
- 호스트 IP & Port: 내 컴퓨터(Host)의 특정 포트(8000, 80)로 들어오는 요청을 감지한다.
- 게스트 IP & Port: 해당 요청을 가상 머신(10.0.2.4)의 동일한 포트로 포워딩한다.
이 설정을 통해 이루어지는 통신 흐름을 도식화하면 아래와 같다.

주요 포트 매핑
- 8000번 포트
- Django 개발 서버(runserver)의 기본 포트이다.
- 호스트에서 localhost:8000 으로 접속하면 가상 머신의 Django 서비스로 연결된다.
- 80 포트
- HTTP 기본 포트이다.
- 추후 Nginx를 연동했을 때 웹 서버로 접속하기 위해 미리 열어둔다.
이 설정을 통해 우리는 가상 머신 내부의 복잡한 터미널 환경 대신 호스트의 쾌적한 웹 브라우저 환경에서 개발 결과물을 즉시 확인할 수 있다.
2. YAML 기초
도커 컴포즈나 쿠버네티스를 다루다 보면 .yml 혹은 .yaml 확장자를 가진 파일을 보게 된다.
YAML 은 YAML Ain’t Markup Language 의 약자로, 데이터의 정의와 구조에 집중한 사람이 읽기 쉬운 데이터 직렬화 양식이다.
JSON이나 XML보다 가독성이 뛰어나 설정 파일 작성에 표준처럼 사용된다.
여기서는 파이썬 라이브러리를 통해 YAML 파일을 직접 파싱해보며 구조와 문법을 알아본다.
2.1. pyyaml 설치
파이썬 코드 내에서 YAML 파일을 읽고 쓸 수 있게 해주는 라이브러리인 pyyaml을 설치한다.
앞서 생성한 가상환경인 py3_13_9에서 진행하여 환경을 격리한다.
# 1. 파이썬 가상 환경 활성화
assu@myserver01:~$ pyenv activate py3_13_9
# 2. pyyaml 라이브러리 설치
(py3_13_9) assu@myserver01:~$ pip install pyyaml
...
Successfully installed pyyaml-6.0.3
설치가 완료되면 파이썬 인터프리터를 실행하여 라이브러리가 정상적으로 로드되는지 확인한다.
# 파이썬 가상 환경에서 파이썬 실행
(py3_13_9) assu@myserver01:~$ python
Python 3.13.9 (main, Nov 22 2025, 06:17:02) [GCC 13.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
# 모듈 임포트 테스트
>>> import yaml
# 파이썬 종료
>>> quit()
# 파이썬 가상 환경 종료
(py3_13_9) assu@myserver01:~$ source deactivate
pyenv-virtualenv: deactivate 3.13.9/envs/py3_13_9
assu@myserver01:~$
2.2. YAML 문법과 파이썬 객체 매핑
YAML의 가장 큰 특징은 들여쓰기(indentation)을 통해 데이터의 계층 구조를 표현한다는 점이다.
실제 쿠버네티스에서 사용하는 Pod 정의 파일을 정의해보고, 이를 파이썬으로 파싱하여 데이터 구조가 어떻게 변환되는지 확인해본다.
1) YAML 파일 작성(yaml_practice.yml)
assu@myserver01:~/work/ch05/ex01$ vim yaml_practice.yml
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx:latest
- name: ubuntu
image: ubuntu:latest
- key:value 형태(예: kind: Pod)
- 계층 구조
- 스페이스 2칸 또는 4칸 들여쓰기로 하위 항목 표현
- 리스트(배열)는 하이픈(
-)으로 표현
2) 파이썬으로 YAML 파싱
이제 pyyaml을 사용하여 위 파일을 읽어온다. YAML 파일이 파이썬의 딕셔너리와 리스트로 어떻게 변환되는지 확인해보자.
# pyenv을 활용해 파이썬 가상 환경 실행
assu@myserver01:~/work/ch05/ex01$ pyenv activate py3_13_9
# 해당 가상 환경에서 파이썬 실행
(py3_13_9) assu@myserver01:~/work/ch05/ex01$ python
Python 3.13.9 (main, Nov 22 2025, 06:17:02) [GCC 13.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
# YAML 파일을 해석하기 위한 pyyaml 라이브러리 로드
>>> import yaml
# 파이썬의 open() 함수를 이용해 yml 파일 열기
>>> raw = open("/home/assu/work/ch05/ex01/yaml_practice.yml", "r+")
# yaml 라이브러리의 load() 함수를 활용해 데이터 로드
>>> data = yaml.load(raw, Loader=yaml.SafeLoader)
# 변환된 전체 데이터 구조 확인
>>> data
{'apiVersion': 'v1', 'kind': 'Pod', 'metadata': {'name': 'nginx'}, 'spec': {'containers': [{'name': 'nginx', 'image': 'nginx:latest'}, {'name': 'ubuntu', 'image': 'ubuntu:latest'}]}}
3) 데이터 구조 분석
로딩된 data 객체는 파이썬의 딕셔너리이다. key를 통해 계층적으로 데이터에 접근할 수 있다.
# 최상위 키 접근
>>> data['apiVersion']
'v1'
# 중첩된 딕셔너리 접근 (metadata -> name)
>>> data['metadata']
{'name': 'nginx'}
>>> data['metadata']['name']
'nginx'
# 리스트(배열) 접근 (spec -> containers)
>>> data['spec']['containers']
[{'name': 'nginx', 'image': 'nginx:latest'}, {'name': 'ubuntu', 'image': 'ubuntu:latest'}]
# 리스트 내부 요소 접근
# containers 리스트의 첫 번째 요소(Nginx)
>>> data['spec']['containers'][0]['name']
'nginx'
- YAML의 들여쓰기는 파이썬 딕셔너리의 중첩(Nesting)으로 변환된다.
- YAML의 하이픈은 파이썬의 리스트로 변환된다.
이 구조를 이해하면, 복잡한 쿠버네티스 설정 파일도 결국 거대한 딕셔너리와 리스트의 조합이라는 것을 알 수 있다.
# 파이썬 종료
>>> quit()
# 파이썬 가상 환경 종료
(py3_13_9) assu@myserver01:~/work/ch05/ex01$ source deactivate
pyenv-virtualenv: deactivate 3.13.9/envs/py3_13_9
3. 도커를 활용한 django 실행
이제 본격적으로 도커를 활용해 Django 웹 서비스를 실행해보자.
- 도커 호스트(VM)에 Django 프로젝트 생성: 소스 코드를 준비하고 로컬에서 테스트
- Django 이미지 빌드: 작성한 코드를 기반으로 도커 이미지 생성
- Django 컨테이너 실행: 이미지를 기반으로 컨테이너를 띄워 서비스 시작
3.1. 도커 호스트에 Django 프로젝트 생성
Django는 “Batteries Included”라는 철학을 가진 파이썬 대표 웹 프레임워크로, 웹사이트 개발에 필요한 많은 기능을 기본적으로 제공한다.
1) 프로젝트 생성
위에서 설정한 파이썬 가상 환경인 py3_13_9를 활성화한 후, django-admin 명령어를 사용해 프로젝트를 생성한다.
# 작업 디렉터리 확인 및 가상 환경 활성화
assu@myserver01:~/work/ch05/ex02$ pwd
/home/assu/work/ch05/ex02
assu@myserver01:~/work/ch05/ex02$ pyenv activate py3_13_9
# 'myapp'이라는 이름의 Django 프로젝트 생성
(py3_13_9) assu@myserver01:~/work/ch05/ex02$ django-admin startproject myapp
(py3_13_9) assu@myserver01:~/work/ch05/ex02$ ls
myapp
프로젝트가 생성되면 tree 명령어로 디렉터리 구조를 확인해본다.
(py3_13_9) assu@myserver01:~/work/ch05/ex02$ tree ./
./
└── myapp
├── manage.py # 프로젝트 관리 유틸리티
└── myapp # 프로젝트 설정 패키지
├── asgi.py
├── __init__.py
├── settings.py # 전반적인 설정 파일
├── urls.py # URL 라우팅 설정
└── wsgi.py # 웹 서버 게이트웨이 인터페이스
3 directories, 6 files
2) 호스트 접근 허용 설정: settings.py
Django는 보안상의 이유로 미리 지정된 호스트(도메인 또는 IP)에서만 접근을 허용한다.
우리는 현재 가상 머신 외부(호스트 PC)에서 접속하거나, 추후 도커 컨테이너 환경에서 실행할 예정이므로 모든 호스트에서의 접근을 허용하도록 설정을 변경해야 한다.
(py3_13_9) assu@myserver01:~/work/ch05/ex02$ cd myapp/myapp
(py3_13_9) assu@myserver01:~/work/ch05/ex02/myapp/myapp$ vim settings.py
settings.py 파일 내의 ALLOWED_HOSTS 리스트를 찾아 아래와 같이 수정한다.
# 수정 전
ALLOWED_HOSTS = []
# 수정 후: 모든 호스트 허용 ('*')
ALLOWED_HOSTS = ['*']
ALLOWED_HOSTS = ['*']설정은 개발 환경에서의 편의를 위한 것이다.
실제 운영 환경에서는 보안을 위해 반드시 실제 사용하는 도메인이나 IP 주소만 명시해야 한다.
3) 데이터베이스 마이그레이션: migrate
Django 프로젝트를 실행하기 전, 데이터베이스 초기화가 필요하다.
migrate 명령어는 Django의 모델(Model) 변경 사항을 실제 데이터베이스 스키마에 적용하는 역할을 한다. 별도의 DB 설정이 없다면 기본적으로 sqlite3 파일 기반 DB를 사용한다.
# 프로젝트 루트 디렉터리(manage.py가 있는 곳)로 이동
(py3_13_9) assu@myserver01:~/work/ch05/ex02/myapp/myapp$ cd ..
(py3_13_9) assu@myserver01:~/work/ch05/ex02/myapp$ ls
manage.py myapp
# 마이그레이션 실행
(py3_13_9) assu@myserver01:~/work/ch05/ex02/myapp$ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
...
Applying sessions.0001_initial... OK
4) 개발 서버 실행 및 접속 확인
이제 Django 내장 개발 서버를 실행한다. 여기서 중요한 점은 IP 주소를 0.0.0.0 으로 지정하는 것이다.
- 127.0.0.1(localhost): 가상 머신 내부에서만 접속 가능
- 0.0.0.0: 모든 네트워크 인터페이스에 바인딩(외부 접속 허용)
# 8000번 포트로 서버 실행 (모든 인터페이스 허용)
(py3_13_9) assu@myserver01:~/work/ch05/ex02/myapp$ python manage.py runserver 0.0.0.0:8000
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
November 23, 2025 - 03:33:12
Django version 5.2.8, using settings 'myapp.settings'
Starting development server at http://0.0.0.0:8000/
Quit the server with CONTROL-C.
서버가 정상적으로 실행되었다면 호스트 PC(맥/윈도우)의 브라우저를 열고 http://127.0.0.1:8000 주소로 접속한다.
아래와 같이 로켓이 발사되는 Django 환영 홈페이지가 보인다면, 프로젝트 생성 및 기본 설정이 완료된 것이다.

3.2. Django 이미지 빌드
앞서 Django 프로젝트가 로컬 환경에서 정상 작동하는 것을 확인하였다. 이제 이 프로젝트를 도커 이미지로 만들어 배포 가능한 상태로 패키징해보자.
전체적인 빌드 흐름은 아래 그림과 같이 호스트의 소스 코드를 도커 이미지 내부로 복사하고, 필요한 라이브러리를 설치하는 과정으로 이루어진다.

1) 디렉터리 구조 정리
기존의 ex02 디렉터리를 ex03 으로 복사하여 작업을 진행한다.
(py3_13_9) assu@myserver01:~/work/ch05$ pwd
/home/assu/work/ch05
(py3_13_9) assu@myserver01:~/work/ch05$ cp -r ex02 ex03
(py3_13_9) assu@myserver01:~/work/ch05$ ls
ex01 ex02 ex03
(py3_13_9) assu@myserver01:~/work/ch05$ cd ex03
# 파일 구조 확인
(py3_13_9) assu@myserver01:~/work/ch05/ex03$ tree ./ -L 3
./
└── myapp
├── db.sqlite3
├── manage.py
└── myapp
├── asgi.py
├── __init__.py
├── __pycache__
├── settings.py
├── urls.py
└── wsgi.py
4 directories, 7 files
2) 의존성 명시: requirements.txt
컨테이너 내부에는 우리가 로컬 가상 환경에 설치했던 라이브러리들이 없다. 따라서 이미지 빌드 시 설치해야 할 패키지 목록은 requirements.txt 파일에 명시해야 한다.

(py3_13_9) assu@myserver01:~/work/ch05/ex03$ ls
myapp
(py3_13_9) assu@myserver01:~/work/ch05/ex03$ vim requirements.txt
# 파일에 django 버전을 적어준다.
(py3_13_9) assu@myserver01:~/work/ch05/ex03$ cat requirements.txt
django==4.2.7
(py3_13_9) assu@myserver01:~/work/ch05/ex03$ ls
myapp requirements.txt
(py3_13_9) assu@myserver01:~/work/ch05/ex03$
3) Dockerfile 작성
Dockerfile은 도커 이미지를 생성하기 위한 설계도이다. 베이스 이미지 선택부터 파일 복사, 명령어 실행까지의 모든 과정을 순서대로 기술한다.

(py3_13_9) assu@myserver01:~/work/ch05/ex03$ vim Dockerfile
(py3_13_9) assu@myserver01:~/work/ch05/ex03$ ls
Dockerfile myapp requirements.txt
(py3_13_9) assu@myserver01:~/work/ch05/ex03$ cat Dockerfile
# 베이스 이미지 지정 (Python 3.13.9)
FROM python:3.13.9
# 작업 디렉터리 설정 (컨테이너 내부 경로)
WORKDIR /usr/src/app
# 호스트의 모든 파일을 컨테이너 작업 디렉터리로 복사
COPY . .
# pip 업그레이드 및 의존성 패키지 설치
RUN python -m pip install --upgrade pip
RUN pip install -r requirements.txt
# Django 프로젝트 내부로 작업 디렉터리 변경
WORKDIR ./myapp
# 컨테이너 실행 시 작동할 명령어 (서버 구동)
CMD python manage.py runserver 0.0.0.0:8000
# 8000번 포트 노출 선언
EXPOSE 8000
FROM- 이미지 생성의 기반이 되는 베이스 이미지를 지정한다.
- 여기서는 파이썬 3.13.9 버전이 설치된 리눅스 환경을 가져온다.
WORKDIR- 리눅스의
cd명령어와 유사하다. - 이후 실행되는 명령어들이 수행될 컨테이너 내부의 작업 경로를 지정한다.
- 리눅스의
COPY . .COPY [호스트 경로] [컨테이너 경로]형식이다.COPY . .는 호스트의 현재 위치(ex03)에 있는 모든 파일을 컨테이너의 현재 위치(/usr/src/app)로 복사한다.
RUN- 이미지를 빌드(Build)하는 시점에 실행되는 명령어이다.
- 주로 패키지 설치 등에 사용된다.
CMD- 만들어진 이미지를 바탕으로 컨테이너를 실행(Run)할 때 수행되는 명령어이다.
- 여기서는 Django 개발 서버를 띄우는 명령을 입력했다.
EXPOSE- 이 컨테이너가 8000번 포트를 사용한다는 것을 문서화하고 명시한다.
RUNvsCMD
RUN: 이미지를 만드는 과정에서 실행된다. (결과가 이미지 레이어에 저장됨)
CMD: 이미지가 다 만들어진 후, 컨테이너가 시작될 때 실행된다. (애플리케이션 구동)
4) 도커 이미지 빌드
이제 작성한 Dockerfile 과 소스 코드를 바탕으로 실제 이미지를 빌드한다.
도커 클라이언트에 빌드 명령을 내리면 도커 데몬이 Dockerfile의 지시 사항을 한 줄씩 수행하여 이미지를 생성한다.


# 도커 이미지 빌드 (이미지 이름: weweb01)
(py3_13_9) assu@myserver01:~/work/ch05/ex03$ docker image build . -t weweb01
...
=> [2/6] WORKDIR /usr/src/app 0.5s
=> [3/6] COPY . . 0.0s
=> [4/6] RUN python -m pip install --upgrade pip 2.7s
=> [5/6] RUN pip install -r requirements.txt 2.4s
=> [6/6] WORKDIR ./myapp 0.0s
=> exporting to image 0.3s
=> => exporting layers 0.3s
=> => writing image sha256:d7f65add56eb3fe94cbe9b85daf60543a3eca3eb7865406b5f90620a4ee284a7 0.0s
=> => naming to docker.io/library/weweb01 0.0s
1 warning found (use docker --debug to expand):
- JSONArgsRecommended: JSON arguments recommended for CMD to prevent unintended behavior related to OS signals (line 12)
build .- 현재 디렉터리(
.)를 빌드 컨텍스트로 지정한다. 이 경로에 있는 파일들이 데몬으로 전송된다.
- 현재 디렉터리(
-t weweb01- 생성될 이미지의 이름(tag)을 지정한다.
빌드 결과 확인
(py3_13_9) assu@myserver01:~/work/ch05/ex03$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
weweb01 latest d7f65add56eb About a minute ago 1.18GB
weweb01 이라는 이미지가 정상적으로 생성된 것을 확인할 수 있다. 이제 이 이미지는 어디서든 실행 가능한 ‘패키징 된 애플리케이션’이 되었다.
3.3. Django 컨테이너 실행
이미지 빌드에 성공했다면, 이제 그 이미지를 바탕으로 컨테이너를 실행하여 실제 웹 서비스를 배포할 차례이다.
1) 컨테이너 실행: docker container run
weweb01 이미지를 사용하여 컨테이너를 실행한다. 이 때 백그라운드 실행과 포트 연결을 위한 옵션을 함께 사용한다.
assu@myserver01:~$ docker container run -d -p 8000:8000 weweb01
012547e5aa1562aac9fb5eb5df31832c95b0d1638e473e7ff22ccd0622cffe76
-d(Detached Mode)- 컨테이너를 백그라운드에서 실행한다. 이 옵션이 없으면 터미널이 컨테이너의 로그로 점유되어 다른 작업을 할 수 없다.
-p(Publish Port)- 사용법:
-p [도커 호스트 포트]:[컨테이너 포트] - 예시:
-p 8000:8000은 호스트(VM)의 8000번 포트로 들어오는 요청을 컨테이너 내부의 8000번 포트로 전달하라는 의미이다.
- 사용법:
2) 실행 상태 확인
컨테이너가 정상적으로 떠 있는지 확인한다.
assu@myserver01:~$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
012547e5aa15 weweb01 "/bin/sh -c 'python …" 3 seconds ago Up 3 seconds 0.0.0.0:8000->8000/tcp, [::]:8000->8000/tcp epic_wozniak
STATUS가 Up 상태이고, PORTS 항목에 0.0.0.0:8000->8000/tcp 가 표시되어 있다면 포트포워딩이 정상적으로 설정된 것이다.
3) 접속 테스트
이제 호스트 PC(맥/윈도우)의 웹 브라우저에서 http://127.0.0.1:8000에 접속해본다.
방금 전 로컬에서 python manage.py runserver로 띄웠을 때와 동일한 Django 환영 페이지가 보인다면 성공적으로 컨테이너 배포가 완료된 것이다.
4) 네트워크 구조 이해
도커 컨테이너는 실행 시 자신만의 고유한 내부 IP 주소를 할당받는다. 이를 확인하기 위해 inspect 명령어를 사용해보자.
# 브리지 네트워크 상세 정보 확인 및 IPv4 주소 필터링
assu@myserver01:~/work/ch05/ex03$ docker network inspect bridge | grep IPv4Address
"IPv4Address": "172.17.0.2/16",
위 결과에서 보듯 컨테이너는 172.17.0.X 대역의 사설 IP를 가지고 있다. 하지만 외부 사용자는 이 내부 IP를 알 필요가 없다. 우리가 설정한 포트포워딩 규칙 덕분이다.
전체적인 통신 흐름을 그림으로 정리하면 아래와 같다.

- 사용자(Browser): 127.0.0.1:8000 으로 접속 요청
- Host OS(Local PC): 포트포워딩 설정을 통해 가상머신(10.0.2.4:8000)으로 전달
- Docker Deamon(VM): 8000번 포트로 들어온 요청을 감지하여 컨테이너(172.17.0.2:8000)로 전달
- Django Container: 요청 처리 후 응답 반환
이로써 소스 코드를 이미지로 패키징하고, 이를 어디서든 동일하게 실행할 수 있는 컨테이너 환경을 구축했다.
4. Nginx, Django 연동 후 실행
지금까지는 Django 개발 서버(runserver)를 이용해 단일 컨테이너로 서비스를 실행했다. 하지만 실제 운영 환경에서는 성능, 보안, 정적 파일 처리 등을 위해 웹 서버(Nginx)와 WAS(Django)를 분리하여 구성하는 것이 표준이다.
여기서는 Django 앞에 Nginx를 배치하고, Gunicorn을 통해 이 둘을 연동하는 구조를 만들어본다.
4.1. Nginx 컨테이너 실행
본격적인 연동에 앞서, 먼저 Nginx를 도커 컨테이너로 단독 실행해보며 기본 동작 방식을 익혀본다.
1) 디렉터리 준비

assu@myserver01:~/work/ch05$ cd ex04
assu@myserver01:~/work/ch05/ex04$
2) Nginx Dockerfile 작성
Nginx 이미지를 빌드하기 위한 Dockerfile을 작성한다.

assu@myserver01:~/work/ch05$ cd ex04
assu@myserver01:~/work/ch05/ex04$ vim Dockerfile
Dockerfile 내용
FROM nginx:1.25.3
CMD ["nginx", "-g", "daemon off;"]
FROM nginx:1.25.3- 안정적인 서비스를 위해 구체적인 버전을 명시한 공식 Nginx 이미지를 베이스로 사용한다.
CMD ["nginx", "-g", "daemon off;"]- CMD 명령어를 활용할 때 대괄호(
[])를 사용하는 Exec Form과 사용하지 않는 Shell Form이 있다. - Exec Form(
[".."]) 권장- JSON 배열 형태로 명령어를 전달하면 쉘(/bin/sh)을 거치지 않고 프로세스가 실행된다. 이는 OS 시그널(예: 종료 신호)을 프로세스가 즉시 받을 수 있어 컨테이너 관리에 유리하다.
- CMD 명령어를 활용할 때 대괄호(
daemon off;- Nginx는 기본적으로 백그라운드 데몬으로 실행되도록 설계되었다.
- 하지만 도커 컨테이너는 메인 프로세스가 종료되면 컨테이너도 함께 종료된다.
- 따라서
daemon off;옵션을 주어 Nginx가 포그라운드에서 계속 실행되도록 강제해야 컨테이너가 죽지 않고 유지된다.
3) Nginx 이미지 빌드
작성한 Dockerfile을 기반으로 이미지를 빌드한다.


# 도커 이미지 빌드(태그: mynginx01)
assu@myserver01:~/work/ch05/ex04$ docker image build . -t mynginx01
[+] Building 9.9s (5/5) FINISHED docker:default
=> [internal] load build definition from Dockerfile 0.0s
... 0.0s
=> => naming to docker.io/library/mynginx01 0.0s
# 생성된 이미지 확인
assu@myserver01:~/work/ch05/ex04$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
weweb01 latest d7f65add56eb 2 hours ago 1.18GB
mynginx01 latest e0d1e1d4b5dd 2 years ago 192MB
4) Nginx 컨테이너 실행 및 접속 확인
빌드된 이미지를 컨테이너로 실행한다. 웹 서비스의 표준 포트인 80번을 사용한다.

# 80번 포트 포워딩 및 백그라운드 실행
assu@myserver01:~/work/ch05/ex04$ docker container run -p 80:80 -d mynginx01
328786484f24037341b2b1332e7a4a81f2f06d8ad4d90648dc7be23339820544
# 실행 상태 확인 (Django와 Nginx 두 개의 컨테이너가 실행 중)
assu@myserver01:~/work/ch05/ex04$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
328786484f24 mynginx01 "/docker-entrypoint.…" 35 seconds ago Up 34 seconds 0.0.0.0:80->80/tcp, [::]:80->80/tcp jolly_antonelli
012547e5aa15 weweb01 "/bin/sh -c 'python …" 2 hours ago Up 2 hours 0.0.0.0:8000->8000/tcp, [::]:8000->8000/tcp epic_wozniak
이제 호스트 브라우저에서 http://127.0.0.1 로 접속하면 Nginx의 기본 환영 페이지를 확인할 수 있다.

5) 컨테이너 내부 설정 파일 확인
추후 Django와의 연동을 위해 Nginx 설정 파일의 위치를 파악해두자.
exec 명령어로 실행 중인 컨테이너 내부에 접속할 수 있다.
# 컨테이너 내부 쉘 접속
assu@myserver01:~/work/ch05/ex04$ docker container exec -it 328786484f24 /bin/bash
# 설정 파일 위치 확인
root@328786484f24:/# cd /etc/nginx/conf.d
root@328786484f24:/etc/nginx/conf.d# ls
default.conf
# 기본 설정 내용 확인
root@328786484f24:/etc/nginx/conf.d# cat default.conf
server {
listen 80;
listen [::]:80;
server_name localhost;
...
}
# 컨테이너 접속 종료
root@328786484f24:/etc/nginx/conf.d# exit
exit
확인된 default.conf 파일은 앞으로 우리가 Django 서버로 요청을 넘겨주기 위해(Reverse Proxy) 수정하게 될 핵심 파일이다.
6) 리소스 정리
# 실행 중인 컨테이너 ID를 사용하여 중지
assu@myserver01:~/work/ch05/ex04$ docker container stop 328786484f24 012547e5aa15
328786484f24
012547e5aa15
# 종료 확인
assu@myserver01:~/work/ch05/ex04$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
4.2. gunicorn 을 통한 연동
위에서는 Django와 Nginx를 각각 독립적으로 실행해보았다. 이제 이 둘을 하나의 서비스 흐름으로 연결해야 한다.
하지만 Nginx(웹 서버)는 파이썬 코드를 직접 해석하거나 실행할 수 없다.
이 때 WSGI(Web Server Gateway Interface) 서버인 Gunixorn이다.
gunicorn은 Nginx로부터 받은 HTTP 요청을 파이썬이 이해할 수 있는 형태로 변환하여 Django에 전달하고, Django의 처리 결과를 다시 Nginx에게 돌려주는 중개자 역할을 한다.
즉, Nginx ↔ gunicorn ↔ Django의 구조가 완성이다.

4.3. Django 이미지 빌드(gunicorn 포함)
gunicorn을 사용하기 위해 기존 Django 프로젝트 구조를 수정하고 이미지를 다시 빌드해야 한다.
1) Django 디렉터리 정리

assu@myserver01:~/work/ch05$ mkdir ex05
# ex03, ex04 디렉터리를 ex05 디렉터리로 복사
# ex03은 django 였고, ex04는 nginx 였다.
assu@myserver01:~/work/ch05$ cp -r ex03 ex04 ex05
assu@myserver01:~/work/ch05$ cd ex05
assu@myserver01:~/work/ch05/ex05$ ll
total 16
drwxrwxr-x 4 assu assu 4096 Nov 29 05:31 ./
drwxrwxr-x 7 assu assu 4096 Nov 29 05:30 ../
drwxrwxr-x 3 assu assu 4096 Nov 29 05:31 ex03/
drwxrwxr-x 2 assu assu 4096 Nov 29 05:31 ex04/
assu@myserver01:~/work/ch05/ex05$ mv ex03 myDajngo02
assu@myserver01:~/work/ch05/ex05$ mv ex04 myNginx02
drwxrwxr-x 4 assu assu 4096 Nov 29 05:31 ../
이제 Django 설정 파일인 requirements.txt 와 Dockerfile 을 수정한다.
먼저 Django 이미지가 실행될 때 gunicorn을 통해 구동되도록 설정을 변경한다.
requirements.txt: gunicorn 라이브러리 추가
assu@myserver01:~/work/ch05/ex05/myDajngo02$ ls
Dockerfile myapp requirements.txt
assu@myserver01:~/work/ch05/ex05/myDajngo02$ cat requirements.txt
django==4.2.7
gunicorn==20.1.0
Dockerfile: 실행 명령어(CMD)를 runserver 에서 gunicorn 으로 변경
assu@myserver01:~/work/ch05/ex05/myDajngo02$ cat Dockerfile
FROM python:3.13.9
WORKDIR /usr/src/app
COPY . .
RUN python -m pip install --upgrade pip
RUN pip install -r requirements.txt
WORKDIR ./myapp
# 개발용 서버(runserver) 대신 프로덕션용 WSGI 서버(gunicorn) 실행
# 문법: gunicorn --bind [주소]:[포트] [프로젝트명].wsgi:application
CMD gunicorn --bind 0.0.0.0:8000 myapp.wsgi:application
EXPOSE 8000
2) Django 이미지 빌드(myweb02)
변경된 설정을 반영하여 새로운 이미지를 빌드한다.

# 이미지 빌드
assu@myserver01:~/work/ch05/ex05/myDajngo02$ docker image build . -t myweb02
[+] Building 9.0s (11/11) FINISHED docker:default
=> [internal] load build definition from Dockerfile 0.0s
...
# 생성 확인
assu@myserver01:~/work/ch05/ex05/myDajngo02$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
myweb02 latest 304ff34def4f 2 minutes ago 1.19GB
4.4. Nginx 이미지 빌드(Reverse Proxy 설정)
다음으로 Nginx가 들어온 요청을 Django 컨테이너로 토스(Reverse Proxy)할 수 있도록 설정을 변경한다.
3) Nginx 설정 파일 작성(default.conf, Dockerfile)

default.conf 파일을 추가한다.
assu@myserver01:~/work/ch05/ex05/myNginx02$ cat default.conf
# upstream 블록: 요청을 보낼 백엔드 서버 그룹 정의
upstream myweb{
# djangotest는 추후 실행할 Django 컨테이너의 이름
# 도커 네트워크 내부 DNS를 통해 IP로 자동 해석됨
server djangotest:8000;
}
server{
listen 80;
server_name localhost;
location /{
# 들어온 요청을 upstream 'myweb'으로 전달
proxy_pass http://myweb;
}
}
즉, Nginx는 80번 포트로 받은 요청을 djangotest 컨테이너의 8000번 포트로 전송한다는 의미이다.
작성한 default.conf 파일을 컨테이너 내부의 설정 경로로 덮어쓰도록 Dockerfile을 수정한다.
assu@myserver01:~/work/ch05/ex05/myNginx02$ cat Dockerfile
FROM nginx:1.25.3
# 기존 설정 삭제
RUN rm /etc/nginx/conf.d/default.conf
# 커스텀 설정 복사
COPY default.conf /etc/nginx/conf.d/
CMD ["nginx", "-g", "daemon off;"]
4) Nginx 이미지 빌드

assu@myserver01:~/work/ch05/ex05/myNginx02$ docker image build . -t mynginx02
[+] Building 2.9s (8/8) FINISHED docker:default
=> [internal] load build definition from Dockerfile 0.0s
...
# 생성 확인
assu@myserver01:~/work/ch05/ex05/myNginx02$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
mynginx02 latest 0339a129e503 19 seconds ago 192MB
4.5. Django와 Nginx 연동 후 컨테이너 실행
이제 Django 컨테이너와 Nginx 컨테이너를 연동해서 하나의 네트워크로 묶어 서비스를 완성해본다.

5) 도커 네트워크 생성
컨테이너들이 이름(djangotest)으로 서로를 찾을 수 있도록 사용자 정의 브리지 네트워크를 생성한다.
# 현재 네트워크 목록 확인
assu@myserver01:~/work/ch05/ex05/myNginx02$ docker network ls
NETWORK ID NAME DRIVER SCOPE
a37c58c71116 bridge bridge local
26dfd2bd80ec host host local
426836c8786b none null local
# mynetwork02 네트워크 생성
assu@myserver01:~/work/ch05/ex05/myNginx02$ docker network create mynetwork02
06dbba6ab19f6d020e6f2c3fa93ad23aedd8c661c43bdf6727edcba0c31cfdc2
# 생성된 네트워크 확인
assu@myserver01:~/work/ch05/ex05/myNginx02$ docker network ls
NETWORK ID NAME DRIVER SCOPE
a37c58c71116 bridge bridge local
26dfd2bd80ec host host local
06dbba6ab19f mynetwork02 bridge local
426836c8786b none null local
6) 컨테이너 실행 및 연동
Django 컨테이너 실행(djangotest)
Django는 외부에서 직접 접속할 필요가 없으므로 -p(포트 포워딩) 옵션을 사용하지 않는다. 오직 Nginx와 통신하면 된다.
assu@myserver01:~/work/ch05/ex05/myNginx02$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
mynginx02 latest 0339a129e503 10 minutes ago 192MB
myweb02 latest 304ff34def4f 24 minutes ago 1.19GB
# django 컨테이너 실행
# -d 를 이용해 백그라운드 실행을 하고, --name 옵션을 활용해 컨테이너 이름을 djangotest라고 짓는다. 네트워크는 mynetwork02를 사용한다.
assu@myserver01:~/work/ch05/ex05/myNginx02$ docker container run -d --name djangotest --network mynetwork02 myweb02
8b4b6fb4c3b6fc943755787cc1f92bfdd971fb4533ee7c692e274a1fbb0bdbca
assu@myserver01:~/work/ch05/ex05/myNginx02$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
8b4b6fb4c3b6 myweb02 "/bin/sh -c 'gunicor…" 13 seconds ago Up 13 seconds 8000/tcp djangotest
Nginx 컨테이너 실행(nginxtest)
Nginx는 사용자의 요청을 받아야 하므로 -p 80:80을 통해 호스트 포트를 개방한다.
# Nginx 컨테이너를 실행한다.
# -p 옵션으로 호스트의 80번 포트와 컨테이너의 80번 포트를 연결한다.
assu@myserver01:~/work/ch05/ex05/myNginx02$ docker container run -d --name nginxtest --network mynetwork02 -p 80:80 mynginx02
3b4c3a7434e4ef248d1e4dec7472ffb85514835b335022668fa2d08217e283d0
assu@myserver01:~/work/ch05/ex05/myNginx02$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3b4c3a7434e4 mynginx02 "/docker-entrypoint.…" 6 seconds ago Up 5 seconds 0.0.0.0:80->80/tcp, [::]:80->80/tcp nginxtest
8b4b6fb4c3b6 myweb02 "/bin/sh -c 'gunicor…" 55 seconds ago Up 55 seconds 8000/tcp djangotest
7) 접속 테스트
호스트 PC의 브라우저에서 http://127.0.0.1:80 으로 접속한다.
- 사용자가 80번 포트로 요청을 보냄
- Nginx가 받아서 mynetwork02 네트워크를 통해 Django(8000)으로 전달
- Gunicorn이 요청 처리 후 응답 반환
브라우저에 Django 환영 페이지가 뜬다면 Nginx - gunicorn - Django 로 이어지는 웹 애플리케이션 인프라가 정상적으로 구축된 것이다.

이제 컨테이너를 정지시킨다.
assu@myserver01:~/work/ch05/ex05/myNginx02$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3b4c3a7434e4 mynginx02 "/docker-entrypoint.…" 6 seconds ago Up 5 seconds 0.0.0.0:80->80/tcp, [::]:80->80/tcp nginxtest
8b4b6fb4c3b6 myweb02 "/bin/sh -c 'gunicor…" 55 seconds ago Up 55 seconds 8000/tcp djangotest
assu@myserver01:~/work/ch05/ex05/myNginx02$ docker container stop 3b4c3a7434e4 8b4b6fb4c3b6

위 그림은 호스트의 웹 브라우저에서 주소창에 127.0.0.1:80 을 입력하면 포트포워딩 기능을 통해 10.0.2.4:80 트래픽이 전달된다. 그리고 나서 nginxtest 컨테이너를 통과한 후 djangotest 컨테이너에 접속하게 되어 django 서비스를 활용하게 된다.
참고 사이트 & 함께 보면 좋은 사이트
본 포스트는 장철원 저자의 한 권으로 배우는 도커&쿠버네티스를 기반으로 스터디하며 정리한 내용들입니다.
