Ctrl + Shift + ESC

3티어 웹 앱 컨테이너화 해보기 본문

Linux

3티어 웹 앱 컨테이너화 해보기

단축키실행해보세요 2025. 11. 21. 22:57

* 해당 포스트는 실무 초밀착 리눅스: 클라우드 환경 운영부터 성능분석까지 강의를 듣고 정리한 것입니다.

 

n-티어 아키텍처란?

https://learn.microsoft.com/ko-kr/azure/architecture/guide/architecture-styles/n-tier

 

n-티어 아키텍처란 애플리케이션을 n개의 레이어으로 나눈 아키텍처를 말한다.

레이어은 물리적으로 구분되고, 별도의 컴퓨터에서 실행된다. 이를 통해 책임을 분리하고 종속성을 관리한다.

레이어은 다른 레이어를 직접 호출하거나 메시지 큐를 통해 비동기 메시징 패턴을 사용할 수 있다.

또한 상위 레이어은 하위 레이어의 서비스를 사용할 수 있지만, 하위 레이어는 상위 레이어의 서비스를 사용할 수 없다.

 

n-티어 애플리케이션에는 폐쇄형 레이어 아키텍처 또는 개방형 레이어 아키텍처가 있다.

  • 폐쇄형 레이어 아키텍처: 자기 다음의 레이어로만 접근이 가능한 통신 모델
  • 개방형 레이어 아키텍처: 자기의 하위 레이어면 전부 접근 가능한 통신 모델

일반적으로 3계층 애플리케이션에는 프레젠테이션 레이어(프론트), 미들 레이어(백), 데이터베이스 레이어로 구성된다.

 

실습 준비

# 샘플 프로젝트 가져오기
cd projects
git clone https://github.com/go4real/Django-Poll-App.git

# python package installer 설치 
sudo apt install -y python3-pip

# 설치된 Django는 버전이 3.1.14 버전이어서 Python 3.10 버전과 호환된다.
# 하지만 Python 3.10 버전은 Ubuntu 24.04 LTS 버전에서 바로 설치할 수 없으므로 사전 작업을 진행해야 한다.
sudo apt update
sudo apt install software-properties-common -y
sudo add-apt-repository ppa:deadsnakes/ppa -y
sudo apt update
sudo apt install python3.10 python3.10-venv python3.10-distutils -y

# Ubuntu는 시스템 패키지(apt)로 Python을 관리하고 있는데, pip install로 시스템 Python 폴더에 패키지를 설치하면 운영체제가 사용하는 Python 패키지가 충돌하거나 깨질 위험이 있으므로 가상 환경을 만들어 진행함
python3.10 -m venv venv
source venv/bin/activate

# 어플리케이션 실행에 필요한 관련 패키지 설치
pip install -r requirements.txt

# 데이터베이스 스키마 자동 생성
python3 manage.py migrate

# 어드민 메뉴 사용에 필요한 관리자 계정 생성  (username: fast / password: 1234)
python3 manage.py createsuperuser

# Dummy data 생성
pip3 install faker
python3 manage.py shell
import seeder
seeder.seed_all(30)

# 로컬 서버 구동
python3 manage.py runserver

 

 

여기까지 잘 따라왔다면 다음과 같은 화면이 보일 것이고, (username: fast / password: 1234)로 로그인이 될 것이다.

 

Docker Cheeet Sheet

Docker Container Lifecycle & Start, Stop

  • docker run : 명령 실행
  • docker rm: 중단된 컨테이너 삭제
  • docker start : 컨테이너 시작
  • docker stop : 컨테이너 중지

Docker Info & Executing Commands

  • docker ps: 실행 중인 컨테이너 확인
  • docker logs : 컨테이너 로그 확인
  • docker inspect : 컨테이너 상세 정보 확인
  • docker exec -it <컨테이너명> <명령어> : 컨테이너 안에서 명령어 실행

Docker Image Lifecycle

  • docker images : 이미지 목록 확인
  • docker build : Dockerfile로 도커 이미지 생성
  • docker rmi : 이미지 삭제

Docker Volume

  • docker volume create : 컨테이너 스토리지는 데이터를 휘발성으로 저장함, 스토리지를 볼륨으로 따로 구성해 컨테이너에 연결
  • docker volume rm : 볼륨 삭제
  • docker volume ls : 도커 볼륨 목록 확인
  • docker volume inspect : 도커 볼륨 상세 정보 확인

 

