├── backend ├── backend │ ├── __init__.py │ ├── asgi.py │ ├── wsgi.py │ ├── urls.py │ └── settings.py ├── todos │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── tests.py │ ├── apps.py │ ├── admin.py │ ├── serializers.py │ ├── urls.py │ ├── views.py │ └── models.py ├── requirements.txt ├── README.md ├── Dockerfile ├── manage.py └── .gitignore ├── .gitignore ├── settings ├── prod │ ├── .env.db.template │ ├── .env.db │ ├── .env.prod.template │ └── .env.prod └── dev │ └── .env.dev ├── .gitmodules ├── nginx ├── Dockerfile └── nginx.conf ├── .vscode └── settings.json ├── docker-compose.yml ├── docker-compose.prod.yml ├── docker-compose.logging.yml ├── README.md └── README.ko.md /backend/backend/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/todos/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/todos/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | # .env.db 3 | # .env.prod 4 | 5 | .DS_Store 6 | .idea 7 | *.log -------------------------------------------------------------------------------- /settings/prod/.env.db.template: -------------------------------------------------------------------------------- 1 | POSTGRES_USER= 2 | POSTGRES_PASSWORD= 3 | POSTGRES_DB= 4 | -------------------------------------------------------------------------------- /backend/todos/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /settings/prod/.env.db: -------------------------------------------------------------------------------- 1 | POSTGRES_USER=eg_user 2 | POSTGRES_PASSWORD=eg_pw 3 | POSTGRES_DB=eg_db 4 | -------------------------------------------------------------------------------- /backend/todos/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TodosConfig(AppConfig): 5 | name = 'todos' 6 | -------------------------------------------------------------------------------- /backend/todos/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from todos.models import Todo 4 | 5 | admin.site.register(Todo) 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "frontend"] 2 | path = frontend 3 | url = https://github.com/litsynp/react-todo-example.git 4 | [submodule "logging-example"] 5 | path = logging-example 6 | url = https://github.com/0BVer/logging-example.git 7 | -------------------------------------------------------------------------------- /nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:latest 2 | 3 | # Delete nginx default configuration file in virtual directory 4 | # 가상 공간의 nginx 기본 설정파일 삭제 후 작성한 설정파일로 대체 5 | RUN rm /etc/nginx/conf.d/default.conf 6 | COPY nginx.conf /etc/nginx/conf.d 7 | -------------------------------------------------------------------------------- /backend/todos/serializers.py: -------------------------------------------------------------------------------- 1 | from todos.models import Todo 2 | from rest_framework import serializers 3 | 4 | 5 | class TodoSerializer(serializers.ModelSerializer): 6 | class Meta: 7 | model = Todo 8 | fields = '__all__' 9 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.4.0 2 | autopep8==1.5.7 3 | Django==3.2.4 4 | django-cors-headers==3.10.1 5 | djangorestframework==3.13.1 6 | gunicorn==20.1.0 7 | psycopg2==2.9.1 8 | pycodestyle==2.7.0 9 | pytz==2021.1 10 | sqlparse==0.4.1 11 | toml==0.10.2 -------------------------------------------------------------------------------- /backend/todos/urls.py: -------------------------------------------------------------------------------- 1 | from rest_framework.routers import DefaultRouter 2 | 3 | from todos.views import TodoViewSet 4 | 5 | app_name = 'todos' 6 | 7 | router = DefaultRouter() 8 | router.register(r'', TodoViewSet) 9 | 10 | urlpatterns = [] + router.urls 11 | -------------------------------------------------------------------------------- /settings/prod/.env.prod.template: -------------------------------------------------------------------------------- 1 | # === Django === 2 | DEBUG=0 3 | SECRET_KEY= 4 | DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1] 5 | SQL_ENGINE=django.db.backends.postgresql 6 | SQL_DATABASE= 7 | SQL_USER= 8 | SQL_PASSWORD= 9 | SQL_HOST=db 10 | SQL_PORT=5432 11 | -------------------------------------------------------------------------------- /settings/dev/.env.dev: -------------------------------------------------------------------------------- 1 | # === Django === 2 | DEBUG=1 3 | SECRET_KEY=foobar 4 | DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1] 5 | SQL_ENGINE=django.db.backends.postgresql 6 | SQL_DATABASE=eg_db 7 | SQL_USER=eg_user 8 | SQL_PASSWORD=eg_pw 9 | SQL_HOST=db 10 | SQL_PORT=5432 11 | -------------------------------------------------------------------------------- /settings/prod/.env.prod: -------------------------------------------------------------------------------- 1 | # === Django === 2 | DEBUG=0 3 | SECRET_KEY=foobar 4 | DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1] 5 | SQL_ENGINE=django.db.backends.postgresql 6 | SQL_DATABASE=eg_db 7 | SQL_USER=eg_user 8 | SQL_PASSWORD=eg_pw 9 | SQL_HOST=db 10 | SQL_PORT=5432 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "backend/venv/bin/python3", 3 | "python.autoComplete.extraPaths": ["./backend"], 4 | "python.analysis.extraPaths": ["./backend"], 5 | "python.languageServer": "Pylance", 6 | "python.venvPath": "${workspaceFolder}/backend/venv" 7 | } 8 | -------------------------------------------------------------------------------- /backend/todos/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets 2 | 3 | from todos.serializers import TodoSerializer 4 | from todos.models import Todo 5 | 6 | 7 | class TodoViewSet(viewsets.ModelViewSet): 8 | serializer_class = TodoSerializer 9 | queryset = Todo.objects.all() 10 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # backend - Django 2 | 3 | ## 가상환경 세팅 4 | 5 | - 가상환경 생성 6 | 7 | ```shell 8 | $ virtualenv venv 9 | ``` 10 | 11 | - 가상환경 활성화 12 | 13 | ```shell 14 | $ .\venv\Scripts\activate 15 | (venv) $ 16 | ``` 17 | 18 | - 가상환경에 패키지 설치 19 | 20 | ```shell 21 | (venv) $ pip install -r requirements.txt 22 | ``` 23 | -------------------------------------------------------------------------------- /backend/todos/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Todo(models.Model): 5 | text = models.CharField(max_length=50, null=False, blank=False) 6 | completed = models.BooleanField(null=False, default=False) 7 | created_on = models.DateTimeField(auto_now_add=True) 8 | 9 | class Meta: 10 | db_table = 'todo' 11 | -------------------------------------------------------------------------------- /backend/backend/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for backend project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /backend/backend/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for backend project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | # pull official base image 2 | FROM python:3.8.3-alpine 3 | 4 | ENV PYTHONDONTWRITEBYTECODE 1 5 | ENV PYTHONUNBUFFERED 1 6 | 7 | ARG DJANGO_ALLOWED_HOSTS 8 | ARG DJANGO_SECRET_KEY 9 | ARG DJANGO_CORS_ORIGIN_WHITELIST 10 | 11 | ENV DJANGO_ALLOWED_HOSTS $DJANGO_ALLOWED_HOSTS 12 | ENV DJANGO_SECRET_KEY $DJANGO_SECRET_KEY 13 | ENV DJANGO_CORS_ORIGIN_WHITELIST $DJANGO_CORS_ORIGIN_WHITELIST 14 | 15 | WORKDIR /backend 16 | COPY requirements.txt /backend/ 17 | 18 | RUN apk add postgresql-dev libressl-dev libffi-dev gcc musl-dev gcc python3-dev musl-dev zlib-dev jpeg-dev #--(5.2) 19 | 20 | RUN pip install --upgrade pip 21 | RUN pip install -r requirements.txt 22 | 23 | COPY . /backend/ 24 | -------------------------------------------------------------------------------- /backend/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /backend/todos/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.4 on 2022-01-05 22:03 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Todo', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('text', models.CharField(max_length=50)), 19 | ('completed', models.BooleanField(default=False)), 20 | ('created_on', models.DateTimeField(auto_now_add=True)), 21 | ], 22 | options={ 23 | 'db_table': 'todo', 24 | }, 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | # nginx에 백엔드, 프론트엔드 연결해줌 2 | # nginx에 백엔드 연결 3 | upstream api { 4 | server backend:8000; 5 | } 6 | 7 | server { 8 | listen 8080; 9 | server_name localhost; 10 | charset utf-8; 11 | 12 | # 요청받은 uri로 연결 eg) http://127.0.0.1/login 13 | location /api/ { 14 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 15 | proxy_set_header X-Forwarded-Proto $scheme; 16 | proxy_set_header Host $http_host; 17 | proxy_redirect off; 18 | proxy_pass http://api; 19 | } 20 | 21 | # static 파일 디렉토리 연결 22 | location /staticfiles { 23 | alias /backend/staticfiles; 24 | } 25 | 26 | # media 파일 디렉토리 연결 27 | location /mediafiles { 28 | alias /backend/mediafiles; 29 | } 30 | 31 | # ignore cache frontend 32 | location ~* (service-worker\.js)$ { 33 | add_header 'Cache-Control' 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0'; 34 | expires off; 35 | proxy_no_cache 1; 36 | } 37 | 38 | location / { 39 | root /var/www/frontend; 40 | try_files $uri $uri/ /index.html?q=$uri&$args; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /backend/backend/urls.py: -------------------------------------------------------------------------------- 1 | """backend URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.2/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.conf import settings 17 | from django.conf.urls.static import static 18 | from django.contrib import admin 19 | from django.urls import path, include 20 | 21 | urlpatterns = [ 22 | path('admin/', admin.site.urls), 23 | path('api-auth/', include('rest_framework.urls')), 24 | path('api/v1/todos/', include('todos.urls')), 25 | ] 26 | 27 | urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 28 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 29 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | db: 5 | image: postgres:12.0-alpine 6 | volumes: 7 | - postgres_data_dev:/var/lib/postgresql/data/ 8 | environment: 9 | POSTGRES_USER: "eg_user" 10 | POSTGRES_PASSWORD: "eg_pw" 11 | POSTGRES_DB: "eg_db" 12 | ports: 13 | - 5432:5432 14 | 15 | backend: 16 | build: 17 | context: ./backend 18 | args: 19 | DJANGO_ALLOWED_HOSTS: "*" 20 | DJANGO_SECRET_KEY: "*" 21 | DJANGO_CORS_ORIGIN_WHITELIST: "*" 22 | command: python manage.py runserver 0.0.0.0:8000 23 | ports: 24 | - 8000:8000 25 | volumes: 26 | - ./backend/:/backend/ 27 | expose: 28 | - 8000 29 | env_file: 30 | - ./settings/dev/.env.dev 31 | depends_on: 32 | - db 33 | 34 | frontend: 35 | build: 36 | context: ./frontend 37 | command: [ "yarn", "start" ] 38 | ports: 39 | - 80:3000 40 | volumes: 41 | - ./frontend/:/frontend 42 | - ./frontend/node_modules/:/frontend/node_modules 43 | environment: 44 | - CI=true 45 | - CHOKIDAR_USEPOLLING=true 46 | - REACT_APP_BACKEND_URL=http://localhost:8000 47 | tty: true 48 | 49 | volumes: 50 | postgres_data_dev: null 51 | -------------------------------------------------------------------------------- /docker-compose.prod.yml: -------------------------------------------------------------------------------- 1 | #배포용, for production 2 | version: "3" 3 | 4 | services: 5 | # db 컨테이너 6 | db: 7 | image: postgres:12.0-alpine 8 | volumes: 9 | - postgres_data:/var/lib/postgresql/data/ 10 | env_file: 11 | - ./settings/prod/.env.db 12 | ports: 13 | - 5432:5432 14 | 15 | # Django 컨테이너 16 | backend: 17 | build: 18 | context: ./backend 19 | args: 20 | DJANGO_ALLOWED_HOSTS: "*" 21 | DJANGO_SECRET_KEY: "*" 22 | DJANGO_CORS_ORIGIN_WHITELIST: "*" 23 | command: gunicorn backend.wsgi --bind 0.0.0.0:8000 24 | ports: 25 | - 8000:8000 26 | volumes: 27 | - static_volume:/backend/staticfiles 28 | - media_volume:/backend/mediafiles 29 | - ./backend/:/backend/ 30 | expose: 31 | - 8000 32 | env_file: 33 | - ./settings/prod/.env.prod 34 | depends_on: 35 | - db 36 | 37 | frontend: 38 | build: 39 | context: ./frontend 40 | args: 41 | API_URL: "*" 42 | environment: 43 | - REACT_APP_BACKEND_URL=http://localhost 44 | volumes: 45 | - ./frontend/:/frontend 46 | - build_folder:/frontend/build 47 | - ./frontend/node_modules/:/frontend/node_modules 48 | tty: true 49 | 50 | # nginx 컨테이너(서버) 51 | nginx: 52 | build: ./nginx 53 | ports: 54 | - 80:8080 55 | volumes: 56 | - static_volume:/backend/staticfiles 57 | - media_volume:/backend/mediafiles 58 | - build_folder:/var/www/frontend 59 | - ./nginx/log:/var/log/nginx 60 | 61 | depends_on: 62 | - backend 63 | - frontend 64 | 65 | # 컨테이너 내려도 데이터 유지되도록 함 66 | volumes: 67 | postgres_data: 68 | static_volume: 69 | media_volume: 70 | build_folder: 71 | -------------------------------------------------------------------------------- /docker-compose.logging.yml: -------------------------------------------------------------------------------- 1 | # docker-compose.prod 와 함께 실행 해야 합니다. # need to run with docker-compose.prod 2 | # docker compose -f docker-compose.prod.yml -f docker-compose.logging.yml up --build 3 | 4 | # changeme 로 설정된 임시 패스워드를 변경하여 사용하시길 바랍니다. # need to change temp password 'changeme' 5 | 6 | version: '3.7' 7 | 8 | services: 9 | elasticsearch: 10 | build: 11 | context: ./logging-example/elasticsearch 12 | args: 13 | ELASTIC_VERSION: 8.5.2 14 | volumes: 15 | - ./logging-example/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml:ro,Z 16 | - elasticsearch:/usr/share/elasticsearch/data:Z 17 | ports: 18 | - 9200:9200 19 | - 9300:9300 20 | environment: 21 | node.name: elasticsearch 22 | ES_JAVA_OPTS: -Xms512m -Xmx512m 23 | # Bootstrap password. 24 | # Used to initialize the keystore during the initial startup of 25 | # Elasticsearch. Ignored on subsequent runs. 26 | ELASTIC_PASSWORD: changeme 27 | # Use single node discovery in order to disable production mode and avoid bootstrap checks. 28 | # see: https://www.elastic.co/guide/en/elasticsearch/reference/current/bootstrap-checks.html 29 | discovery.type: single-node 30 | restart: unless-stopped 31 | 32 | logstash: 33 | build: 34 | context: ./logging-example/logstash 35 | args: 36 | ELASTIC_VERSION: 8.5.2 37 | volumes: 38 | - ./logging-example/logstash/config/logstash.yml:/usr/share/logstash/config/logstash.yml:ro,Z 39 | - ./logging-example/logstash/pipeline:/usr/share/logstash/pipeline:ro,Z 40 | ports: 41 | - 5044:5044 42 | - 50000:50000/tcp 43 | - 50000:50000/udp 44 | - 9600:9600 45 | environment: 46 | LS_JAVA_OPTS: -Xms256m -Xmx256m 47 | LOGSTASH_INTERNAL_PASSWORD: changeme 48 | depends_on: 49 | - elasticsearch 50 | restart: unless-stopped 51 | 52 | kibana: 53 | build: 54 | context: ./logging-example/kibana 55 | args: 56 | ELASTIC_VERSION: 8.5.2 57 | volumes: 58 | - ./logging-example/kibana/config/kibana.yml:/usr/share/kibana/config/kibana.yml:ro,Z 59 | ports: 60 | - 5601:5601 61 | environment: 62 | KIBANA_SYSTEM_PASSWORD: changeme 63 | depends_on: 64 | - elasticsearch 65 | restart: unless-stopped 66 | 67 | filebeat: 68 | build: 69 | context: ./logging-example/filebeat 70 | args: 71 | ELASTIC_VERSION: 8.5.2 72 | entrypoint: "filebeat -e -strict.perms=false" 73 | volumes: 74 | - ./logging-example/filebeat/config/filebeat.yml:/usr/share/filebeat/filebeat.yml 75 | - ./nginx/log:/var/log/nginx # nginx log path (require same option on nginx container) 76 | depends_on: 77 | - logstash 78 | - elasticsearch 79 | - kibana 80 | 81 | volumes: 82 | elasticsearch: 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docker Example 2 | 3 | **English** | [한국어](https://github.com/litsynp/docker-example/blob/main/README.ko.md) 4 | 5 | ![Web App Screenshot](https://user-images.githubusercontent.com/42485462/175573053-a9292722-8c12-492a-b60c-14cb4d12fab5.png) 6 | 7 | This is a repository to demonstrate how to develop a full-stack web application using Docker. 8 | 9 | You can simply run the application with [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/). 10 | 11 | The application implemented is a todo app like a [Microsoft To Do](https://todo.microsoft.com/). 12 | 13 | ## Description 14 | 15 | - Back-end: [Django](https://www.djangoproject.com/) - [Django REST Framework](https://www.django-rest-framework.org/) (Created using `django-admin startproject backend`) 16 | 17 | - Front-end: [React](https://reactjs.org/) - [Create-React-App](https://create-react-app.dev/) (Created using `npx create-react-app frontend`) 18 | 19 | - Database: [PostgreSQL](https://www.postgresql.org/) 20 | 21 | ## Installation 22 | 23 | - Clone the repository using this command. 24 | 25 | ```sh 26 | $ git clone --recursive https://github.com/litsynp/docker-example.git 27 | ``` 28 | 29 | - If you missed `--recursive` option, fetch the frontend submodule using `git clone`. 30 | 31 | ## How to 32 | 33 | - Put all the python modules in `requirements.txt`. 34 | 35 | - Run `yarn` in `frontend` directory to fetch yarn modules. 36 | 37 | ```sh 38 | $ cd frontend 39 | $ yarn 40 | ``` 41 | 42 | - Run `docker compose up --build` in the root directory. You will create front-end, back-end and database Docker containers. 43 | 44 | - When the docker containers are all up and running, open another terminal and run `docker-compose exec backend python manage.py migrate` to proceed with data migration. If you skip this, you will encounter an error in Django backend container later, because the DB has not been initialized. 45 | 46 | ## Stop/Remove the servers and containers 47 | 48 | - `docker compose down` for just stopping the server. 49 | 50 | - If you want to run the server again after stopping it, run `docker compose up` again. 51 | 52 | - `docker compose down -v` to stop and **remove** the server. This will remove all the volumes of the containers. 53 | 54 | - If you want to run the server again after stopping and removing it, run `docker compose up --build`. Build only when you are running the server for the first time. 55 | 56 | ## Notes 57 | 58 | - The production build `prod` includes NGINX as a load balancer. 59 | 60 | - To run as production build, append `-f docker-compose.prod.yml` after `docker compose` for all the commands above. 61 | 62 | - e.g., `docker compose -f docker-compose.prod.yml up` 63 | 64 | - You can include `.vscode`, which includes workspace settings for [Visual Studio Code](https://code.visualstudio.com/), to `.gitignore` as well. It is included as an example here. 65 | 66 | ## Secret Management - `.env` 67 | 68 | `.env` files are for keeping secrets and credentials that should not be exposed to Git repository. 69 | 70 | You must inlcude them to `.gitignore` so that the Git repository doesn't include it. 71 | 72 | - There are sample `.env` files of `dev` and `prod` build in `settings` directory. 73 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # ==================== 2 | # Created by https://www.toptal.com/developers/gitignore/api/django 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=django 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | pytestdebug.log 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | doc/_build/ 78 | 79 | # PyBuilder 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 100 | __pypackages__/ 101 | 102 | # Celery stuff 103 | celerybeat-schedule 104 | celerybeat.pid 105 | 106 | # SageMath parsed files 107 | *.sage.py 108 | 109 | # Environments 110 | .env 111 | .venv 112 | env/ 113 | venv/ 114 | ENV/ 115 | env.bak/ 116 | venv.bak/ 117 | pythonenv* 118 | 119 | # Spyder project settings 120 | .spyderproject 121 | .spyproject 122 | 123 | # Rope project settings 124 | .ropeproject 125 | 126 | # mkdocs documentation 127 | /site 128 | 129 | # mypy 130 | .mypy_cache/ 131 | .dmypy.json 132 | dmypy.json 133 | 134 | # Pyre type checker 135 | .pyre/ 136 | 137 | 138 | *.pyc 139 | mediafiles/* 140 | 141 | # If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ 142 | # in your Git repository. Update and uncomment the following line accordingly. 143 | # /staticfiles/ 144 | staticfiles/* 145 | 146 | # pytype static type analyzer 147 | .pytype/ 148 | 149 | # profiling data 150 | .prof 151 | 152 | # End of https://www.toptal.com/developers/gitignore/api/django 153 | 154 | # macOS specific files 155 | .DS_Store 156 | -------------------------------------------------------------------------------- /README.ko.md: -------------------------------------------------------------------------------- 1 | # Docker Example 2 | 3 | [English](https://github.com/litsynp/docker-example/blob/main/README.md) | **한국어** 4 | 5 | ![Web App Screenshot](https://user-images.githubusercontent.com/42485462/175573053-a9292722-8c12-492a-b60c-14cb4d12fab5.png) 6 | 7 | Docker를 이용한 간단한 full stack 개발을 위해 만들어진 예시용 repository 입니다. 8 | 9 | [Docker](https://www.docker.com/)와 [Docker Compose](https://docs.docker.com/compose/)를 이용해 웹 애플리케이션을 간단하게 실행해볼 수 있습니다. 10 | 11 | 구현된 예제는 [Microsoft To Do 앱](https://todo.microsoft.com/)과 유사한 Todo 앱입니다. 12 | 13 | ## 설명 14 | 15 | - 백엔드: [Django](https://www.djangoproject.com/) - [Django REST Framework](https://www.django-rest-framework.org/) (`django-admin startproject backend`으로 생성) 16 | 17 | - 프론트엔드: [React](https://reactjs.org/) - [Create-React-App](https://create-react-app.dev/) (`npx create-react-app frontend`으로 생성) 18 | 19 | - 데이터베이스 (DB): [PostgreSQL](https://www.postgresql.org/) 20 | 21 | ## 설치 22 | 23 | - 해당 repository를 서브모듈과 함께 clone 받습니다. 24 | 25 | ```sh 26 | $ git clone --recursive https://github.com/litsynp/docker-example.git 27 | ``` 28 | 29 | - `--recursive` 옵션을 빼고 실행하셨다면, `git clone`을 이용해 프론트엔드 모듈을 따로 받아줍니다. 30 | 31 | ## 사용법 32 | 33 | - `requirements.txt`에 필요한 모듈을 담아둡니다. 34 | 35 | - frontend 경로에서 `yarn`을 해서 모듈을 최신화합니다. 36 | 37 | ```sh 38 | $ cd frontend 39 | $ yarn 40 | ``` 41 | 42 | - 프로젝트 루트 디렉토리에서 `docker compose up --build`를 실행합니다. 프론트엔드와 백엔드, 데이터베이스 도커 컨테이너를 생성하게 됩니다. 43 | 44 | - 도커 컨테이너가 완전히 올라간 후, 다른 터미널을 하나 더 열어서 `docker-compose exec backend python manage.py migrate` 를 입력하여 데이터 마이그레이션을 진행합니다. 이를 진행하지 않으면 DB가 생성되지 않기 때문에 나중에 Django 백엔드에서 오류가 발생합니다. 45 | 46 | ## 서버와 컨테이너 중지 및 삭제 47 | 48 | - 서버를 내릴 땐 `docker compose down`을 실행합니다. 49 | 50 | - 서버를 내린 후 다시 올릴 땐 `docker compose up`을 실행하면 됩니다. 51 | 52 | - 서버를 내리고 **삭제까지** 할 때는 `docker compose down -v`를 실행합니다. 컨테이너의 볼륨도 지우게 됩니다. 53 | 54 | - 서버를 내리고 삭제한 후 다시 올릴 땐 `docker compose up --build`을 실행하면 됩니다. 항상 처음에만 빌드하면 됩니다. 55 | 56 | ## 추가 사항 57 | 58 | - Production build (배포 버전)에는 로드 밸런서로 사용할 NGINX가 포함되어 있습니다. 59 | 60 | - Production build로 실행하려면 위의 명령어들 (`docker compose`) 뒤에 `-f docker-compose.prod.yml` 명령어를 붙여서 사용하면 됩니다. 61 | 62 | - (e.g., `docker compose -f docker-compose.prod.yml up`) 63 | 64 | - [Visual Studio Code](https://code.visualstudio.com/)의 워크스페이스 설정을 담고 있는 폴더인 `.vscode`를 `.gitignore`에 추가하셔도 됩니다. 여기서는 예시로 포함해두었습니다. 65 | 66 | ## 비밀 파일 관리 - `.env` 67 | 68 | `.env` 파일은 주로 공개되선 안되는 비밀 정보를 담는 데에 사용되며, Git repository에 공개되선 안됩니다. 69 | 70 | `.gitignore` 파일에 `.env`를 추가해 Git repository에 추가되지 않도록 합시다. 71 | 72 | - `settings` 디렉토리에 예시를 위한 `dev`, `prod` 빌드 버전의 `.env` 파일이 존재합니다. 73 | 74 | ## ELK 로깅 75 | 76 | ![es-index-management-screenshot](https://user-images.githubusercontent.com/42485462/213172320-be589d5e-81c0-4c2f-bdcd-5031d45cd834.png) 77 | 78 | - logging-example 서브 모듈을 사용하여 **배포 버전**에서 로깅이 가능합니다. 79 | - *단일 compose 파일 실행시 동작하지 않습니다.* 80 | 81 | ### 동작 방식 82 | 1. NGINX의 로그파일을 Filebeat로 수집 83 | 2. 수집한 로그를 Logstash에 전달 84 | 3. 전달 받은 로그를 Elasticsearch에 저장 85 | 4. 저장된 로그를 Kibana를 통해 분석 86 | 87 | ### 사용법 88 | 89 | ```sh 90 | $ docker compose -f docker-compose.prod.yml -f docker-compose.logging.yml up --build 91 | ``` 92 | 93 | ### Kibana를 통한 로그 시각화 방법 94 | 1. localhost:5601 접속 95 | 2. 상단 메뉴에서 index management 검색 96 | 3. 수집된 로그 인덱스 확인 weblogs-yyyy.MM.dd 형식 97 | 4. 좌측 메뉴의 Analytics의 Dashboard 클릭 98 | 5. Create data view를 통해 인덱스 선택 99 | - 전체 조회 : Index Pattern에 `weblogs-*` 입력 및 저장 100 | - 선택 조회 : Index Pattern에 보고싶은 날짜입력 (eg. `weblogs-2023.01.01`) 101 | 6. Create Visualization 클릭 102 | 7. 보고 싶은 필드를 화면에 드롭다운 하여 시각화 -------------------------------------------------------------------------------- /backend/backend/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for backend project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.2.4. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.2/ref/settings/ 11 | """ 12 | 13 | import os 14 | from pathlib import Path 15 | 16 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 17 | BASE_DIR = Path(__file__).resolve().parent.parent 18 | 19 | SETTINGS_PATH = os.path.dirname(os.path.dirname(__file__)) 20 | 21 | # Quick-start development settings - unsuitable for production 22 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 23 | 24 | # SECURITY WARNING: keep the secret key used in production secret! 25 | SECRET_KEY = os.getenv('SECRET_KEY', 'foo') 26 | 27 | # SECURITY WARNING: don't run with debug turned on in production! 28 | DEBUG = int(os.getenv('DEBUG', 1)) 29 | 30 | if os.getenv('DJANGO_ALLOWED_HOSTS'): 31 | ALLOWED_HOSTS = os.getenv('DJANGO_ALLOWED_HOSTS').split(' ') 32 | else: 33 | ALLOWED_HOSTS = ['localhost', '127.0.0.1', '[::1]'] 34 | 35 | 36 | # Application definition 37 | 38 | INSTALLED_APPS = [ 39 | 'django.contrib.admin', 40 | 'django.contrib.auth', 41 | 'django.contrib.contenttypes', 42 | 'django.contrib.sessions', 43 | 'django.contrib.messages', 44 | 'django.contrib.staticfiles', 45 | 46 | 'rest_framework', 47 | 48 | 'todos', 49 | ] 50 | 51 | MIDDLEWARE = [ 52 | 'django.middleware.security.SecurityMiddleware', 53 | 'django.contrib.sessions.middleware.SessionMiddleware', 54 | 'corsheaders.middleware.CorsMiddleware', 55 | 'django.middleware.common.CommonMiddleware', 56 | 'django.middleware.csrf.CsrfViewMiddleware', 57 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 58 | 'django.contrib.messages.middleware.MessageMiddleware', 59 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 60 | ] 61 | 62 | ROOT_URLCONF = 'backend.urls' 63 | 64 | TEMPLATES = [ 65 | { 66 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 67 | 'DIRS': [os.path.join(SETTINGS_PATH), 'templates'], 68 | 'APP_DIRS': True, 69 | 'OPTIONS': { 70 | 'context_processors': [ 71 | 'django.template.context_processors.debug', 72 | 'django.template.context_processors.request', 73 | 'django.contrib.auth.context_processors.auth', 74 | 'django.contrib.messages.context_processors.messages', 75 | ], 76 | }, 77 | }, 78 | ] 79 | 80 | WSGI_APPLICATION = 'backend.wsgi.application' 81 | 82 | 83 | # Database 84 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 85 | 86 | # PostgreSQL 87 | DATABASES = { 88 | 'default': { 89 | 'ENGINE': os.environ.get('SQL_ENGINE', 'django.db.backends.sqlite3'), 90 | 'NAME': os.environ.get('SQL_DATABASE', os.path.join(BASE_DIR, 'db.sqlite3')), 91 | 'USER': os.environ.get('SQL_USER', 'eg_user'), 92 | 'PASSWORD': os.environ.get('SQL_PASSWORD', 'eg_pw'), 93 | 'HOST': os.environ.get('SQL_HOST', 'eg_db'), 94 | 'PORT': os.environ.get('SQL_PORT', '5432'), 95 | } 96 | } 97 | 98 | 99 | # Password validation 100 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 101 | 102 | AUTH_PASSWORD_VALIDATORS = [ 103 | { 104 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 105 | }, 106 | { 107 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 108 | }, 109 | { 110 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 111 | }, 112 | { 113 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 114 | }, 115 | ] 116 | 117 | 118 | # CORS realted settings 119 | 120 | CORS_ORIGIN_ALLOW_ALL = True 121 | 122 | # CORS_ALLOW_METHODS = ['DELETE','GET','OPTIONS','PATCH','POST','PUT'] 123 | 124 | CORS_ORIGIN_WHITELIST = ['http://localhost:3000', 125 | 'http://localhost:8000', 126 | 'http://localhost:80', 127 | 'http://localhost'] 128 | 129 | CORS_ALLOW_CREDENTIALS = True 130 | 131 | 132 | # Internationalization 133 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 134 | 135 | LANGUAGE_CODE = 'ko-kr' 136 | 137 | TIME_ZONE = 'Asia/Seoul' 138 | 139 | USE_I18N = True 140 | 141 | USE_L10N = True 142 | 143 | USE_TZ = True 144 | 145 | 146 | # Static files (CSS, JavaScript, Images) 147 | # https://docs.djangoproject.com/en/3.1/howto/static-files/ 148 | 149 | STATIC_URL = '/staticfiles/' 150 | STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') 151 | 152 | # Media files (User uploaded files) 153 | 154 | MEDIA_URL = '/mediafiles/' 155 | MEDIA_ROOT = os.path.join(BASE_DIR, 'mediafiles') 156 | 157 | 158 | # Default primary key field type 159 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 160 | 161 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 162 | 163 | ENABLE_LOGGING = True 164 | 165 | if ENABLE_LOGGING: 166 | # Enable SQL queries logging 167 | LOGGING = { 168 | 'version': 1, 169 | 'filters': { 170 | 'require_debug_true': { 171 | '()': 'django.utils.log.RequireDebugTrue', 172 | } 173 | }, 174 | 'handlers': { 175 | 'console': { 176 | 'level': 'DEBUG', 177 | 'filters': ['require_debug_true'], 178 | 'class': 'logging.StreamHandler', 179 | } 180 | }, 181 | 'loggers': { 182 | 'django.db.backends': { 183 | 'level': 'DEBUG', 184 | 'handlers': ['console'], 185 | } 186 | } 187 | } 188 | --------------------------------------------------------------------------------