はじめに
本記事は後編である。
前編では、フロントエンド/バックエンドをそれぞれ立ち上げるところまで実装した。
本編ではテストとして、記事を投稿できるアプリケーションを実装する。
また本番環境へのデプロイを想定して、Django側のアプリケーション起動を
python manage.py runserver 0.0.0.0:8000
ではなく、Gunicorn
を用いて起動し、Nginxコンテナとバインドする形に変更する。
nginxの導入
DjangoプロジェクトをNginxとバインドしてアクセス可能にするためにNginxのDockerコンテナを作成する。また、DjangoにGunicornを導入する。
ディレクトリの作成
sample_project/
配下にnginxコンテナ用のディレクトリを作成する。
mkdir nginx
各種ファイルの作成
以下の2つのファイルを作成する。
1つ目のファイルはDockerfileで、nginxのバージョン1.18.0-alpineをベースに、nginxの設定ファイルをコンテナ内にコピーする。また、デフォルトの設定ファイルを削除する。
FROM nginx:1.18.0-alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d
2つ目のファイルは、nginxの設定ファイルで、backendサービスを指すupstreamブロックと、クライアントからのリクエストをbackendサービスにプロキシするserverブロックを設定する。
upstream project {
server backend:8000;
}
server {
listen 80;
location / {
proxy_pass http://project;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
}
.env.devファイルの作成とdocker-compose.ymlの編集
今後、開発環境と本番環境で変数を分けて利用するため、env.devに環境変数をまとめる。
.env.devファイルの作成
docker-compose.ymlファイルと並列で、.env.devファイルを作成する。
SECRET_KEY
は、/backend/sample_project/settings.py
の"SECRET_KEY"
と同じ値を使用する。
NEXT_PUBLIC_BE_BASEURL=http://localhost:1317
DEBUG=True
SECRET_KEY='[/backend/sample_project/settings.pyの"SECRET_KEY"から引用]'
ALLOWED_HOSTS=127.0.0.1 [::1] localhost
DATABASE_NAME='sample_project'
DATABASE_USER='root'
DATABASE_PASSWORD='password'
DATABASE_HOST='db'
DATABASE_PORT='5432'
ALLOWED_ORIGINS=http://localhost:3000
POSTGRES_USER=root
POSTGRES_PASSWORD=password
POSTGRES_DB=sample_project
DATABASE=postgres
docker-compose.ymlを編集
これに伴い、docker-compose.ymlも編集する。
version: '3'
services:
frontend:
build:
context: frontend
tty: true
volumes:
- ./frontend:/frontend
ports:
- 3000:3000
env_file:
- ./.env.dev
environment:
- WATCHPACK_POLLING=true
backend:
build:
context: backend
command: gunicorn sample_project.wsgi:application --bind 0.0.0.0:8000
tty: true
volumes:
- ./backend:/backend
expose:
- 8000
depends_on:
- db
env_file:
- ./.env.dev
db:
image: postgres
env_file:
- ./.env.dev
volumes:
- postgres_data:/var/lib/postgresql/data
nginx:
build: ./nginx
ports:
- 1317:80
depends_on:
- backend
volumes:
postgres_data:
pgadmin4_data:
Django側の変更
settings.py の編集
settings.py
を編集して、環境変数から設定値を取得し、新たに追加したアプリケーションと各種ミドルウェアを登録します。また、データベースの設定も行います。
import os
...(略)
# SECRET_KEY = "既存の値をenvファイルに利用"
SECRET_KEY = os.environ.get("SECRET_KEY")
DEBUG = os.environ.get("DEBUG")
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS").split(" ")
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework', # 追加
'corsheaders', # 追加
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'corsheaders.middleware.CorsMiddleware', # 追加
]
...(略)
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': os.environ.get("DATABASE_NAME"),
'USER': os.environ.get("DATABASE_USER"),
'PASSWORD': os.environ.get("DATABASE_PASSWORD"),
'HOST': os.environ.get("DATABASE_HOST"),
'PORT': os.environ.get("DATABASE_PORT"),
}
}
...(略)
CORS_ALLOWED_ORIGINS = os.environ.get("ALLOWED_ORIGINS").split(" ")
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
requirements.txtの編集
django-cors-headers
はDjangoプロジェクトでCross-Origin Resource Sharing (CORS)ヘッダーを操作するためのパッケージです。これは、異なるオリジンからのリクエストを許可するために必要です。バージョン3.11.0を使用します。
gunicorn
はPython WSGI HTTPサーバーであり、DjangoやFlaskなどのWebアプリケーションを動かすために使われます。その効率性と簡単な使用方法から、本番環境でよく用いられます。バージョン20.1.0を使用します。
Django
psycopg2
djangorestframework
django-cors-headers==3.11.0
gunicorn==20.1.0
Dockerコンテナの再起動
一度全てdockerコンテナを削除する。
docker compose down -v
そして、composeファイルを書き換えているので、ビルドからやり直す。
docker compose up -d --build
http://localhost:1317 にアクセスしてが表示されることを確認する(cssがくずれているかも)。
バックエンド
下準備
バックエンドの実装では、まずDjangoのコマンドstartapp
を使用してarticle
というアプリケーションを作成する。
docker exec -it sample_project-backend-1 bash
python manage.py startapp article
Articleの作成
settings.pyへの反映
sample_project/settings.py
を編集してarticle
というアプリをプロジェクトに反映させる。
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'corsheaders',
'article' # 追加
]
models.pyの編集
models.py
を編集してArticle
という名前のモデルを作成します。このモデルにはarticle_id
、article_heading
、article_body
というフィールドがあります。
from django.db import models
class Article(models.Model):
article_id = models.AutoField(primary_key=True)
article_heading = models.CharField(max_length=250)
article_body = models.TextField()
マイグレートの実行
モデルの作成後は、makemigrations
とmigrate
コマンドを使用してデータベースに反映します。
python manage.py makemigrations
python manage.py migrate
シリアライザの作成
article/ 配下にserializers.pyを作成する。
次に、article/
配下にserializers.py
を作成し、モデルをJSON形式に変換するためのシリアライザを定義します。
from rest_framework import serializers
from .models import Article
class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = '__all__'
ViewSetの作成
さらに、article/
配下にviewsets.py
を作成し、APIのエンドポイントとなるViewSetを作成します。ViewSetでは、各種CRUD操作が定義されます。
from rest_framework import viewsets, filters
from .models import Article
from .serializers import ArticleSerializer
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
filter_backends = (filters.SearchFilter,)
search_fields = ('article_id', 'article_heading', 'article_body')
ルータの作成
その後、sample_project/
配下にrouters.py
を作成し、APIのルートを定義します。ここではarticle
という名前でViewSetを登録しています。
from rest_framework import routers
from article.viewsets import ArticleViewSet
router = routers.DefaultRouter()
router.register('article', ArticleViewSet)
最後に、Djangoのurls.py
を編集して、作成したルータを登録します。
from django.contrib import admin
from django.urls import path, include
from .routers import router
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include(router.urls))
]
これにより、以下のようなRESTfulなAPIエンドポイントが作成されます:
- GET:/api/article/ (記事の一覧を取得)
- POST:/api/article (新規記事を投稿)
- DELETE:/api/article/{article_id}/ (指定したIDの記事を削除)
- GET:/api/article/{article_id}/ (指定したIDの記事の詳細を取得)
- PUT:/api/article/{article_id}/ (指定したIDの記事を更新)
- PATCH:/api/article/{article_id}/ (指定したIDの記事の一部を更新)
動作確認
gunicornを再起動する
djangoのコードを変更した場合、以下の方法でgunicornを再起動する。
ps -a | grep gunicorn
表示されるPIDの一番小さいプロセスIDがmasterプロセスのため、killを行う。
kill -HUP <一番小さいプロセスID>
HUPオプションを付加することにより、単純にプロセスをkillするのではなく、プロセスを再起動(reload)させることができる。
アクセスしてみる
http://localhost:1317/api/article にアクセスし、(cssは崩れているが)以下のような画面が出ればOK。
フロントエンド
下準備
CSSファイルの削除
src/styles/globals.css
に用意されているCSSを消す。以下の状態にする。
@tailwind base;
@tailwind components;
@tailwind utilities;
フロントエンドのコンテナに入る
docker exec -it nextdjango-frontend-1 bash
npm installをしておく。
cd sample_project
npm install
axiosのインストール
frontendのdockerコンテナにexecで入り、axiosをインストールする。
npm install axios --save
メイン画面の作成
src/pages/index.tsx
に簡単なCRUD機能を実装する。編集機能は未実装。
中で用いる環境変数は後述する。
import { useEffect, useState } from "react";
import axios from "axios";
// Create a type for articles
type ArticleType = {
article_id: number;
article_heading: string;
article_body: string;
}
export default function Home() {
const [articles, setArticles] = useState<ArticleType[]>([]);
const [heading, setHeading] = useState("");
const [body, setBody] = useState("");
// Fetch articles from the backend
const getArticles = async () => {
try {
const response = await axios.get(`${process.env.NEXT_PUBLIC_BE_BASEURL}/api/article/`);
setArticles(response.data.reverse());
} catch (error) {
console.error(error);
}
};
// Post new article to the backend
const postNewArticle = async () => {
try {
await axios.post(`${process.env.NEXT_PUBLIC_BE_BASEURL}/api/article/`, {
article_heading: heading,
article_body: body
});
getArticles(); // Refresh the article list
setHeading(""); // Clear form
setBody("");
} catch (error) {
console.error(error);
}
};
// delete
const deleteArticle = async (article_id: number) => {
try {
await axios.delete(`${process.env.NEXT_PUBLIC_BE_BASEURL}/api/article/${article_id}/`);
getArticles(); // Refresh the list after deletion
} catch (error) {
console.error(error);
}
};
useEffect(() => {
getArticles();
}, []);
return (
<div className="container mx-auto p-4">
<h1 className="text-3xl font-bold text-center my-4">記事投稿ページ</h1>
<div className="mb-8">
<input
type="text"
placeholder="タイトル"
className="input input-bordered w-full mb-4 px-4 py-2 rounded text-lg border-2 border-gray-300 focus:border-blue-500 focus:outline-none"
value={heading}
onChange={(e) => setHeading(e.target.value)}
/>
<textarea
placeholder="本文"
className="textarea textarea-bordered w-full mb-4 px-4 py-2 rounded text-lg border-2 border-gray-300 focus:border-blue-500 focus:outline-none"
value={body}
onChange={(e) => setBody(e.target.value)}
/>
<button
onClick={postNewArticle}
className="btn btn-primary w-1/2 py-2 text-lg border-2 rounded border-gray-300 hover:border-blue-500 mx-auto block"
>
送信
</button>
</div>
<div>
{articles.map((article) => (
<div key={article.article_id} className="p-4 mb-2 shadow-lg rounded-lg relative">
<button
onClick={() => deleteArticle(article.article_id)}
className="absolute top-0 right-0 p-2 text-lg"
>
✗
</button>
<h2 className="text-2xl font-bold">{article.article_heading}</h2>
<p>{article.article_body}</p>
</div>
))}
</div>
</div>
);
}
動作確認
開発用に立ち上げる。
npm run dev
http://localhost:3000にアクセスして確認する。
色々入力したり、リロードしたりして、テストする。
ここまでで、CRUD(更新機能は未実装)アプリケーションをDocker環境で開発できる状態が整った。
余力があれば本番環境(VPS)へのデプロイについても投稿したい。