본문 바로가기

Backend/Docker, k8s

Docker + Frontend(Nginx, React) + Backend(Nginx, Gunicorn, Django, PostgreSQL)

* 목차

 - Frontend Container 제작

  1. React Project 시작

  2. React build

  2. Dockerfile 제작

  3. Docker build

 - Backend Container 제작

  1. Django Project 시작

  2. Postgresql, Django Database Setting

  3. Django Model, API 작성

  4. PostgreSQL 설정

  5. Dockerfile 제작

  6. Docker build

  7. static file 추가

- Frontend, Backend 연결

  1. API 작성

  2. React api 요청 작성

  3. Backend Container, Frontend Container 연결

 


Intro

이번 글에서 구축할 시스템은 아래 그림과 같습니다.

 

이 시스템은 3가지 영역으로 크게 나뉘어 있습니다.

 

첫 번째로 Code Area입니다. Dev Area는 개발자가 기능을 구현하기 위해 코드를 실제로 작성하는 공간입니다. 이 공간에서 Frontend와 Backend 코드가 제작됩니다. 개발자는 방화벽과 SSH Key를 통해 서버에 접근을 할 수 있으며 서버 안에서 Code 작업을 하게 됩니다. 실제로는 git을 통해 다양한 개발자들이 git push, pull request를 하여 Develop branch의 code가 모이는 구조가 이상적이지만 이번 예제에서는 간단하게 코드만 작성하는 공간으로 진행합니다.

 

두 번째는 Dev(Development) Area입니다. Test Area는 작성한 Code가 실제로 잘 동작하는지 Test를 할 수 있는 공간입니다. 개발자는 Test Area의 방화벽 Open 된 Port들을 통해 접근할 수 있으며, DB 설정 확인, Frontend 기능 확인, Backend 기능 확인, 상호 통신 확인을 진행하게 됩니다. 여기서 Integration test가 끝나야 Code가 Production Area에 반영이 됩니다. 이 공간이 제대로 갖춰지려면 운영공간과 같은 환경의 Container이어야 이상적이지만 이번 예제에서는 단순히 임시 서버를 구동하는 수준으로 진행합니다.

 

마지막으로는 Production Area입니다. Production Area에서는 Code가 환경에 영향을 받지 않도록 Docker container가 사용됩니다. 이번 예제에서는 이 공간을 Docker로 구성할 때 어떻게 구성해야 할지 집중적으로 진행해 보도록 하겠습니다.

 

 

* 필자는 이 구조를 하나의 EC2(t3.medium)에 구현했지만 보안과 성능, 관리 측면에서는 VPC를 별도로 구성하고 Production Area와 Dev Area는 완전히 분리되는것이 좋습니다. 그리고 DB는 RDS에 구현, Media file은 Storage는 S3로 나누어 DB의 안정성을 전문적으로 관리해야 할 것입니다. 하지만 이번 예제의 취지는 그런 infra적인 이해도를 높이기 위한 것이 아니라 Docker, Nginx, django, React 설정를 하는 것을 목표로 하고 있습니다. 이 구조는 단순히 예제의 이해를 돕기 위한 구조일 뿐, 정답으로는 생각하실 필요는 없습니다.

 

 

* 환경은 AWS EC2에서 진행합니다. 익숙하지 않으신 분들은 아래를 참조해 주세요

 

AWS - EC2 초 간단 생성 + vscode 원격연결 (2023년 version)

독립된 서버가 급히 필요한데 주변에 아무것도 없다면? Linux OS가 필요한 상황인데 집에 Windows만 있는 상황이라면? Docker를 windows 환경에서 또 새로 구축하긴 귀찮죠... 그럴 때는 EC2를 사용해 봅시

tyoon9781.tistory.com

 

* docker가 익숙하지 않으신 분들은 docker tutorial부터 시작해 보세요

 

Docker - Tutorial

