5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.js×DjangoアプリケーションをDocker環境で開発する(後編)

Last updated at Posted at 2024-04-20

はじめに

本記事は後編である。
前編では、フロントエンド/バックエンドをそれぞれ立ち上げるところまで実装した。

本編ではテストとして、記事を投稿できるアプリケーションを実装する。

また本番環境へのデプロイを想定して、Django側のアプリケーション起動を
python manage.py runserver 0.0.0.0:8000
ではなく、Gunicornを用いて起動し、Nginxコンテナとバインドする形に変更する。

nginxの導入

DjangoプロジェクトをNginxとバインドしてアクセス可能にするためにNginxのDockerコンテナを作成する。また、DjangoにGunicornを導入する。

ディレクトリの作成

sample_project/ 配下にnginxコンテナ用のディレクトリを作成する。

host
mkdir nginx

各種ファイルの作成

以下の2つのファイルを作成する。

1つ目のファイルはDockerfileで、nginxのバージョン1.18.0-alpineをベースに、nginxの設定ファイルをコンテナ内にコピーする。また、デフォルトの設定ファイルを削除する。

nginx/Dockerfile
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ブロックを設定する。

nginx/nginx.conf
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"と同じ値を使用する。

.env.dev
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も編集する。

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を編集して、環境変数から設定値を取得し、新たに追加したアプリケーションと各種ミドルウェアを登録します。また、データベースの設定も行います。

backend/sample_project/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を使用します。

backend/requirements.txt
Django
psycopg2
djangorestframework
django-cors-headers==3.11.0
gunicorn==20.1.0

Dockerコンテナの再起動

一度全てdockerコンテナを削除する。

host
docker compose down -v

そして、composeファイルを書き換えているので、ビルドからやり直す。

host
docker compose up -d --build

http://localhost:1317 にアクセスしてが表示されることを確認する(cssがくずれているかも)。

バックエンド

下準備

バックエンドの実装では、まずDjangoのコマンドstartappを使用してarticleというアプリケーションを作成する。

host
docker exec -it sample_project-backend-1 bash
#backend
python manage.py startapp article

Articleの作成

settings.pyへの反映

sample_project/settings.pyを編集してarticleというアプリをプロジェクトに反映させる。

backend/sample_project/settings.py
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_idarticle_headingarticle_bodyというフィールドがあります。

backend/article/models.py
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()

マイグレートの実行

モデルの作成後は、makemigrationsmigrateコマンドを使用してデータベースに反映します。

#backend
python manage.py makemigrations
python manage.py migrate

シリアライザの作成

article/ 配下にserializers.pyを作成する。

次に、article/ 配下にserializers.pyを作成し、モデルをJSON形式に変換するためのシリアライザを定義します。

backend/article/serializers.py
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操作が定義されます。

backend/article/viewsets.py
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を登録しています。

backend/sample_project/routers.py
from rest_framework import routers
from article.viewsets import ArticleViewSet

router = routers.DefaultRouter()
router.register('article', ArticleViewSet)

最後に、Djangoのurls.pyを編集して、作成したルータを登録します。

backend/sample_project/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を再起動する。

#backend
ps -a | grep gunicorn

表示されるPIDの一番小さいプロセスIDがmasterプロセスのため、killを行う。

#backend
kill -HUP <一番小さいプロセスID>

HUPオプションを付加することにより、単純にプロセスをkillするのではなく、プロセスを再起動(reload)させることができる。

アクセスしてみる

http://localhost:1317/api/article にアクセスし、(cssは崩れているが)以下のような画面が出ればOK。

image.png

フロントエンド

下準備

CSSファイルの削除

src/styles/globals.css に用意されているCSSを消す。以下の状態にする。

frontend/sample_project/src/styles/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;

フロントエンドのコンテナに入る

host
docker exec -it nextdjango-frontend-1 bash

npm installをしておく。

#frontend
cd sample_project
npm install

axiosのインストール

frontendのdockerコンテナにexecで入り、axiosをインストールする。

#frontend
npm install axios --save

メイン画面の作成

src/pages/index.tsx に簡単なCRUD機能を実装する。編集機能は未実装。

中で用いる環境変数は後述する。

src/pages/index.tsx
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>
  );
}

動作確認

開発用に立ち上げる。

#frontend
npm run dev

http://localhost:3000にアクセスして確認する。

色々入力したり、リロードしたりして、テストする。

image.png

ここまでで、CRUD(更新機能は未実装)アプリケーションをDocker環境で開発できる状態が整った。

余力があれば本番環境(VPS)へのデプロイについても投稿したい。

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?