상세 컨텐츠

본문 제목

Docker + Nginx + Flask + Gunicorn + Mysql로 웹 서비스 개발하기

Instructions

by Posting-Neuron 2023. 11. 30. 00:00

본문

#Docker #Nginx #Flask #Gunicorn #Mysql #Python

Docker위에 nginx, flask, mysql로 웹 페이지를 제작할 일이 있어서 기본적인 연동만 완료된 상태의 소스코드를 기록해둔다.

  • 모든 서비스를 일일히 Dockerfile로 관리하기가 어려우므로, docoker compose를 활용한다.
  • nginx와 flask를 연결하기 위해서 gnucorn을 이용한다.
    • flask는 production level의 서비스는 불가능하므로 반드시 웹 서버와 연결하여서 사용해야한다.
  • 최근 NoSQL과 같이 RDBMS가 아닌 것들을 많이 사용하고 있기는 하지만, 나는 여전히 데이터 관리에 있어서 정형화된 것이 편하고, NoSQL 은 오히려 제대로 데이터 처리를 하지 않으면 오히려 성능이 더 안좋아질 수 있다. 결국 MYSQL을 사용하기로 결정!
  • Flask와 DB를 production용과 development 서비스로 구분 나누어서 사용한다.
    • 기능 구현 시에 publish된 웹 페이지를 직접적으로 수정할 수 없으므로, development의 flask나 DB로 테스트를 하고, 검증이 완료되면 publish하기 위함이다.

디렉토리 구조

project_root
├─ .env
├─ docker-compose.yml
└─ services
   ├─ nginx
   │  ├─ Dockerfile
   │  └─ nginx.conf
   └─ web
      ├─ Dockerfile
      ├─ Dockerfile.prod
      ├─ manage.py
      ├─ requirements.txt
      └─ src
         ├─ app.py
         └─ config.py
  • .env : 포트, 패스워드 등 사용자 / 환경에 따라서 자주 바꿔야하는 것들을 환경 변수로 빼놓음
  • docker-compose.yml : 모든 서비스들을 일괄적으로 관리하기 위한 파일
  • services: nginx와 web (flask)의 dockerfile을 build하기위한 파일들이 있음
    • manage.py: nginx와 flask를 연결하기 위함
    • 웹 페이지를 위한 소스코드는 services/web/src에 위치하게 된다.

소스 코드

.env

# --- Common Configs ---
HOME_DIR=/home/app
APP_FOLDER=/home/app/web
FLASK_APP=src/app.py
FLASK_DEBUG=0
DB_ROOT_PASSWORD=123456
DB_NAME=DB_MYAPP
DB_TIMEZONE=Asia/Seoul

# --- Prod configs ---
PROD_FLASK_PORT=5000
PROD_DB_PORT=3306
PROD_PHPMYADMIN_PORT=8081
PROD_NGINX_PORT=80

# --- Developer configs ---
DEV_FLASK_PORT=8003
DEV_DB_PORT=3307
DEV_PHPMYADMIN_PORT=8082

docker-compose.yaml

version: '3.8'

services:
  # --------------
  # - Flask - Dev
  # --------------
  flask_dev:
    container_name: flask_dev
    build: ./services/web
    command: python manage.py run -h 0.0.0.0
    restart: on-failure
    volumes:
      - ./services/web/:/usr/src/app/
    ports:
      - ${DEV_FLASK_PORT}:5000
    env_file:
      - ./.env
    depends_on:
      - db_dev
    networks:
      - net
  
  # --------------
  # - Flask - Prod
  # --------------
  flask_prod:
    container_name: flask_prod
    build:
      context: ./services/web
      dockerfile: Dockerfile.prod
    command: gunicorn -b 0.0.0.0:5000 manage:app
    restart: on-failure
    volumes:
      - static_volume:/home/app/web/src/static
      - media_volume:/home/app/web/src/media
    expose:
      - ${PROD_FLASK_PORT}
    env_file:
      - ./.env
    depends_on:
      - db_prod
    networks:
      - net

  # --------------
  # - DB (Dev)
  # --------------
  db_dev:
    container_name: db_dev
    image: mysql:8.0.32-debian
    platform: linux/amd64
    restart: on-failure
    expose:
      - ${DEV_DB_PORT}
    volumes:
      - mysql_data-dev:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: ${DB_NAME}
      TZ: ${DB_TIMEZONE}
    networks:
      - net
  
  # --------------
  # - DB (Prod)
  # --------------
  db_prod:
    container_name: db_prod
    image: mysql:8.0.32-debian
    platform: linux/amd64
    restart: on-failure
    expose:
      - ${PROD_DB_PORT}
    volumes:
      - mysql_data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
      MYSQL_DATABASE: ${DB_NAME}
      TZ: ${DB_TIMEZONE}
    networks:
      - net

  # --------------
  # - Nginx
  # --------------
  nginx:
    container_name: nginx
    build: ./services/nginx
    restart: on-failure
    volumes:
      - static_volume:/home/app/web/project/static
      - media_volume:/home/app/web/project/media
    ports:
      - ${PROD_NGINX_PORT}:80
    depends_on:
      - flask_prod
    networks:
      - net

  # --------------
  # - PhpMyAdmin (Dev)
  # --------------
  phpmyadmin_dev:
    container_name: phpmyadmin_dev
    image: phpmyadmin/phpmyadmin
    restart: on-failure
    ports:
      - ${DEV_PHPMYADMIN_PORT}:80
    networks:
      - net
    depends_on:
      - db_dev
    environment:
      PMA_HOST: db
      PMA_PORT: ${DEV_DB_PORT}
      PMA_USER: root
      PMA_PASSWORD: ${DB_ROOT_PASSWORD}
      PMA_ARBITRARY: 1

  # --------------
  # - PhpMyAdmin (Prod)
  # --------------
  phpmyadmin_prod:
    container_name: phpmyadmin_prod
    image: phpmyadmin/phpmyadmin
    restart: on-failure
    ports:
      - ${PROD_PHPMYADMIN_PORT}:80
    networks:
      - net
    depends_on:
      - db_prod
    environment:
      PMA_HOST: db
      PMA_PORT: ${DEV_DB_PORT}
      PMA_USER: root
      PMA_PASSWORD: ${DB_ROOT_PASSWORD}
      PMA_ARBITRARY: 1