* 목차 - Docker Engine 설치 - Docker Image 제작 (flask) - Docker Image 실행 - Docker Image 배포 * 환경은 EC2의 ubuntu으로 진행합니다. 아래 링크를 참조해 주세요 [간단 정리] EC2 초 간단 생성 + vscode 원격연결 (202

tyoon9781.tistory.com

 

* yarn을 활용해 react를 구축합니다. 만약 yarn이 설치되어 있지 않으신 분들은 아래 글을 참고해 주세요

 

React - EC2에서 React 환경 구축하기 (2023년 version)

* 목차 1. node.js, npm 설치 2. yarn, vite 설치 3. ec2 network 설정 예전에는 React 시작하려면 npm create-react-app이 대부분이었는데 webpack의 속도 issue와 vite와 같이 훌륭한 Build 도구가 있어서 요즘은 또 트렌

tyoon9781.tistory.com

 

* nginx가 익숙하지 않으신 분들은 nginx tutorial부터 시작해 보세요

 

Nginx - Tutorial

* 목차 - Intro - Nginx 설치 - Nginx 실행 - Nginx 구조 살펴보기 - Nginx로 나만의 Service 적용하기 Intro 이번 글은 Nginx tutorial입니다. Nginx의 간단한 설정방법과 나만의 service를 만들어 보도록 합시다. * 환

tyoon9781.tistory.com

 

 

* 본격적으로 시작하기 전에 project folder를 생성해 줍시다.

mkdir ~/fullstack_practice
cd ~/fullstack_practice
mkdir backend
mkdir frontend

 

글이 매우 길지만 쉽게 따라올 수 있도록 작성하였습니다. 급하지 않게 천천히 따라와 주세요.

 

그럼, 시작하겠습니다.

 


1. Frontend Container 제작

1.1. React Project 시작

먼저 frontend folder에 접근합니다.

cd frontend

 

frontend folder에 yarn을 활용하여 React project를 생성합니다. 

yarn create vite

 

그러면 다음과 같이 진행이 되었을 겁니다.

Project name, 다양한 옵션을 선택할 수 있다.

 

다양한 file들이 생성되었다.

 

 

바로 frontend dev server를 실행해 보도록 합시다. yarn 명령어로 package를 설치하고 dev server를 실행해 보도록 합시다. host option을 통해 외부에서도 접근할 수 있도록 했습니다.

cd vite-project
yarn
yarn dev --host 0.0.0.0

 

기본 package 설치가 진행된다.

 

기본 port는 5173을 사용한다.

 

 

만약 기본 port인 5173을 방화벽 해제 하지 않았다면 web server를 확인할 수 없습니다. EC2에서 port를 open 해줍시다.

5173 포트가 열려있어야 EC2에 접근이 가능하다.

 

그러면 brower로 접근해 봅시다. Public IP로도 접근이 가능하지만 localhost로도 접근이 가능합니다.

React의 기본 Port는 5173이다.

 

현재 진행된 상황은 여기입니다.

시작이 반입니다...

Server dev가 동작하는 것을 확인했으면 Ctrl + C로 종료하도록 합시다.

 

1.2. React build

react 프로젝트를 배포하기 위해서 index.html로 만들어야 합니다. yarn build 명령어를 입력합니다.

yarn build

 

그러면 dist라는 폴더가 생기고 그 안에 index.html file과 기타 file들이 생성됩니다.

yarn build를 하니 index.html이 생겼다.

 

dist폴더에 file들이 생겼다

 

축하합니다! react project를 배포할 파일까지 완성했습니다! 

 

1.3. Dockerfile 제작

Frontend Container 설계도인 Dockerfile은 어떻게 작성해야 할까요?

 

1. nginx 설치

2. build 된 html을 복사

3. build 된 file을 nginx가 serving 할 수 있도록 설정

4. container에 접근할 Port 설정 (5000)

5. nginx 실행

 

이런 순서로 환경 구성을 하면 frontend container가 동작을 할 것입니다. 이 동작에 맞춰서 Dockerfile을 작성해 보도록 합시다.

# base image는 node image로 시작한다. npm과 yarn이 모두 설치되어 있다.
FROM nginx:1.25.1-alpine3.17-slim

# nginx의 기본 service를 제거한다.
RUN rm -rf /etc/nginx/sites-enabled/default

# nginx에 serving할 html의 설정파일을 복사한다.
COPY nginx.conf /etc/nginx/conf.d

# 작업영역을 선택한다. mkdir, cd를 동시에 진행한다 생각하면 된다.
WORKDIR /code

# 배포할 파일을 복사한다.
COPY dist/ dist/

# frontend Port를 설정한다.
EXPOSE 5000

# container가 종료될 때 정상종료를 유도한다.
STOPSIGNAL SIGTERM

# nginx를 global 설정
# Docker에서는 nginx가 daemon으로 실행되지 않도록 한다.
# daemon으로 실행하지 않으면 container가 바로 종료된다.
CMD ["nginx", "-g", "daemon off;"]

 

*원래는 dist/ 폴더를 복사해서 dockerfile로 만드는 것이 아니라 yarn build 후 S3 같은 공간에 upload 한 후 

 

COPY에서 불필요한 폴더를 복사하지 않도록 .dockerignore 파일을 작성합니다. 실제로는 동작하지 않더라도 차후의 실수를 방지할 수 있습니다.

# Dependency directories
node_modules/

 

그리고 /etc/nginx/conf.d에 복사할 server 설정 파일인 nginx.conf도 간단하게 만들었습니다.

server {
    listen 5000;
    root /code/dist/;    

    location /{
        try_files $uri $uri/ =404;
    }
}

 

1.4. Docker build

이제 준비가 끝났으니 바로 docker image build를 해 보도록 하겠습니다.

tag는 {발행자}/{container이름}:{version}을 따랐습니다.

sudo docker build --tag toy/frontend:0.1 .

 

docker build가 제대로 되었는지 image 목록을 확인해 보겠습니다.

sudo docker image list

 

image가 제대로 생성되었음을 확인할 수 있다.

 

Image가 생성된 것을 확인했으면 이 image를 실행(container화)시켜 보겠습니다. container를 background로 돌릴 수 있도록 -d option (detach)를 설정하고 port 5000으로 접근할 수 있도록 설정도 추가해 줍시다.

sudo docker run -d -p 5000:5000 toy/frontend:0.1

 

container id를 출력하고 daemon으로 실행된다.

 

Container 목록을 확인해 실행 중인지 확인해 보겠습니다. STATUS가 Exit이 아니어야 합니다.

sudo docker ps -a

 

docker container의 status를 확인해보자 exit이라면 nginx daemon 설정이 제대로 안되었을 것이다.

 

그리고 5000 port를 들어가 보면 Container의 nginx가 serving중인 html을 확인해 볼 수 있습니다.

frontend container 제작이 완료 됨.

 

축하합니다! Frontend Container 생성을 완료했습니다! 이제는 backend를 만들어 볼까요?

 


2. Backend Container 제작

2.1. Django Project 시작

이번에는 Django를 설치해 보도록 하겠습니다. Backend영역을 다뤄야 하니 폴더경로도 바꿔주세요

cd ~/fullstack_practice/backend
python3 -m venv .venv
. .venv/bin/activate
pip install django

 

그리고 django project도 만들어 줍시다. project 이름은 config로 설정, project 폴더 위치는 현재 폴더입니다.

django-admin startproject config .

 

django에서 Web api를 구축하기 위한 도구로 rest framework를 설치해 줍시다.

pip install djangorestframework

 

app의 이름은 api로 하도록 하겠습니다.

django-admin startapp api

 

 

여기까지 진행하셨으면 다음과 같은 폴더구조가 생겼을 것입니다.

 

이제 django가 제대로 설치되었는지 서버를 실행해 보겠습니다.

python manage.py runserver

 

접근 Port는 8000입니다. Frontend때와 마찬가지로 Port 방화벽을 해제해 주도록 합시다.

8000 포트가 열려있어야 EC2에 접근이 가능하다.

 

그럼 Browser에서 다음과 같은 화면을 확인하실 수 있습니다.

django의 기본 port는 8000이다.

 

제대로 동작합니다. server를 끄려면 ctrl + C를 눌러서 종료하도록 합시다.

 

현재 진행된 상황은 다음과 같습니다.

Backend도 슬슬 준비가 되어가는군요.

 

2.2. PostgreSQL install, Django Database Setting

이번 Django에서 DB는 Postgresql을 사용할 것입니다. 먼저 PostgreSQL을 설치하도록 합시다. contrib는 extension과 같이 편의성을 위한 확장기능들이 설치되어 있다 생각하시면 됩니다.

sudo apt update
sudo apt install postgresql postgresql-contrib

 

설치가 완료되면 postgresql service가 동작중인지 확인합니다. 보통은 설치 직후 active상태입니다.

sudo service postgresql status

 

service가 active상태다.

 

이제 postgresql을 사용할 계정 하나를 만들겠습니다. ubuntu server에는 postgres라는 사용자가 만들어졌습니다. 그 계정으로 postgresql을 실행해 보겠습니다.

sudo -i -u postgres psql

 

postgresql에 접속한 모습

 

이제 postgresql에 Django가 사용할 Database를 만들고 그 DB에 접근할 user를 생성하면 됩니다. 

 

먼저 database를 만듭니다. 저는 toydb라는 이름으로 만들었습니다.

postgres=# create database toydb;

 

그리고 user를 생성합니다. 비밀번호는 따옴표 사이에 text로 입력하면 됩니다.

postgres=# create user toyuser with encrypted password '{비밀번호}';

 

그리고 만든 DB에 User를 등록해서 해당 User는 DB를 사용할 수 있도록 합니다.

postgres=# grant all privileges on database toydb to toyuser;

 

이렇게 하면 postgresql setting이 끝났습니다. exit을 입력하여 종료합니다.

postgres=# exit

 

그리고 위의 내용을 django의 config/settings.py의 DATABASES에 작성합니다.

기존에 있는 sqlite3는 주석처리하거나 지우면 됩니다.

DATABASES = {
    'default': {
        # 'ENGINE': 'django.db.backends.sqlite3',
        # 'NAME': BASE_DIR / 'db.sqlite3',
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'toydb',
        'USER': 'toyuser',
        'PASSWORD': '{password}',
        'HOST': 'localhost',
        'PORT': '',
    }
}

 

Django에서 postgresql backend engine을 사용하기 위해서 필요한 library를 설치합니다.

pip install psycopg2-binary

 

2.3. Django Model, API 작성

django Rest framework를 몰라도 아래의 코드를 복붙 하면 이상 없이 동작합니다. 하지만 rest framework의 이해를 좀 더 높이고 싶으시다면 아래의 링크를 참조해 주세요.

 

* Rest framework를 더 알고 싶다면?

 

[간단 정리] Django REST framework tutorial1 - Serialization

* 목차 - intro - Set Environment - Serialization을 위한 Model 작성 - Serializer class 만들기 - Serializer 사용하기 - ModelSerializers 사용하기 - Serializer를 Django View에서 활용하기 - Web API test Intro 이번 시간에는 django

tyoon9781.tistory.com

 

먼저 config/settings.pyINSTALLED_APPS에 api app과 rest_framework를 추가합니다.

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'api.apps.ApiConfig',
]

 

