Okan Çakmak의 How to Deploy Django REST Framework and React-Redux application with Docker 에 기반하여 작성한 글 입니다.
mkdir article
cd article
python3 -m venv venv
source venv/bin/activate
pip install django djangorestframework django-cors-headers gunicorn
mkdir backend
django-admin startproject config backend
cd backend
mkdir apps && mkdir apps/articles
django-admin startapp articles apps/articles
python manage.py migrate && python manage.py runserver
System check identified no issues (0 silenced).
XXXX XX, 2020 - XX:XX:XX
Django version 3.X.X, using settings 'config.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
pip freeze > requirements.txt
from django.db import models
class Article(models.Model):
title = models.CharField(max_length=50)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.titles
from rest_framework.permissions import AllowAny
from rest_framework.viewsets import ModelViewSet
from .models import Article
from .serializers import ArticleSerializer
class ArticleViewSet(ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
permission_classes = [AllowAny]
from rest_framework import serializers
from . import models
class ArticleSerializer(serializers.ModelSerializer):
class Meta:
fields = ("id", "title", "content", "created_at", "updated_at")
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("api/", include("apps.articles.urls")),
]
from rest_framework.routers import SimpleRouter
from . import views
router = SimpleRouter()
router.register(r'articles', views.ArticleViewSet, 'articles')
urlpatterns = router.urls
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# UI에서 API로의 호출을 위해 필요한 cors policy 세팅
"corsheaders",
# django 기반 rest-api framework == rest_framework
"rest_framework",
# 'articles' 추가
"apps.articles",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
# cors middleware 추가 (순서가 중요함!)
"corsheaders.middleware.CorsMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
# 개발 시에만 허용될 설정
CORS_ORIGIN_ALLOW_ALL = True
# makemigrations은 django로 하여 model 변화를 인식하게 한다 - migrations 폴더에 파일 생성되는 것 참고
python manage.py makemigrations
# migrate는 변경사항을 데이터베이스에 적용한다
python manage.py migrate
http://127.0.0.1:8000/api/articles
endpoint는 POST, GET, DELETE requests에 응답한다.
# 'article' directory
$ npx create-react-app frontend
$ cd frontend
# 개발 서버 실행
$ yarn start
# 현재 디렉토리 상황
article
├── backend
│ ├── apps
│ │ └── articles
│ │ └── migrations
│ └── config
└── frontend
├── public
└── src
$ cd .. && yarn add antd redux axios react-redux redux-thunk redux-logger
$ mkdir frontend/src/Containers frontend/src/Redux
import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import logger from "redux-logger";
import reducer from "./reducers.js";
const middlewares = [thunk, logger];
export default function configureStore(initialState) {
const store = createStore(
reducer,
initialState,
applyMiddleware(...middlewares)
);
return store;
}
const Types = {
POSTS_LOADING: "POSTS_LOADING",
GET_POSTS: "GET_POSTS",
DELETE_POST: "DELETE_POST",
CREATE_POST: "CREATE_POST",
};
export default Types;
import Types from "./types";
import axios from "axios";
export const getPosts = () => {
return (dispatch) => {
dispatch({ type: Types.POSTS_LOADING, payload: true });
axios
.get(`${process.env.REACT_APP_HOST_IP_ADDRESS}/api/posts`)
.then((response) => {
dispatch({ type: Types.GET_POSTS, payload: response.data });
})
.catch((err) => {
console.log(err);
dispatch({ type: Types.POSTS_LOADING, payload: false });
});
};
};
export const deletePost = (id, cb) => {
return (dispatch) => {
dispatch({ type: Types.POSTS_LOADING, payload: true });
axios
.delete(`${process.env.REACT_APP_HOST_IP_ADDRESS}/api/posts/${id}/`)
.then((response) => {
dispatch({ type: Types.DELETE_POST, payload: id });
cb();
})
.catch((err) => {
console.log(err);
});
};
};
export const createPost = (data, cb) => {
return (dispatch) => {
axios
.post(`${process.env.REACT_APP_HOST_IP_ADDRESS}/api/posts/`, data)
.then((response) => {
console.log(response);
dispatch({ type: Types.CREATE_POST, payload: response.data });
cb();
})
.catch((err) => {
console.log(err);
dispatch({ type: Types.POSTS_LOADING, payload: false });
});
};
};
import Types from "./types";
const initialState = {
posts: [],
loading: false,
};
const postReducer = (state = initialState, action) => {
switch (action.type) {
case Types.POSTS_LOADING: {
console.log("create_item");
return { ...state, loading: action.payload };
}
case Types.GET_POSTS: {
return { ...state, posts: action.payload };
}
case Types.DELETE_POST: {
return {
...state,
posts: state.posts.filter((post) => post.id != action.payload),
};
}
default:
return state;
}
};
export default postReducer;
import React from "react";
import { Layout, Menu } from "antd";
import "antd/dist/antd.css";
import ListPosts from "./ListPosts";
import CreateNewPost from "./CreateNewPost";
class Article extends React.Component {
constructor(props) {
super(props);
this.state = {
selected: 1,
};
}
handleClick = (e) => {
this.setState({ selected: e });
};
render() {
const { Header, Content } = Layout;
return (
<Layout>
<Header>
<Menu
theme="dark"
mode="horizontal"
defaultSelectedKeys={[this.state.selected.toString()]}
>
<Menu.Item key="1" onClick={() => this.handleClick(1)}>
{" "}
Read Posts
</Menu.Item>
<Menu.Item key="2" onClick={() => this.handleClick(2)}>
{" "}
Create Post
</Menu.Item>
</Menu>
</Header>
<Content>
{this.state.selected == 1 ? <ListPosts /> : <CreateNewPost />}
</Content>
</Layout>
);
}
}
export default Article;
import React from "react";
import { connect } from "react-redux";
import { createPost } from "../Redux/actions";
import "antd/dist/antd.css";
import { Form, Input, message, Button } from "antd";
class CreateNewPost extends React.Component {
onFinish(values) {
console.log(values);
this.props.createPost(values, this.info());
}
info() {
message.info("Post Created");
}
render() {
const posts = this.props.posts;
console.log(posts);
const layout = {
labelCol: { span: 5 },
wrapperCol: { span: 12 },
};
const validateMessages = {
required: "${label} is required!",
types: {
email: "${label} is not validate email!",
number: "${label} is not a validate number!",
},
number: {
range: "${label} must be between ${min} and ${max}",
},
};
return (
<Form
{...layout}
name="nest-messages"
onFinish={this.onFinish.bind(this)}
validateMessages={validateMessages}
>
<Form.Item name="title" label="Title" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item name="content" label="Content" rules={[{ required: true }]}>
<Input.TextArea />
</Form.Item>
<Form.Item wrapperCol={{ ...layout.wrapperCol }}>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
}
}
const mapStateToProps = (state) => ({
posts: state.posts,
});
const mapDispatchToProps = {
createPost,
};
export default connect(mapStateToProps, mapDispatchToProps)(CreateNewPost);
import React from "react";
import { connect } from "react-redux";
import { getPosts, deletePost } from "../Redux/actions.js";
import { Card, Row, Col, message } from "antd";
import {
EditOutlined,
EllipsisOutlined,
DeleteOutlined,
} from "@ant-design/icons";
import "antd/dist/antd.css";
class ListPosts extends React.Component {
componentDidMount() {
this.props.getPosts();
}
deletePost = (id) => {
this.props.deletePost(id, this.info);
};
info() {
message.info("Post Deleted");
}
render() {
const posts = this.props.posts;
console.log(posts);
return (
<Col span={12} offset={6}>
{posts.map((p) => (
<Row gutter={[48, 48]}>
<Col span={24}>
<Card
key={p.id}
title={p.title}
style={{ width: "100%" }}
actions={[
<DeleteOutlined
key="delete"
onClick={() => this.deletePost(p.id)}
/>,
]}
>
<p>{p.content}</p>
</Card>
</Col>
</Row>
))}
</Col>
);
}
}
const mapStateToProps = (state) => ({
posts: state.posts,
});
const mapDispatchToProps = {
getPosts,
deletePost,
};
export default connect(mapStateToProps, mapDispatchToProps)(ListPosts);
import React from "react";
import Article from "../Components/Article";
import { Provider as ReduxProvider } from "react-redux";
import configureStore from "../Redux/store";
const reduxStore = configureStore(window.REDUX_INITIAL_DATA);
function App() {
return (
<ReduxProvider store={reduxStore}>
<div className="App">
<Article />
</div>
</ReduxProvider>
);
}
export default App;
// Code above code stays is the same
import App from './Containers/App';
// Code below stays the same
# API 주소가 환경변수로 설정된 것에 주의
$ REACT_APP_HOST_IP_ADDRESS=http://localhost:8000 yarn start
# Rest of the lines will be remain the same
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get("SECRET_KEY")
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS").split(" ")
# CORS_ORIGIN_ALLOW_ALL = True
CORS_ORIGIN_WHITELIST =os.environ.get("CORS_ORIGIN_WHITELIST").split(" ")
FROM python:3
ENV PYTHONUNBUFFERED 1
ARG DJANGO_ALLOWED_HOSTS
ARG DJANGO_SECRET_KEY
ARG DJANGO_CORS_ORIGIN_WHITELIST
ENV DJANGO_ALLOWED_HOSTS $DJANGO_ALLOWED_HOSTS
ENV DJANGO_SECRET_KEY $DJANGO_SECRET_KEY
ENV DJANGO_CORS_ORIGIN_WHITELIST $DJANGO_CORS_ORIGIN_WHITELIST
RUN mkdir /backend
WORKDIR /backend
COPY requirements.txt /backend/
EXPOSE 8000
RUN pip install -r requirements.txt
COPY . /backend/
RUN python manage.py makemigrations
RUN python manage.py migrate
FROM node:14.4.0-alpine3.10
USER root
WORKDIR /frontend
COPY . /frontend
ARG API_URL
ENV REACT_APP_HOST_IP_ADDRESS $API_URL
RUN yarn
RUN yarn build
# in the article directory
$ mkdir webserver
upstream api {
server backend:8000;
}
server {
listen 8080;
location /api/ {
proxy_pass http://api$request_uri;
}
# ignore cache frontend
location ~* (service-worker\.js)$ {
add_header 'Cache-Control' 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
expires off;
proxy_no_cache 1;
}
location / {
root /var/www/frontend;
try_files $uri $uri/ /index.html;
}
}
version: '3'
services:
backend:
build:
context: ./backend
args:
DJANGO_ALLOWED_HOSTS: *
DJANGO_SECRET_KEY: *
DJANGO_CORS_ORIGIN_WHITELIST: *
command: gunicorn blog_backend.wsgi --bind 0.0.0.0:8000
ports:
- "8000:8000"
frontend:
build:
context: ./frontend
args:
API_URL: *
volumes:
- build_folder:/frontend/build
nginx:
image: nginx:latest
ports:
- 80:8080
volumes:
- ./webserver/nginx-proxy.conf:/etc/nginx/conf.d/default.conf:ro
- build_folder:/var/www/frontend
depends_on:
- backend
- frontend
volumes:
build_folder:
# nginx의 conf에 정의되어 있으므로 허용되는 호스트 변수에 대해 직접 api 를 사용할 수 있습니다 .
DJANGO_ALLOWED_HOSTS : ec2-3-122 ... amazonaws.com, api
DJANGO_SECRET_KEY : $a-fpiay!bdp+g=&df34578fv+$z3yd!!(%*0caqjf((r$2-jvs_
'DJANGO_CORS_ORIGIN_WHITELIST : http : // ec2- 3-122 -... amazonaws.com
API_URL : http : //ec2-3-122...amazonaws.com
백엔드 및 프런트 엔드 서비스에 다른 호스트를 사용하려면 CORS 화이트리스트 변수를 설정해야 할 수 있습니다.
$ docker-compose up
[ec2-user @ ip-172-31-21-251 ~] $ docker container ls
그러면 article_nginx_1 및 article_backend_1 이라는 2개의 활성 컨테이너를 확인할 수 있습니다. 앞서 언급했듯이 프론트엔드 도커는 코드를 빌드하고 웹 서버에 전달합니다. 그 후 종료됩니다.
부족한 글 읽어 주셔서 감사합니다. 틀린 부분 있다면 계속 수정해 나가겠습니다.
좋은 번역글 감사합니다. 다만 사소한 오류가 있어서 글남깁니다.
backend/config/settings.py에서
"SECRET_KEY" 대신 "DJANGO_SECRET_KEY",
"CORS_ORIGIN_WHITELIST" 대신 "DJANGO_CORS_ORIGIN_WHITELIST", (이부분은 원문 댓글에 있던데 저는 일단 이렇게 했습니다)
article/docker-compose.yml 에서
command: gunicorn blog_backend.wsgi --bind 0.0.0.0:8000 대신
command: gunicorn config.wsgi --bind 0.0.0.0:8000로 해서 성공했습니다.
다만, 올려주신 코드 그대로 react/redux 세팅을 하면 글을 post 요청은 할 수 있는데 저장이 되지 않습니다. 제가 react/redux를 모르는 탓에 어떻게 고쳐야할 지 모르겠네요 ㅠ
다시 한번 좋은 글 감사합니다!