volumes:
  mysql_data:
  mysql_data-dev:
  static_volume:
  media_volume:
networks:
  net:

services/nginx/Dockerfile

FROM nginx:mainline

RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d

services/nginx/nginx.conf

upstream emoji_flask {
    # flask_prod is the name of the docker compose service.
    # flask_prod:5000 is the port the service is listening on.
    # This is the same as the port you set in the Dockerfile.
    server flask_prod:5000;
}

server {

    listen 80;

    location / {

        resolver 127.0.0.11 valid=30s;
        resolver_timeout 10s;

        set $upstream http://myapp.com;
        proxy_pass $upstream;

        proxy_redirect     off;
        proxy_set_header   Host $host;
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    location /static/ {
        alias /home/app/web/project/static/;
    }

    location /media/ {
        alias /home/app/web/project/media/;
    }

}

services/web/Dockerfile

# pull official base image
FROM python:3.10.7-slim-buster

# set work directory
WORKDIR /usr/src/app

# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# install system dependencies
RUN apt-get update && apt-get install -y netcat

# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt /usr/src/app/requirements.txt
RUN pip install -r requirements.txt

# copy project
COPY . /usr/src/app/

Dockerfile.prod

###########
# BUILDER #
###########

# pull official base image
FROM python:3.10.7-slim-buster as builder

# set work directory
WORKDIR /usr/src/app

# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# install system dependencies
RUN apt-get update && \
    apt-get install -y --no-install-recommends gcc

# lint
RUN pip install --upgrade pip
RUN pip install flake8==6.0.0
COPY . /usr/src/app/
# RUN flake8 --ignore=E501,F401 .

# install python dependencies
COPY ./requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /usr/src/app/wheels -r requirements.txt

#########
# FINAL #
#########

# pull official base image
FROM python:3.10.7-slim-buster

# create directory for the app user
RUN mkdir -p /home/app

# create the app user
RUN addgroup --system app && adduser --system --group app

# create the appropriate directories
ENV HOME=$HOME_DIR
ENV APP_HOME=$HOME_DIR/web
RUN mkdir $APP_HOME
WORKDIR $APP_HOME

# install dependencies
RUN apt-get update && apt-get install -y --no-install-recommends netcat
COPY --from=builder /usr/src/app/wheels /wheels
COPY --from=builder /usr/src/app/requirements.txt .
RUN pip install --upgrade pip
RUN pip install --no-cache /wheels/*

# copy project
RUN rm -rf $APP_HOME
COPY . $APP_HOME

# chown all the files to the app user
RUN chown -R app:app $APP_HOME

# change to the app user
USER app

services/web/manage.py

from flask.cli import FlaskGroup
from src.app import app

cli = FlaskGroup(app)

if __name__ == "__main__":
    cli()

services/web/requirements.txt

Flask==3.0.0
Flask-SQLAlchemy==3.1.1
gunicorn==21.2.0
mysql-connector-python==8.2.0

services/web/src/app.py

from flask import (
    Flask,
    abort,
    jsonify,
    redirect,
    render_template,
    request,
    send_from_directory,
    session,
)
from sqlalchemy import create_engine

app = Flask(__name__)
app.config.from_object("src.config.Config")
db = create_engine(app.config["SQLALCHEMY_DATABASE_URI"])

@app.route("/")
def index():
    return jsonify({"message": "Hello, World!"})

@app.route("/static/<path:filename>")
def staticfiles(filename):
    return send_from_directory(app.config["STATIC_FOLDER"], filename)

@app.route("/media/<path:filename>")
def mediafiles(filename):
    return send_from_directory(app.config["MEDIA_FOLDER"], filename)

services/web/src/config.py

import os

basedir = os.path.abspath(os.path.dirname(__file__))

class Config(object):
    SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URL", "sqlite://")
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    STATIC_FOLDER = f"{os.getenv('APP_FOLDER')}/src/static"
    MEDIA_FOLDER = f"{os.getenv('APP_FOLDER')}/src/media"

Docker Container 실행

docker-compose up -d --build
  • 소스 코드를 수정하면 dev 컨테이너의 flask app 은 알아서 refresh되고 테스트가 완료되면 위 명령어를 다시 실행해주면 production 컨테이너가 업데이트된다.

관련글 더보기