web_bonsaiの日記

web開発の学習日記です。誰に見せるためでもないただの日記です。

本番環境にNext.jsアプリケーションをデプロイする | Mac + Docker + Rails + Next.js その0035

今日の環境

package.jsonの編集

scripts.startを以下の通り編集します。

"start": "next start -p 3001",

ログインして移動

いつも通りsshでサーバーにログインします。

docker-compose.ymlのあるディレクトリに移動します。

git pullする

最新のコードを取得します。

docker-compose downする

不慣れな作業の前にdocker-compose downします。

$ docker-compose down

本番環境のdocker-compose.ymlのnginxサービスのdepends_onにfrontendを追記する

以下のような感じになります。

  nginx:
    build: ./nginx
    ports:
      - "8000:8000"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./nginx/html:/var/www/html
      - ./nginx/log:/var/log
    depends_on:
      - app
      - frontend

本番環境のdocker-compose.ymlにfrontendサービスを追加する

以下の通り追記しました。本番環境なのでyarn startです。

  frontend:
    build:
      context: ./frontend
    ports:
      - '3001:3001'
    command: 'yarn start'
    volumes:
      - ./frontend:/usr/src/app

docker-compose buildする

$ docker-compose build

yarnコマンドが使えるか確認する

$ docker-compose run --rm frontend yarn -v

yarn installする

$ docker-compose run --rm frontend yarn install

yarn buildする

Next.jsアプリケーションをビルドします。

$ docker-compose run --rm frontend yarn build

docker-compose upする

$ docker-compose up

frontendサービスが以下のような感じで起動できたらOKだと思います。

frontend_1      | yarn run v1.22.19
frontend_1      | $ next start -p 3001
frontend_1      | ready - started server on 0.0.0.0:3001, url: http://localhost:3001
frontend_1      | info  - SWC minify release candidate enabled. https://nextjs.link/swcmin

一旦止める

control + c を押して一旦止めます。

docker-compose downもしておきます。

$ docker-compose down

nginx.conf にupstream nextjsを追記する

以下のような感じで追記します。

  # upstreamのnextjsを定義
  upstream nextjs {
    # server service名:3001; のように記述
    server frontend:3001;
  }

locationを追記する

以下の通り追記しました。

    # location
    location / {
      proxy_pass http://nextjs/;
    }

docker-compose upする

以下のコマンドを実行します。

$ docker-compose up

アクセスしてみる

本番環境のURLにアクセスして、Next.jsのスタートページが表示されることを確認しました。

他のURLもこれまで通り表示されるか確認しておきます。

一旦止める

control + c を押して一旦止めます。

docker-compose downもしておきます。

$ docker-compose down

docker-compose up -dする

以下のコマンドで起動しなおします。

$ docker-compose up -d

もう一度ブラウザで確認する

もう一度ブラウザで確認します。

ここまでやった段階での本番環境のdocker-compose.yml

version: "3.9"
services:
  nginx:
    build: ./nginx
    ports:
      - "8000:8000"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./nginx/html:/var/www/html
      - ./nginx/log:/var/log
    depends_on:
      - app
      - frontend
  https-portal:
    image: steveltn/https-portal:1
    ports:
      - 80:80
      - 443:443
    restart: always
    environment:
      DOMAINS: 'web-bonsai.tech -> http://nginx:8000'
      STAGE: production
    volumes:
      - ./https_portal/ssl_certs:/var/lib/https-portal
    depends_on:
      - nginx
  db:
    image: postgres
    volumes:
      - ./tmp/db:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: xxxxxxxxxx(何か強いパスワード)
  app:
    build: ./rails
    command: bash -c 'rm -f tmp/pids/server.pid && bundle exec pumactl start'
    volumes:
      - ./rails:/myapp
    environment:
      - RAILS_SERVE_STATIC_FILES=false
      - RAILS_ENV=production
      - MYAPP_DATABASE_PASSWORD=xxxxxxxxxx(何か強いパスワード)
    depends_on:
      - db
  frontend:
    build:
      context: ./frontend
    ports:
      - '3001:3001'
    command: 'yarn start'
    volumes:
      - ./frontend:/usr/src/app

ここまでやった段階での本番環境のnginx.conf

user nginx;

events {
  # 1ワーカーの接続数
  # worker_connections 2048;

  # 複数のリクエストを同時に受け付けるか
  multi_accept on;

  # 複数アクセスをさばくためにI/O多重化に使うシステムコールを指定する
  use epoll;
}