api/models.py는 다음과 같이 작성합니다.

from django.db import models

class Item(models.Model):
    name = models.CharField(max_length=256)
    created = models.DateTimeField(auto_now_add=True)

 

api/serializers.py 파일을 만들고 다음과 같이 작성합니다.

from rest_framework import serializers
from .models import *

class ItemSerializer(serializers.ModelSerializer):
    class Meta:
        model = Item
        fields = '__all__'

 

api/views.py는 generic view를 활용해 간단히 작성합니다.

from rest_framework import generics
from .models import *
from .serializers import *


class Items(generics.ListCreateAPIView):
    queryset = Item.objects.all()
    serializer_class = ItemSerializer

class ItemDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Item.objects.all()
    serializer_class = ItemSerializer

 

api/urls.py파일을 만들고 format_suffix_patterns를 활용해 다음과 같이 작성합니다.

from django.urls import path
from rest_framework.urlpatterns import format_suffix_patterns
from . import views


urlpatterns = [
    path('items/', views.Items.as_view()),
    path('items/<int:pk>/', views.ItemDetail.as_view())
]

urlpatterns = format_suffix_patterns(urlpatterns)

 

config/urls.py에는 api app을 포함할 수 있도록 작성합니다.

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('api.urls')),
]

 

model이 실제로 db에 적용될 수 있도록 migration 합니다.