Docker 설치

https://docs.docker.com/engine/install/linux-postinstall/

 

Post-installation steps

Find the recommended Docker Engine post-installation steps for Linux users, including how to run Docker as a non-root user and more.

docs.docker.com

 

위의 경로를 참고하여 Docker를 설치한다.

 

sudo groupadd docker
sudo usermod -aG docker $USER
newgrp docker
docker run hello-world

 

hello-world가 잘 출력되는 것을 확인하면 된다.

 

실습 1.  앱 컨테이너화 - DB 서버 구성

Postgres 데이터베이스 컨테이너 띄우기

 

현재 python runserver로 실행된 코드는 기본적으로 sqlite를 데이터베이스를 사용하고 있다.

이번 실습에서는 데이터베이스를 sqlite에서 postgres로 변경하고, 컨테이너 환경에서 postgres를 구동하는 것을 목표로 한다.

 

docker volume create poll-db-volume
docker volume ls
docker volume inspect poll-db-volume

 

위에서 설명했듯이 컨테이너 스토리지는 데이터를 휘발성으로 저장하기 때문에 스토리지를 볼륨으로 따로 구성해 컨테이너에 연결하는 것이 좋다. 데이터베이스의 역할을 한 volume을 poll-db-volume이라는 이름으로 생성한 뒤에 정보를 살펴보았다.

 

docker run -p 5432:5432 --rm --name poll_db \
-v poll-db-volume:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=1234qwer \
-e POSTGRES_USER=fast \
-e POSTGRES_DB=poll \
-d postgres:12

 

Postgresql 데이터베이스 컨테이너를 실행한다.

위의 명령어는 poll_db라는 이름으로 컨테이너를 띄운 뒤 이전 명령어로 생성한 볼륨을 postgres 데이터 폴더에 연결한다. 또한 postgres의  user, password, db를 설정한다.

여기서 유의할 점은 불러올 이미지의 버전을 지정해야 한다는 점이다. 일반적으로 태그가 작성되어 있지 않다면 latest 버전을 불러오는데, 현대 Django 버전과 호환되는 postgres 이미지 버전은 12이므로 태그를 통해 적절한 이미지 버전을 불러와야 문제 없이 구동 가능하다.

 

sudo apt install -y postgresql-client
psql -h 127.0.0.1 -U fast -d  # pw = 1234qwer

 

컨테이너가 실행되었다면 psql client Database에 접속해본다.

데이터베이스가 실행만 되었을 뿐 아무런 데이터가 저장되지 않은 것을 확인할 수 있다.

 

# projects/Django-Poll-App/pollme/settings.py
"""
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'poll',
        'USER': 'fast',
        'PASSWORD': '1234qwer',
        'HOST': '127.0.0.1',
        'PORT': '5432',
    }
}
"""

 

Django와 DB를 연결하기 위해 settings.py의 DATABASES의 코드를 다음과 같이 변경한다.

 

# projects/Django-Poll-App/requirements.txt 내용 추가
psycopg2-binary==2.9.3

# OS 종속성 필요한 패키지 설치
sudo apt install -y libpq-dev python3-dev

# postgresql 클라이언트 패키지 설치 
pip3 install -r requirements.txt

# 데이터베이스 스키마 생성
python3 manage.py migrate

# 어드민 메뉴 사용에 필요한 관리자 계정 생성
python3 manage.py createsuperuser

# Dummy data 생성
pip3 install faker
python3 manage.py shell
import seeder
seeder.seed_all(30)

 

Django와 DB가 연결되었지만, 여전히 DB에는 아무런 데이터를 없다.

이전에 python 코드에서는 python3 manage.py migrate 으로 데이터베이스 스키마를 생성했고, seeder를 이용해 더미 데이터를 생성했었다.

이 방식을 동일하게 postgres에서 실행하기 위해 libdq-dev, python3-dev와 psycopg2-binary를 설치한다.

 

 

더미데이터 생성 후 접속해보면 데이터가 생겨있는 것을 확인할 수 있다.

 

실습 2. 앱 컨테이너화– App 서버 구성

python 코드를 컨테이너로 변경

 

데이터베이스처럼 python app server도 Docker를 이용해 컨테이너화 한다.

 

ROM python:3.10-slim-bullseye

ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE 1