http {
  # HTTPレスポンスヘッダのContent_Typeに付与する文字コード
  charset UTF-8;

  # HTTPレスポンスヘッダのServerにnginxのバージョンを入れるか(開発時以外は入れないほうが吉)
  server_tokens off;

  # MIMEタイプと拡張子の関連付けを定義したファイルを読み込む
  include /etc/nginx/mime.types;

  # 上記したmime.typesにマッチしなかった場合の設定
  # octet-streamは任意のバイナリコードを意味し、ブラウザでは実行したりせず、単にダウンロードする挙動をとるらしい
  default_type application/octet-stream;

  # upstreamのpumaを定義
  upstream puma {
    # server service名:3000; のように記述
    server app:3000;
  }

  # upstreamのnextjsを定義
  upstream nextjs {
    # server service名:3001; のように記述
    server frontend:3001;
  }

  # example.comの設定 (nginx:8000 の設定)
  server {
    # リスニングポート
    listen 8000;

    # ドメイン設定(ローカル環境ではlocalhostで、本番環境ではドメインを設定する。)
    # server_name example.com;
    server_name web-bonsai.tech

    # HTTPレスポンスヘッダのContent_Typeに付与する文字コード
    charset utf-8;

    # logの出力先
    access_log /var/log/nginx/access.log;
    error_log  /var/log/nginx/error.log;

    # ドキュメントルート
    root /var/www/html;

    # location共通の設定
    proxy_redirect off;
    proxy_set_header Host $http_host;
    proxy_set_header X-CSRF-Token $http_x_csrf_token;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-Proto $scheme;

    # location
    location /index.html {
      index index.html;
    }

    # location
    location /mpa/ {
      proxy_pass http://puma/mpa/;
    }

    # location
    location /api/ {
      proxy_pass http://puma/api/;
    }

    # location
    location / {
      proxy_pass http://nextjs/;
    }
  }
}

Next.jsのfrontend/pages/index.tsxでAPIから値を取得する | Mac + Docker + Rails + Next.js その0034

参考にさせていただいたページ

fetchの第一引数について

CORSの設定について

はじめに

今回の私のポイントとしては以下のようなことでした。

  • fetchの第一引数にはどういうURL文字列を渡したら良いか
  • RailsのCORSの設定をどうやったら良いか

frontend/pages/index.tsxにgetStaticProps()を定義する

frontend/pages/index.tsxに以下の通り、getStaticProps()を定義して、Homeコンポーネントでpropsを受け取るようにしました。

〜略〜

export const getStaticProps = async () => {
  const response = await fetch('http://nginx:8000/api/tasks');
  const json = await response.json();

  return {
    props: json,
  };
};

const Home: NextPage = (props) => {
  console.log(props);

  〜略〜
}

export default Home

fetchの第一引数は "http://[サービス名]:[ポート番号]/api/tasks" みたいな感じで渡すと大丈夫でした。

RailsのCORSの設定

rails/Gemfileの編集

以下の行を追記しました。

gem 'rack-cors'

bundle install

% docker-compose run --rm app bundle install

build

Gemfileを編集したのでbuildし直します。

% docker-compose build

rails/config/initializers/cors.rbの作成とその記述内容

% vim rails/config/initializers/cors.rb

以下の通り記述しました。

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins "*"
    resource "*",
             headers: :any,
             methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end

rails/config/environments/development.rbの編集

rails/config/environments/development.rbのendの内側の一番下にconfig.hosts << "nginx"と一行追記しました。

以下のようになります。

require "active_support/core_ext/integer/time"

Rails.application.configure do
  〜略〜
  # localhost:ポート番号では通信に失敗します。
  # fetchの引数に渡しているサービス名と合わせます。
  config.hosts << "nginx"
end

docker-compose restartする

railsのconfigを変更したので、設定反映のためにrestartします。

% docker-compose restart

ブラウザで確認してみる

以下の「httpのURL」と「httpsのURL」にブラウザでアクセスして、ブラウザのコンソールでpropsが確認できました。

rails/config/initializers/cors.rbを少し修正する

originsはワイルドカードじゃない方が良いと思うので、以下の通り修正します。

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '127.0.0.1:3000', 'localhost:3000'
    resource '*',
             headers: :any,
             methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end

ブラウザで確認してみる

一度restartします。

% docker-compose restart

以下の「httpのURL」と「httpsのURL」にブラウザでアクセスして、ブラウザのコンソールでpropsが確認できたら完成かなと思います。