python manage.py makemigrations
python manage.py migrate

 

그러면 django가 postgresql toydb Database에 table을 생성합니다.

기존 django의 기본 app들도 적용되는 것을 확인할 수 있다.

 

자 이제 runserver를 다시 해보도록 합시다.

python manage.py runserver

 

그러면 아까와 같은 로켓 화면은 사라지고 Page not found라 뜹니다.

 

당황하지 않고 127.0.0.1:8000/api/items을 browser에 입력하여 GET 요청을 합니다.

Return으로 HTTP 200 OK와 빈 List를 받았다.

 

아직은 db에 데이터가 비어있습니다. test용으로 data를 POST 하도록 합시다.

POST 버튼을 클릭하면 DB에 저장이 된다.

 

 

만약에 이런 화면이 아닌 담백한 text를 보고 싶다면 .json으로 확장자를 바꾸거나 url 끝에? format=json을 붙여주면 됩니다. 이는 rest framework에서 지원하는 기능입니다.

api/items/?format=json

 

api/items.json

 

축하합니다! DB를 설치하고 Django와 연동하여 api를 구현하였습니다! 이제 이 backend를 container로 만들어 봅시다.

DB가 생겼습니다

 

2.4. PostgreSQL 설정

backend container를 만들기 전에 먼저 container가 접속할 Database부터 만들어 주도록 합시다. container는 개발 DB인 toydb가 아닌 다른 운영 DB를 접속할 수 있어야 합니다. postgresql에 접속해서 운영 DB를 만들어 주도록 합시다.

 

postgresql을 실행합니다.

sudo -i -u postgres psql

 

그리고 container에서 운영할 toydb_prd를 만듭니다.

postgres=# CREATE database toydb_prd;
postgres=# grant all privileges on database toydb_prd to toyuser;
postgres=# exit

 

Postgresql은 localhost에서 접속하는 것 외에는 기본적으로 막혀있습니다. 그런데 docker와 docker container는 localhost가 아닌 docker network에서 관리되는 ip를 할당받습니다. 따라서 Postgresql은 localhost가 아닌 docker container의 요청을 받을 수가 없습니다. PostgreSQL이 docker container와 통신하려면 docker container 대역을 접속 허용설정하면 됩니다. 

 

먼저 /etc/postgresql/{version}/main/postgresql.conf에서 listen_addresses를 수정하도록 합니다.

listen_addresses = 'localhost, 172.17.0.1'

 

listen_addresses에 docker ip를 추가하였다.

 

그리고 /etc/postgresql/{version}/main/pg_hba.conf에서 접근 허용 ip를 추가합니다. 172.17.0.1 ~ 254까지 내부 container의 요청은 받을 수 있도록 합시다.

host all all 172.17.0.0/24 scram-sha-256

 

접근 가능한 ip를 할당했다.

 

postgresql 설정을 바꿨으면 이제 postgresql을 재시작합니다.

sudo service postgresql restart

 

 