RUN apt-get update \
  && apt-get install -y gcc libpq-dev python3-dev \
  && rm -rf /var/lib/apt/lists/*

WORKDIR /usr/src/app
COPY requirements.txt ./
RUN pip install -r requirements.txt
COPY . .

EXPOSE 8000
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

 

프로젝트 루트 경로의 다음과 같은 내용의 Dockerfile을 생성한다. Dockerfile의 내용은 다음과 같다.

  • python 3.8 버전의 베이스 이미지를 기반으로 기본 environment 세팅
  • 필요한 패키지 설치
  • 워크 디렉토리 생성 후 어플리케이션이 위치할 도커 내의 컨테이너 위치 지정
  • requirements.txt를 복사해서 설치
  • 현재 모든 코드들을 WORKDIR로 copy
  • 외부에서 접속을 위해 8000 포트로 expose runsover 0.0.0.0(모든 위치):8000 접근 허용
 

 

이미지를 이대로 빌드하면 위와 같은 오류가 발생한다. 

앱 서버와 DB는 별도의 컨테이너에서 실행되기 때문에 앱 서버의 127.0.0.1과 Database의 127.0.0.1은 다른 위치(각자의 localhost)를 가리킨다. 따라서 두 컨테이너를 연결하기 위해서는 네트워크 연결을 해야 한다.

 

ip address show eth0

 

로컬 호스트의 IP를 확인한다. 사실 이렇게 명령어를 입력해 확인하지 않아도 ubuntu@ip-172-31-46-65로 나와 있다.

 

 

projects/Django-Poll-App/pollme/settings.py의 Database 호스트 정보를 확인한 IP로 변경한다.

 

# Docker 이미지 빌드
docker build --tag poll_app .

# 빌드된 이미지 실행 
docker run -p 8000:8000 --rm --name poll poll_app

 

이미지 빌드 후 호스트에서 접근이 가능하도록 포트 -p 8000:8000 옵션을 주고 실행하면 localhost:8000로 접근이 가능하다.

 

실습 3. 앱 컨테이너화– Web 서버 구성

Nginx, Gunicorn 도입

 

개발용 local 서버에서는 웹 서버와 애플리케이션 서버를 하나로 구동해도 문제가 없지만, 운영 환경에서는 두 서버를 구분해야 한다.

이번 실습에서는 Nginx로 웹 서버를 구성하고, GUnicon을 이용해 운영 앱 서버를 구성한다.

 

# requiremets.txt에 아래 줄 추가
gunicorn==20.1.0

# Dockerfile의 마지막 줄 변경
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "pollme.wsgi:application"]

# poll 이미지 새로 생성
docker build --tag poll_app .

# 빌드된 이미지 실행 
docker run -d -p 8000:8000 --rm --name poll poll_app

 

변경 사항은 다음과 같다.

 

 

이전에는 python manage.py runserver로 서버를 실행했다.

runserver는 개발용 서버이기 때문에 DEBUG=True 상태에서는 Django가 static 파일을 직접 서빙해 준다.

그래서 /static/css/home_style.css 파일이 collectstatic 하지 않아도 보인다.

 

Gunicorn은 생산용 WSGI 서버이고, Django는 개발용 runserver와 달리 static 파일을 서빙하지 않는다.

따라서 Not Found: /static/css/home_style.css 문제가 발생하고, css가 보이지 않는다.

해당 문제는 이후 nginx 통해 해결한다.

 

detach 모드(-d)로 백그라운드로 실행을 시킨 뒤 Nginx 설정으로 넘어간다.

 

# nginx 작업을 위한 경로 추가
mkdir -p nginx/config && cd nginx

# nginx 설정 파일 작성
vim config/nginx.conf

# config/nginx.conf 파일 내용
"""
events {
  worker_connections  4096;
}
http {
    charset utf-8;
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    server {
        listen      80;
        server_name localhost;
        charset     utf-8;

        # max upload size
        client_max_body_size 75M;   # adjust to taste

        location /static/ {
            alias /data/static/;
        }

        location / {
            proxy_pass              http://172.31.46.65:8000;  
            proxy_set_header        Host $host;
        }
    }
}
"""

 

nginx.conf 파일을 다음과 같이 작성한다. 해당 파일의 주요 내용은 다음과 같다.

  • localhost의 80 포트로 요청을 받음
  • static 요청이 들어오면 Django 프로젝트의 static 파일 위치로 서빙한다.
  • static 요청이 아닌 경우 host private ip로 서빙한다.

 

# nginx Dockerfile 작성
vim Dockerfile

# Dockerfile 내용
"""
FROM nginx
COPY nginx/config/nginx.conf /etc/nginx/nginx.conf
COPY static /data/static

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]
"""
nginx의 Dockerfile 내용을 다음과 같이 작성한다. 해당 파일의 주요 내용은 다음과 같다.
  • nginx 기본 이미지를 불러온다.
  • nginx.conf를 기본 config로 사용하도록 복사해온다.
  • static 경로의 파일을 복사해서 nginx에서 서빙한다.
  • nginx를 80번 포트로 서빙하므로(nginx.conf) 80번 포트를 노출시킨다.

루트 경로로 돌아와서 nginx 디렉토리 안에 있는 dockerfile을 빌드하고 루트 경로의 디렉토리 구조를 그대로 가져가기 위해 루트 경로로 돌아와서 dockre build를 실행한다.

 

 

80번 포트 추가한 뒤에 localhost:64107로 접속하면 css 화면이  보인다.

 

 

실습 4. 앱 컨테이너화 – docker-compose

docker-conpose 사용

 

이전에는 개념을 이해하기 위해서 각각 한 레이어씩 컨테이너화 하면서 진행했다.

한 컨테이너씩 Dockerfile로 구성하면 네트워크가 분리되는 형태로 구성이 된다. 그래서 설정을 하는데 어려움이 있고, 하나의 서비스지만 다른 형태의 라이프사이클을 가지고 관리가 된다.

 

docker compose 이용하면 하나의 yaml 파일로 서비스를 구성할 수 있고, 컨테이너 간 네트워크를 공유하고, 라이프사이클을 번에 관리할 있다.

 

docker-compose cheetsheet

  • docker-compose ps : docker-compose로 구동되고 있는 컨테이너들의 목록 확인
  • docker-compose up : docker-compose로 서비스 실행
  • docker-compose down : docker-compose로 실행된 서비스를 내리기

docker-compose는 파일 내에서 빌드가 가능하며, volume 속성을 사용할 수 있다.

 

https://docs.docker.com/compose/install/

 

Install

Learn how to install Docker Compose. Compose is available natively on Docker Desktop, as a Docker Engine plugin, and as a standalone tool.

docs.docker.com

 

# 실행 중인 모든 컨테이너 종료
docker stop poll poll_db

# docker compose 다운로드
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
sudo curl \
    -L https://raw.githubusercontent.com/docker/compose/1.29.2/contrib/completion/bash/docker-compose \
    -o /etc/bash_completion.d/docker-compose
source ~/.bashrc

 

실행 중인 모든 컨테이너를 종료하고, 공식 문서를 참고하여 버전에 맞는 docker-compose를 다운로드한다.

 

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.environ.get('POSTGRES_DB'),
        'USER': os.environ.get('POSTGRES_USER'),
        'PASSWORD': os.environ.get('POSTGRES_PASSWORD'),
        'HOST': os.environ.get('POSTGRES_HOST'),
        'PORT': '5432',
    }
}

 

네트워크 환경을 공유하므로 주요 환경 변수들을 설정 파일에 모아서 관리한다.

projects/Django-Poll-App/pollme/settings.py의 Database 호스트 정보를 하드 코딩 값에서 환경 변수로 변경한다.

 

docker build --tag poll_app . 로 poll_app 이미지를 새로 생성한다.

 

version: "3"
   
services:
  db:
    image: postgres
    volumes:
      - poll-db-volume:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=poll
      - POSTGRES_USER=fast
      - POSTGRES_PASSWORD=1234qwer

  app:
    image: poll_app
    environment:
      - POSTGRES_DB=poll
      - POSTGRES_USER=fast
      - POSTGRES_PASSWORD=1234qwer
      - POSTGRES_HOST=db
    depends_on:
      - db
    
  web:
    image: poll_web
    ports:
      - "80:80"
    depends_on:
      - app

volumes:
  poll-db-volume:
    external: true
    name:
      poll-db-volume

 

프로젝트 루트 경로에 docker-compose.yaml을 생성한다.

 

docker-compose를 통해 DB, APP, WEB으로 구성된 앱을 하나의 서비스 형태로 구성해서 관리한다. 이들은 같은 네트워크 대역에 있기 때문에 접근을 용이하게 있다.

 

DB 설정은 환경변수로 뺐고, poll_app 컨테이너는 db에 종속성이 있다. app은 DB 연결이 되지 않으면 기다려야 하기 때문에 depends_on을 이용해 db 구동이 끝나고 서버가 구동되도록 설정한다.

 

또한 호스트 주소를 직접 작성하지 않고 서비스에 등록된 이름을 그대로 호스트 주소로 사용해 라우팅이 가능해졌다.

포트의 경우 웹으로 요청을 받는 포트만 노출시킨다. 이는 기존의 DB, APP 포트도 노출된 것보다 안전하다.

 

# nginx.conf 수정
proxy_pass http://app:8000;

# nginx image 빌드 
docker build --tag poll_web -f nginx/Dockerfile .

# docker compose 시작 
docker-compose up

 

기존에는 nginx.conf의 static이 아닌 경로를 호스트 ip로 하드코딩 했었다.

하지만 docker-compose는 네트워크 환경을 공유하므로 하드코딩된 호스트 ip 대신 app으로 작성하면 된다.

 

브라우저로 앱이 정상 동작하는지 여부를 확인한다.

혹시 에러가 발생한다면 docker-compose down -v 으로 내렸다가 다시 docker-compose up --build 하면 정상 실행된다.

 

 

실습 5. 앱 컨테이너화 디버깅 환경 구성

디버깅 환경 구성

 

이번 실습에서는 로컬 환경에서도 remote 있는 서버를 브레이크 포인트를 걸고 디버깅을 진행하면서 개발할 있는 환경을 구성한다.

 

먼저, VSCode에서 Docker 확장 프로그램과 Python 확장 프로그램을 설치한다.

 

# 프로젝트 루트 경로에 디버그용 docker compose 작성
vim docker-compose.debug.yml 

"""
version: "3"
   
services:
  db:
    image: postgres
    volumes:
      - poll-db-volume:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=poll
      - POSTGRES_USER=fast
      - POSTGRES_PASSWORD=1234qwer

  app:
    #image: poll_app, 기존에는 이미지를 가져다가 씀
    build:
      context: .
      dockerfile: ./Dockerfile
    command: ["sh", "-c", "pip install debugpy -t /tmp && python /tmp/debugpy --wait-for-client --listen 0.0.0.0:5678 
manage.py runserver 0.0.0.0:8000 --nothreading --noreload"]

    ports:
      - 8000:8000
      - 5678:5678
    volumes:
      - ./:/app
    environment:
      - POSTGRES_DB=poll
      - POSTGRES_USER=fast
      - POSTGRES_PASSWORD=1234qwer
      - POSTGRES_HOST=db
    depends_on:
      - db
    
  web:
    image: poll_web
    ports:
      - "80:80"
    depends_on:
      - app

volumes:
  poll-db-volume:
    external: true
    name:
      poll-db-volume
"""

 

프로젝트 루트 경로에 디버그용 docker-compose를 작성한다.

docker-compose에서는 이미지를 빌드해서 사용한다. 이를 통해 빌드와 관련된 컨텍스트의 위치, 도커 파일의 위치를 지정 가능하다.

이미지를 빌드하고 커맨드를 실행할 디버그 파일을 install해서 디버그 파일 프로그램을 매핑해서 python manage.py 서버가 파이썬의 로컬(개발용) 서버를 띄우고, 5678 포트에서 listening한다.

 

# 디버그를 위한 Visual Studio 셋팅 추가
vim .vscode/launch.json

"""
{
  "version": "0.2.0",
  "configurations": [ 
    {
      "name": "Python: Remote Attach",
      "type": "python",
      "request": "attach",
      "port": 5678, 
      "host": "localhost", 
      "pathMappings": [
          {
              "localRoot": "/home/ubuntu/projects/Django-Poll-App", 
              "remoteRoot": "/usr/src/app"
          }
      ]
    }
  ]
}
"""

 

디버그를 위해 .vscode/launch.json 경로에 다음과 같은 내용을 작성한다.

configuraton 생성해서 5678 포트에서 디버깅을 진행한다는 의미이다.

로컬 환경에서는 프로젝트의 장고 앱이 프로젝트 루트 경로이고, remote 환경에서는 Dockerfile에서 지정한 WORKDIR 위치, 앱이 work directory 프로젝트 root 컨테이너 내에서는 /usr/src/app 위치하도록 작성한다.

 

디버깅 환경 구성 완료

 

docker-compose -f docker-compose.debug.yml up -d --build 로 docker-compose를 실행하면 VSCode에서 디버깅 정보를 확인할 수 있는 환경이 구성된 것을 확인할 수 있다.