railsアプリケーションの「タスクの新規登録」が動かなくなっていたので直す

だいぶ後になってわかったことですが、CORSを設定したことによって動かなくなっていたようです。

大事なのでここに追記しておきます。

「タスクの新規登録」ページからPOSTすると以下のエラーが出るようになりました。

ActionController::InvalidAuthenticityToken (HTTP Origin header (https://web-bonsai.com) didn't match request.base_url (http://web-bonsai.com)):

【Rails/Nginx/ELB 】ActionController::InvalidAuthenticityToken HTTP Origin header (https://example.com) didn’t match request.base_url (http://example.com)の解決法|Rubinistを目指す新米エンジニアのTECH BLOG」を参考にさせていただいて解決しました。

以下の行を追記します。

proxy_set_header X-Forwarded-SSL on;

おまけ

上手くいかない場合1

Uncaught FetchError: request to http://localhost:8000/api/tasks failed, reason: connect ECONNREFUSED 127.0.0.1:8000 のようなエラーが表示されて上手くいかない場合は、fetchの第一引数が適切でないかもしれません。

上手くいかない場合2

FetchError: invalid json response body at http://nginx:8000/api/tasks reason: Unexpected token < in JSON at position 0 のようなエラーが表示されて上手くいかない場合は、以下のようにgetStaticPropsの中で console.log(response) して、responseを確認してみると良いと思います。

export const getStaticProps = async () => {
  const response = await fetch('http://nginx:8000/api/tasks');
  console.log('==============================')
  console.log(response)
  console.log('==============================')
  const json = await response.json();

  return {
    props: json,
  };
};

ターミナルを見ると以下のように403エラーが出力されていると思います。

==============================
Response {
  size: 0,
  timeout: 0,
  [Symbol(Body internals)]: {
    body: PassThrough {
      _readableState: [ReadableState],
      _events: [Object: null prototype],
      _eventsCount: 2,
      _maxListeners: undefined,
      _writableState: [WritableState],
      allowHalfOpen: true,
      [Symbol(kCapture)]: false,
      [Symbol(kCallback)]: null
    },
    disturbed: false,
    error: null
  },
  [Symbol(Response internals)]: {
    url: 'http://nginx:8000/api/tasks',
    status: 403,
    statusText: 'Forbidden',
    headers: Headers { [Symbol(map)]: [Object: null prototype] },
    counter: 0
  }
}
==============================

そんなときはRailsのCORSの設定が上手くいっていないとか、docker-compose restartを忘れてるのかもしれません。

Next.jsのサービスを追加してブラウザでNext.jsのスタートページを表示する | Mac + Docker + Rails + Next.js その0033

参考にさせていただいたページ

createする

TSでやっていくので、docker-compose.ymlのあるディレクトリで以下のコマンドを実行しました。

% yarn create next-app --ts frontend

frontend/Dockerfileの作成とその記述内容

% vim frontend/Dockerfile

以下の通り記述しました。

FROM node:16.16

ENV APP_PATH /usr/src/app
RUN mkdir $APP_PATH
WORKDIR $APP_PATH

COPY package.json yarn.lock $APP_PATH/
RUN yarn

COPY . $APP_PATH/

docker-compose.ymlの編集とその記述内容

% vim docker-compose.yml

以下の通りfrontendサービスを追加しました。:3000はrailsに割り当てられているので、frontendは:3001を使うことにします。

version: "3.9"
services:
  〜略〜
  frontend:
    build:
      context: ./frontend
    ports:
      - '3001:3001'
    command: 'yarn dev'
    volumes:
      - ./frontend:/usr/src/app

この段階で、ローカル環境のdocker-compose.ymlの記述内容は以下の通りです。

version: "3.9"
services:
  nginx:
    build: ./nginx
    ports:
      - "8000:8000"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./nginx/html:/var/www/html
      - ./nginx/log:/var/log
    depends_on:
      - app
  https-portal:
    image: steveltn/https-portal:1
    ports:
      - 80:80
      - 443:443
    restart: always
    environment:
      DOMAINS: 'localhost -> http://nginx:8000'
      STAGE: local
    volumes:
      - ./https_portal/ssl_certs:/var/lib/https-portal
    depends_on:
      - nginx
  db:
    image: postgres
    volumes:
      - ./tmp/db:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: password
  app:
    build: ./rails
    ports:
      - "3000:3000"
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - ./rails:/myapp
    environment:
      - RAILS_SERVE_STATIC_FILES=false
      - RAILS_ENV=development
    depends_on:
      - db
  frontend:
    build:
      context: ./frontend
    ports:
      - '3001:3001'
    command: 'yarn dev'
    volumes:
      - ./frontend:/usr/src/app

frontend/package.jsonの編集とその記述内容

package.jsonのscripts.devを以下の通り編集します。

  "scripts": {
    "dev": "next dev -p 3001",
    〜略〜
  },

ここまでやった段階でのfrontend/package.jsonの記述内容は以下の通りです。

{
  "name": "frontend",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev -p 3001",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "next": "12.2.1",
    "react": "18.2.0",
    "react-dom": "18.2.0"
  },
  "devDependencies": {
    "@types/node": "18.0.3",
    "@types/react": "18.0.15",
    "@types/react-dom": "18.0.6",
    "eslint": "8.19.0",
    "eslint-config-next": "12.2.1",
    "typescript": "4.7.4"
  }
}

buildする

一度buildしてみます。

% docker-compose build

upする

upします。

% docker-compose up

ブラウザでhttpの3000と3001を確認してみる

以下のURLにブラウザでアクセスすると、それぞれページが表示されます。

downする

一度downします。

% docker-compose down

nginx/nginx.confの編集とその記述内容

以下の行を追記します。

  # upstreamのnextjsを定義
  upstream nextjs {
    # server service名:3001; のように記述
    server frontend:3001;
  }

serverのブロックのlocationを変更します。railsのcontrollerをnamespace/mpa//api/でそれぞれ作成しているので、nginxのlocationも合わせています。

  server {
  〜略〜
    # location
    location /index.html {
      index index.html;
    }

    # location
    location /mpa/ {
      #proxy_set_header X-CSRF-Token $http_x_csrf_token;
      #proxy_set_header X-Real-IP $remote_addr;
      proxy_pass http://puma/mpa/;
      #proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $http_host;
      #proxy_set_header X-Forwarded-Proto $scheme;
      proxy_redirect off;
    }

    # location
    location /api/ {
      #proxy_set_header X-CSRF-Token $http_x_csrf_token;
      #proxy_set_header X-Real-IP $remote_addr;
      proxy_pass http://puma/api/;
      #proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $http_host;
      #proxy_set_header X-Forwarded-Proto $scheme;
      proxy_redirect off;
    }

    # location
    location / {
      proxy_pass http://nextjs/;
      proxy_set_header Host $http_host;
      proxy_redirect off;
    }
  }

ここまでやった段階でのnginx/nginx.confは以下の通りになっています。

user nginx;

events {
  # 1ワーカーの接続数
  # worker_connections 2048;

  # 複数のリクエストを同時に受け付けるか
  multi_accept on;

  # 複数アクセスをさばくためにI/O多重化に使うシステムコールを指定する
  use epoll;
}

http {
  # HTTPレスポンスヘッダのContent_Typeに付与する文字コード
  charset UTF-8;

  # HTTPレスポンスヘッダのServerにnginxのバージョンを入れるか(開発時以外は入れないほうが吉)
  server_tokens off;

  # MIMEタイプと拡張子の関連付けを定義したファイルを読み込む
  include /etc/nginx/mime.types;

  # 上記したmime.typesにマッチしなかった場合の設定
  # octet-streamは任意のバイナリコードを意味し、ブラウザでは実行したりせず、単にダウンロードする挙動をとるらしい
  default_type application/octet-stream;

  # upstreamのpumaを定義
  upstream puma {
    # server service名:3000; のように記述
    server app:3000;
  }

  # upstreamのnextjsを定義
  upstream nextjs {
    # server service名:3001; のように記述
    server frontend:3001;
  }

  # web-bonsai.techの設定 (nginx:8000 の設定)
  server {
    # リスニングポート
    listen 8000;

    # ドメイン設定(ローカル環境ではlocalhostで、本番環境ではドメインを設定する。)
    # server_name web-bonsai.tech;
    server_name localhost

    # HTTPレスポンスヘッダのContent_Typeに付与する文字コード
    charset utf-8;

    # logの出力先
    access_log /var/log/nginx/access.log;
    error_log  /var/log/nginx/error.log;

    # ドキュメントルート
    root /var/www/html;

    # location
    location /index.html {
      index index.html;
    }

    # location
    location /mpa/ {
      #proxy_set_header X-CSRF-Token $http_x_csrf_token;
      #proxy_set_header X-Real-IP $remote_addr;
      proxy_pass http://puma/mpa/;
      #proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $http_host;
      #proxy_set_header X-Forwarded-Proto $scheme;
      proxy_redirect off;
    }

    # location
    location /api/ {
      #proxy_set_header X-CSRF-Token $http_x_csrf_token;
      #proxy_set_header X-Real-IP $remote_addr;
      proxy_pass http://puma/api/;
      #proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $http_host;
      #proxy_set_header X-Forwarded-Proto $scheme;
      proxy_redirect off;
    }

    # location
    location / {
      proxy_pass http://nextjs/;
      proxy_set_header Host $http_host;
      proxy_redirect off;
    }
  }
}

再度buildする

再度buildしてみます。

% docker-compose build

upする

再度upします。

% docker-compose up

ブラウザでhttpsを確認してみる

以下のURLにブラウザでアクセスすると、それぞれページが表示されます。

https://localhost/ 」にアクセスしてコンソールを見ると以下のようなエラーが出ていますが、Hot Module Replacementに関するエラーなので一旦そのままにしておくことにします。

WebSocket connection to 'wss://localhost/_next/webpack-hmr'

failed:

init    @   websocket.js?a9be:45
connectHMR  @   websocket.js?a9be:30
eval    @   next-dev.js?3515:40

ちなみに「 http://localhost:3001/ 」にアクセスした場合はこのエラーは発生しません。

今回はここまでにしておきます。

tasksを取得できるapiエンドポイントを作成する | Mac + Docker + Rails + Next.js その0032

rails g controller

rails gします。

$ docker-compose run --rm app rails g controller api/tasks get post put delete

routes.rbの編集

各メソッドに対するアクションを設定します。

Rails.application.routes.draw do
  namespace :api do
    get    'tasks/', to: 'tasks#get'
    post   'tasks/', to: 'tasks#post'
    put    'tasks/', to: 'tasks#put'
    delete 'tasks/', to: 'tasks#delete'
  end

  〜略〜
end

rails/app/controllers/api/tasks_controller.rb の編集

以下の通り編集しました。

module Api
  class TasksController < ApplicationController
    def get
      tasks = Task.all.recent
      render json: {
        tasks:
      }
    end

    def post; end

    def put; end

    def delete; end
  end
end

画面に表示してみる

以下のURLにブラウザでアクセスすると表示できました。

https://localhost/api/tasks

fetchで取得してみる

任意のerbファイルに以下のように記述すれば取得できます。

<script>
fetch('/api/tasks/')
  .then(response => response.json())
  .then(data => {
    data.tasks.forEach((task) => {
      console.log(task);
    });
  });
</script>

FactoryBotをインストールしてRSpecでテストを書く | Mac + Docker + Rails その0031

概要

基本的には、以下の現場Railsの書籍の「Chapter5-1〜5-8. テストをはじめよう」を参考にしつつ、Webで色々検索しながら進めました。

book.mynavi.jp

概要としては以下のことをやりました。番号が振ってあるものは、現場RailsのChapter番号です。

  • 5-5-1. RSpecのインストールと初期準備
  • 5-5-2. Capybaraの初期準備
  • 5-5-3. FactoryBotのインストール
  • FactoryBot.create()を省略してcreate()と書けるように設定
  • rails/spec/rails_helper.rbの環境変数設定をENV['RAILS_ENV'] = 'test'にする
  • 5-7. FactoryBotでテストデータを作成できるように準備する
  • database_cleaner-active_recordをインストールして設定
  • 5-8. タスク一覧表示機能のSystemSpec
  • rubocopのRSpec/ExampleLengthとRSpec/MultipleExpectationsのルールを無効化

現場RailsのChapter4のログイン以外を一通りやる | Mac + Docker + Rails その0030

概要

以下の書籍のChapter4をやりました。

今の自分の目的からすると、ログイン機能はとりあえず不要なので、tasksテーブルのmigrationなど、タスク管理に関係する箇所だけ実装しました。

book.mynavi.jp

現場RailsのChapter3を一通りやる | Mac + Docker + Rails その0029

概要

ここらでデータベースを使って、CRUDの機能作成をしたいので、以下の書籍のChapter3をやりました。

書籍と違う点としては、mpaというnamespaceでcontrollerをgenerateしています。

book.mynavi.jp

表示確認と動作確認をする

以下のページが閲覧できるようになるイメージです。