2.5. Dockerfile 제작

Backend Container는 Nginx로 api 요청이 들어오면 gunicorn을 통해 django의 wsgi를 호출해서 api를 동작하게 설계할 것입니다. 이런 경우 Dockerfile을 어떻게 작성해야 할까요? Dockerfile을 작성하기 막막할 때, docker 없이 환경구성하는 것을 먼저 생각해 보면 길을 찾기 쉽습니다. 만약 Docker 없이 Backend를 구성한다고 하면 아래와 같습니다.

 

1. python 설치

2. python library 설치 (=requirements.txt 활용)

3. nginx 설치

4. django 코드 복사

5. django migration 진행

6. nginx, gunicorn service 실행

 

위와 같이 작성하면 Backend는 의도대로 동작할 것입니다. 이러한 흐름으로 Dockerfile을 작성해 봅시다.

 

dockerfile을 제작하기 전에 해야 할 몇 가지가 있습니다. 먼저 settings.py를 base.py, dev.py과 docker.py환경으로 분리하겠습니다. 이는 docker환경과 dev환경을 분리하기 위함입니다.

settings.py는 이제 없다.

 

base.py는 settings.py의 내용을 그대로 가져갑니다. settings.py의 이름을 바꾸거나 내용을 그대로 복사합니다.

그리고 BASE_DIR의 내용을 다음과 같이 변경합니다.

BASE_DIR = Path(__file__).resolve().parent.parent.parent

 

그리고 Databases의 내용을 다시 default로 변경합니다.

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}

 

dev.py는 다음과 같이 작성합니다. Public IP로도 접근 가능하도록 ALLOWED_HOSTS에 Public IP를 추가하였습니다.

from .base import *

ALLOWED_HOSTS = ["localhost", "127.0.0.1", "xxx.xxx.xxx.xxx(Public IP)"]

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'toydb',
        'USER': 'toyuser',
        'PASSWORD': '{password}',
        'HOST': 'localhost',
        'PORT': '5432',
    }
}

 

docker.py는 다음과 같이 작성합니다. DATABASES의 설정에서 HOST를 그냥 localhost로 진행하면 container안에서만 DB를 찾게 됩니다. host.docker.internal로 HOST를 설정하면 container에서가 아닌 HOST OS에서 DB를 찾게 됩니다. 

from .base import *

ALLOWED_HOSTS = ["localhost", "xxx.xxx.xxx.xxx(Public IP)"]

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'toydb_prd',
        'USER': 'toyuser',
        'PASSWORD': '{password}',
        'HOST': 'host.docker.internal',
        'PORT': '5432',
    }
}

 

config/wsgi.py의 환경설정 경로를 'config.settings.docker'로 설정합니다.

import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.docker')
application = get_wsgi_application()

 

manage.py의 os.environ.setdefault의 변수를 config.settings.dev로 설정합니다.

def main():
    """Run administrative tasks."""
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.dev')
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc
    execute_from_command_line(sys.argv)

 

manage_docker.py를 manage.py로부터 복사해서 만듭니다. 그리고 os.environ.setdefault의 변수를 config.settings.docker로 바꿉니다.

def main():
    """Run administrative tasks."""
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.docker')
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc
    execute_from_command_line(sys.argv)

 

gunicorn_config.py를 작성합니다. socket의 위치는 container의 /code 폴더로 지정했습니다.

bind = "unix:/code/gunicorn.sock"
workers = 2

 

nginx.conf을 작성합니다. gunicorn과 통신할 socket 위치를 맞춰줍니다.

server {
    listen 5500;

    location / {
        proxy_pass http://unix:/code/gunicorn.sock;
    }
}

 

python library에 gunicorn이 포함되도록 설치합니다.

pip install gunicorn

 

requirements.txt를 작성합니다. 이 requirements.txt에는 django와 restframework, gunicorn 등이 포함되어 있습니다.

pip freeze > requirements.txt

 

이제 모든 준비가 끝났습니다. Dockerfile을 작성합니다.

# base image는 python:3.9로 시작한다.
FROM python:3.9-slim

# python library 설치
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# nginx 설치
RUN apt update \
  && apt install -y nginx

# 기본 service 제거
RUN rm -rf /etc/nginx/sites-enabled/default

# gunicorn과 연결할 설정 복사
COPY nginx.conf /etc/nginx/conf.d

# .venv는 .dockerignore로 인해 복사가 안된다.
WORKDIR /code
COPY . .

# backend port 설정
EXPOSE 5500

# container가 종료될 때 정상종료를 유도한다.
STOPSIGNAL SIGTERM

# django migrate 진행, nginx 시작, gunicorn service 시작. gunicorn이 daemon off로 동작
CMD python manage_docker.py migrate \
  && service nginx start \
  && gunicorn --config gunicorn_config.py config.wsgi:application

 

frontend에서 환경구성인 node_modules가 넘어가지 않도록 조치했듯이 backend에서도 .dockerignore을 작성합니다.

### Django ###
*.log
*.pot
*.pyc
__pycache__/
local_settings.py
db.sqlite3
db.sqlite3-journal
media

# Environments
.venv/

 

2.6. Docker build

이제 docker build를 진행합니다.

sudo docker build --tag toy/backend:0.1 .

 

그럼 다음과 같은 진행을 확인할 수 있습니다.

backend container가 완성되었다.

 

생성된 image로 docker run을 해보겠습니다. host.docker.internal을 host os와 제대로 연결할 수 있도록 add-host option을 줍니다.

sudo docker run -d -p 5500:5500 --add-host host.docker.internal:host-gateway toy/backend:0.1

 

backend에 container 동작을 확인하기 위해 임시로 backend container port를 방화벽 예외처리합니다.

 

그리고 public IP로 접근하면 다음과 같은 화면을 볼 수 있습니다.

 

Container가 DB에 정상 접근하여 Data를 불러오는 것(prd database는 새로 만들었으니 빈 데이터가 맞습니다)은 확인했습니다. 하지만 static file을 serving 하지 않아 순수 html만 viewing 했을 때의 모습입니다.

 

2.7. static file 추가

사실 api로만 사용한다면 static file은 필요 없는데 그래도 궁금하신 분들이 있으실 것 같아 static file들도 docker에서 serving 할 수 있도록 설정해 보겠습니다. Django에서 Static file을 한 곳으로 모은 다음 nginx에 경로 설정을 추가하겠습니다.

 

config/settings/base.py에 STATIC_ROOT를 추가합니다.

STATIC_ROOT = 'static/'

 

그리고 python manage.py collectstatic으로 static file을 모아줍니다. STATIC_ROOT 폴더에 모이게 됩니다.

python manage.py collectstatic

 

160개의 file들이 static/에 추가된 것을 확인할 수 있다.

 

그다음에 nginx 설정에서 django의 STATIC_URL(기본 값은 static/ 입니다)로 들어온 요청은 static/ 으로 들어올 수 있게 내용을 추가합니다

server {
    listen 5500;

    location /static/{
        root /code;
    }

    location / {
        proxy_pass http://unix:/code/gunicorn.sock;
    }
}

 

이렇게 하고 다시 Dockerfile을 build 한 다음에 run 하면 Django에서 제공한 static도 연결되었음을 확인할 수 있습니다.

api로 사용할 때는 사실 static file은 필요 없다.

 

축하합니다! 이제 Backend docker container까지 완성이 되었습니다! 지금까지 진행된 상황은 아래와 같습니다. 

 


 

3. Frontend, Backend 연결

3.1.  API 작성

* 원래 개발 순서는 기능 요청에 따른 API 작성, 그리고 model 설계입니다. 하지만 이 글은 작업환경 구성을 중점으로 두고 있기 때문에 generic view를 활용한 간단한 api로 진행합니다.

 

Frontend Backend를 연결하기 전에 서로 소통할 수 있도록 API 목록부터 작성해야겠지요?

Endpoint Method 설명
/api/items GET 모든 item 불러오기
/api/items POST item 생성
/api/items/<int:pk> GET 특정 item 불러오기
/api/items/<int:pk> PUT 특정 item 수정
/api/items/<int:pk> DELETE 특정 item 삭제

 

만약 rest framework를 자주 사용해 보신 분들은 이 내용이 Generic view에서 만들어져 있다는 사실을 아실 겁니다. 

아래는 backend의 Generic view 코드입니다.

 

backend/api/urls.py

from django.urls import path
from rest_framework.urlpatterns import format_suffix_patterns
from . import views


urlpatterns = [
    path('items/', views.Items.as_view()),
    path('items/<int:pk>', views.ItemDetail.as_view())
]

urlpatterns = format_suffix_patterns(urlpatterns)

 

backend/api/views.py

from rest_framework import generics
from .models import *
from .serializers import *


class Items(generics.ListCreateAPIView):
    queryset = Item.objects.all()
    serializer_class = ItemSerializer

class ItemDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Item.objects.all()
    serializer_class = ItemSerializer

 

이 api를 React에서 사용해 보도록 합시다.

 

3.2. React api 요청 작성

먼저 React에서 api를 요청할 수 있도록 axios를 설치합니다.

yarn add axios

 

그러면 다음과 같이 설치가 진행됩니다.

 

App.jsx에 api/items를 호출하기 위해 아래와 같이 작성합니다. BACKEND_URL은 스스로의 상황에 맞춰 잘 작성합시다.

import { useState, useEffect } from "react";
import axios from "axios";

const BACKEND_URL = '{public ip}:8000'

const GetAllItems = () => {
  const [data, setData] = useState([]);

  const fetchData = async () => {
    try {
      const response = await axios.get(`${BACKEND_URL}/api/items/`);
      setData(response.data);
    } catch (error) {
      console.error("[Error] fetching data:", error);
    }
  };

  useEffect(() => {
    fetchData();
  }, []);
  return (
    <div>
      <button onClick={fetchData}>Get All Items</button>
      <ul>
        {data.map((item) => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
};

const App = () => {
  return <GetAllItems />;
};

export default App;

 

main.jsx에 css가 걸려있으므로 제거해 줍시다.

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
// import './index.css'

ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

 

자, 이제 작성이 끝났으니 backend, frontend server를 실행시켜 봅시다.

 

먼저 backend server를 실행합니다.

python manage.py runserver 0.0.0.0:8000

 

그리고 frontend server를 실행합니다.

yarn dev --host 0.0.0.0

 

그리고 frontend server에 접속하면 browser에 다음과 같은 화면이 확인됩니다. 

react Restrict mode라 요청이 2번 날아가 error도 2개씩 뜬다.

 

Chrome Broswer에서 F12를 누르고 Console를 누르면 CORS Error를 확인해 볼 수 있습니다. 이는 Backend에서 api 호출을 하는 요청자가 공격자의 요청인지, 아니면 설계대로의 요청인지 구분할 수 없기 때문입니다. Backend에서 안전한 Frontend요청자를 등록합시다.

 

먼저 django-cors-headers를 설치합니다.

pip install django-cors-headers

 

backend/config/settings/base.py에서 INSTALLED_APPS와 MIDDLEWARE에 corsheaders를 추가합니다.

INSTALLED_APPS = [
    ...
    'cordsheaders',
]

MIDDLEWARE = [
    ...
    'corsheaders.middleware.CorsMiddleware',
]

 

그리고 backend/config/settings/dev.py에 CORS_ORIGIN_WHITELIST를 추가합니다. FRONTEND_URL은 각자의 http를 포함한 public ip, port를 입력하시면 됩니다. (예 : http://123.45.67.89:5173)

CORS_ORIGIN_WHITELIST = [
    {FRONTEND_URL},
]

 

그리고 다시 frontend로 돌아가 api 요청을 해봅시다. 이제는 제대로 동작하는 것을 확인할 수 있습니다!

처음에 넣었던 testData다

 

이제 나머지 api도 frontend에 작성해 보겠습니다.

 

먼저 BACKEND_URL을 jsx가 아닌 외부에 작성하겠습니다.

.env.dev 라는 file을 frontend/vite-project/ 에 만들어주세요

.env.dev 위치 확인

 

그리고 .env.dev에 다음과 같이 작성합니다. 환경 변수명은 꼭 VITE_로 시작해야 합니다.

VITE_BACKEND_URL=http://{backend_ip}:8000

 

package.json을 열어 scripts 영역을 수정합니다. 이렇게 하면 이제 yarn dev 시 .env.dev를 참조합니다.

  "scripts": {
    "dev": "vite --mode dev",
	...
  },

 

.env.dev에 적힌 BACKEND_URL은 jsx file에서 다음과 같이 사용할 수 있습니다.

const BACKEND_URL = import.meta.env.VITE_BACKEND_URL;

 

src 폴더에 components 폴더를 만들고 ItemList.jsx 파일을 만들어줍니다.

components 폴더생성

 

ItemList.jsx는 다음과 같이 작성합니다.

import { useState, useEffect, useRef } from "react";
import axios from "axios";

const BACKEND_URL = import.meta.env.VITE_BACKEND_URL;

const Item = ({ item, onDelete, onUpdate }) => {
  const handleDelete = async () => {
    try {
      await onDelete(item.id);
    } catch (error) {
      console.error("[Error] deleting item:", error);
    }
  };

  const handleUpdate = async () => {
    try {
      await onUpdate(item.id);
    } catch (error) {
      console.error("[Error] updating item:", error);
    }
  };

  return (
    <li>
      <button onClick={handleDelete}>Delete</button>
      <button onClick={handleUpdate}>Update</button>
      {" [" + item.id + "] " + item.name}
    </li>
  );
};

const ItemList = () => {
  const [items, setItems] = useState([]);
  const updatedTextRef = useRef("");

  useEffect(() => {
    getItems();
  }, []);

  const getItems = async () => {
    try {
      const response = await axios.get(`${BACKEND_URL}/api/items/`);
      setItems(response.data);
    } catch (error) {
      console.error("[Error] getting items:", error);
    }
  };

  const deleteItem = async (itemID) => {
    try {
      await axios.delete(`${BACKEND_URL}/api/items/${itemID}/`);
      await getItems();
    } catch (error) {
      console.error("[Error] deleting item:", error);
    }
  };

  const updateItem = async (itemID) => {
    try {
      await axios.put(`${BACKEND_URL}/api/items/${itemID}/`, {
        name: updatedTextRef.current.value,
      });
      await getItems();
      updatedTextRef.current.value = "";
    } catch (error) {
      console.error("[Error] updating item:", error);
    }
  };

  const postItem = async () => {
    try {
      await axios.post(`${BACKEND_URL}/api/items/`, {
        name: updatedTextRef.current.value,
      });
      await getItems();
      updatedTextRef.current.value = "";
    } catch (error) {
      console.error("[Error] posting item:", error);
    }
  };

  return (
    <div>
      <ul>
        <input type="text" ref={updatedTextRef} />
        <button onClick={postItem}>Post</button>
        {items
          .sort((a, b) => a.id - b.id)
          .map((item) => (
            <Item
              key={item.id}
              item={item}
              onDelete={deleteItem}
              onUpdate={updateItem}
            />
          ))}
      </ul>
      <div></div>
    </div>
  );
};

export default ItemList;

 

App.jsx는 다음과 같이 작성합니다.

import ItemList from "./components/ItemList";

const App = () => {
  return <ItemList />;
};

export default App;

 

그러면 이제 GET, POST, DELETE, PUT Method를 활용한 API를 Frontend를 통해 사용할 수 있습니다. 실제로 test를 해보면 Delete, Update. Post button을 통해 api가 동작하며, api 동작 후에는 Get items api를 통해 현재 DB의 상황을 바로 볼 수 있습니다.

API 기능을 확인하기 위한 Page라 UI/UX 관점은 전혀 없습니다.

 

축하합니다! Dev Area의 Backend와 Frontend를 연결했습니다!

 

 

3.3. Backend Container, Frontend Container 연결

먼저 backend부터 설정하겠습니다. backend/config/settings/docker.py에 CORS_ORIGIN_WHITELIST를 추가합니다. 이렇게 설정하면 frontend로부터 오는 요청은 승낙하게 됩니다.

CORS_ORIGIN_WHITELIST = [
    "http://{public ip}:5000",	## frontend url
]

 

frontent의 .env 파일을 생성하고 public ip와 아래의 내용을 작성합니다. .env는 mode를 따로 설정하지 않으면 읽을 수 있는 기본 설정 파일입니다.

VITE_BACKEND_URL=http://{public ip}:5500

 

그리고 build합니다. 이렇게 되면 VITE_BACKEND_URL은 http://{public ip}:5500이 됩니다.

yarn build

 

이제 frontend도 build 하고 run 하겠습니다.

sudo docker build --tag toy/frontend:0.1 .

sudo docker run \
 --name frontend \
 -d -p 5000:5000 \
 toy/frontend:0.1

 

 

자 그럼 이제 {Public IP}:5000으로 접근해 볼까요?

front container에 의해 접근이 되었다.

 

여기서 아무 text를 입력하고 Post를 눌러봅시다.

DB에 data를 넣고 다시 불러온 모습이다.

 

이렇게 하면 이제 서로 통신이 가능해집니다.

 

축하합니다! 이제 처음에 설정했던 모든 그림이 완성되었습니다!

 

여기까지 오시느라 고생하셨습니다 :) 고생 많으셨습니다.

 


마치며...

Frontend, Backend를 한 번에 작성하면 이렇게 됩니다. 이번 실습은 꽤 길지만 그림을 제외하면 내용 자체는 그리 길지 않습니다. 실제로 코드를 작성한 부분은 얼마 되지 않습니다. React의 api 요청 부분과 Django의 api view 작성 부분이 대부분의 코딩이었을 뿐, 나머지는 setting 하는 데에 힘을 쏟았습니다.

 

욕심 같아서는 Backend api를 외부에서 쉽게 호출할 수 없도록 인증 API Key도 실습에 추가해야 하나 싶었지만 애초에 그런 목적의 실습이 아니라 Docker를 활용한 Backend, Frontend 실습이었기 때문에 pass 했습니다. 다음에는 OAuth에 대해 작성해 볼까 합니다.

 

여기까지 따라오신 분들은 많은 것을 얻어가길 바라면서 여기서 마치겠습니다. 감사합니다.

 


* reference

https://ko.vitejs.dev/guide/env-and-mode.html

https://github.com/adamchainz/django-cors-headers

https://docs.docker.com/network/network-tutorial-standalone/

https://wikidocs.net/book/837

https://wikidocs.net/6601#_9

https://www.digitalocean.com/community/tutorials/how-to-install-postgresql-on-ubuntu-20-04-quickstart

https://www.youtube.com/watch?v=cJveiktaOSQ&t=480s&ab_channel=DennisIvy 

https://vitejs.dev/guide/

https://www.django-rest-framework.org/tutorial/quickstart/

https://docs.nginx.com/nginx/admin-guide/installing-nginx/installing-nginx-docker/

'Backend > Docker, k8s' 카테고리의 다른 글

docker image에 vscode extension 설치 방법  (0) 2024.03.01
Windows에서 Docker 설치  (0) 2024.02.11
Docker Compose - tutorial  (0) 2023.06.18
Docker - Tutorial  (0) 2023.06.17
k8s - Service  (0) 2023.04.28