行動すれば次の現実

テック中心の個人ブログ

Rails7+bootstrap+esbuld+Postgresqlの構成でDockerの開発環境を構築する

Docker-Composeを使ったRails7の開発環境構築手順を説明します。

意外にもDocker構築を含めた一筆書きのRails7セットアップ手順が見受けられなかったので自分で試行錯誤しながら記事にしました。極力シンプルな手順を心がけています。誰かの参考になれば幸いです。

環境情報

  • MacBook Pro
    • MacOS Monterey(12.4)
  • Rails 7.0.4
  • Ruby 3.1.2
  • Postgresql 13.6
  • Docker 20.10.12
  • docker-compose 1.29.2
  • node.js 18.9.1
  • bootstrap 5.2.1

設定ファイルの準備

プロジェクトフォルダの作成

プロジェクトのフォルダを作成して、カレントディレクトリを移動しておきます。 空のGemfile.lock yarn.lockの空ファイルも事前に作成しておきます。

mkdir my_app
cd my_app
touch Gemfile.lock
touch yarn.lock

※ 「my_app」と記述されている箇所はご自身のアプリ名に置き換えてください。

Docker設定ファイル

RubyとNode.jsイメージを使用してDockerfileを作成します。 RubyのイメージにデフォルトでインストールされているNode.jsのバージョンををNode.jsイメージのバージョンに置き換えて構築します。

Dockerfile

FROM node:18.9.1 as node
FROM ruby:3.1.2

ENV TZ Asia/Tokyo

WORKDIR /my_app

COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]

COPY --from=node /usr/local/bin/node /usr/local/bin/node
COPY --from=node /usr/local/include/node /usr/local/include/node
COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules
RUN ln -s /usr/local/bin/node /usr/local/bin/nodejs && \
    ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm

RUN apt-get update -qq && apt-get install -y postgresql-client
RUN npm install yarn -g -y

COPY Gemfile Gemfile
COPY Gemfile.lock Gemfile.lock
RUN gem update --system && bundle install

COPY package.json yarn.lock ./
RUN yarn install

COPY . ./
EXPOSE 3000
CMD ["rails", "server", "-b", "0.0.0.0"]

続いて、docker-compose.ymlを作成します。webサーバーとdbサーバーの定義を記述します。

docker-compose.yml

version: "3.9"
services:
  web:
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bin/dev"
    tty: true
    stdin_open: true
    ports:
      - "3000:3000"
    volumes:
      - .:/my_app:delegated
      - bundle:/usr/local/bundle
    depends_on:
      - db
  db:
    image: postgres:13.6
    volumes:
      - postgres:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: my_app
      POSTGRES_PASSWORD: password
      POSTGRES_DB: my_app_development
      TZ: "Asia/Tokyo"
      POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --locale=C"

volumes:
  bundle:
  postgres:

Rails用のファイルの作成

Gemfileを作成します。Rails7以上を使用するという記述します。

Gemfile

source 'https://rubygems.org'
gem 'rails', '~> 7.0.0'

下記の内容でentrypoint.shを作成します。これはRails特有の問題を防ぐために使用されるスクリプトです。 サーバー内にserver.pidというファイルが先に存在していたときに、サーバーが再起動できなくなる問題を回避するために使用されます。

entrypoint.sh

#!/bin/bash
set -e

# Remove a potentially pre-existing server.pid for Rails.
rm -f /my_app/tmp/pids/server.pid

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"

package.json

空のjsonオブジェクトを定義しておきます

{}

ファイル構成の確認

上記を踏まえるとファイル構成は以下の通りになります。

my_app
  Dockerfile
  docker-compose.yml
  entrypoint.sh
  Gemfile
  Gemfile.lock
  package.json
  yarn.lock

環境構築

docker-compose build

docker-compose buildコマンドを使用してDocker環境を構築します。

docker-compose build --no-cache

rails new

続いてカレントディレクトリに対してrails newコマンドでファイルを展開します。

docker-compose run web rails new . --force --no-deps --database=postgresql --css=bootstrap --javascript=esbuild

rails newが完了するとターミナル上に以下のメッセージが表示されますのでpackage.jsonに追記します。

Add "scripts": { "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=assets" } to your package.json
Add "scripts": { "build:css": "sass ./app/assets/stylesheets/application.bootstrap.scss:./app/assets/builds/application.css --no-source-map --load-path=node_modules" } to your package.json

package.json

{
 "scripts": {
    "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=assets",
    "build:css": "sass ./app/assets/stylesheets/application.bootstrap.scss:./app/assets/builds/application.css --no-source-map --load-path=node_modules"
  },
 "dependencies": {
  // 省略...
}

application.jsの書き換え

このままではtooltipsやpopoverが動作しませんので、application.jsに以下を追記します。

app/javascript/application.js

document.addEventListener('turbo:load', () => {
  const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]')
  const popoverList = [...popoverTriggerList].map(d => new bootstrap.Popover(d))
  const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
  const tooltipList = [...tooltipTriggerList].map(d => new bootstrap.Tooltip(d))
})

Procfile.devの書き換え

-b '0.0.0.0'の部分を追記します。

web: bin/rails server -p 3000 -b '0.0.0.0'
js: yarn build --watch
css: yarn build:css --watch

database.ymlの書き換え

rails newで作成されたdatabase.ymlの設定を以下のように変更します。

config/database.yml

default: &default
  adapter: postgresql
  encoding: utf8
  host: db
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: my_app
  password: password

development:
  <<: *default
  database: my_app_development

test:
  <<: *default
  database: my_app_test

rails db:create

データベースを構築します。

docker-compose run web rake db:create db:migrate

アプリ起動

docker-compose up

upコマンドでアプリケーションを起動します。

docker-compose up

起動確認

localhost:3000にアクセスして以下の画面が表示されましたら完了です。お疲れ様でした!

起動確認

ハマったときに深掘りすることの重要性

開発をしていると「ハマる」ということはつきものです。

エンジニアがよく使う「ハマる」とはぴったりの穴に嵌ってしまって長時間抜け出せない状態から由来していると言われています。

ハマりは予期せず起きてしまうのです。なるべくならハマりたくないと考えるのが自然だと思います。 しかし私はハマったときこそエンジニアの成長のチャンスだと考えています。

ハマったときの正しいアプローチ方法

ハマったときに、まず第一に考えることは「ハマった状態をどうやって抜け出すか」ということだと思います。

この状態をどうすれば抜け出せるのか?つまりHowに着目してその方法を探すことが第一歩です。

ググったり、あれこれ試行錯誤しながら、何とか方法(How)を見つけ出し、ハマりを抜け出したとします。 ハマったことを振り返りもせず、何事もなかったかのようにそのまま突き進んでしまうと、ハマったことはただ単に時間を浪費したことに過ぎません。

これではせっかくの成長のチャンスを台無しにしています。

ハマったときにHowに対するWhyをきちんと問うことで成長できると私は考えます。

「なぜこの方法を取ることで解決できたのか?」その根本的な理由を明確にすることで、問題の本質が見えてきます。 本質が見えると似たようなハマり事象が発生したときに、その知見を応用して別の解決策を自分から提示することができるようになります。

この作業を「深堀り」と呼びます。

もし深堀りしていなかったら、類似の事象が発生したときに過去に解決した事象との結びつきが見つけられず、再びHowを一から探すという浪費を繰り返してしまいます。

深堀りすることの重要性を示す例として、Rails+Postgresql+Dockerで発生する「There is an issue connecting to your database with your username/password, username xxx」でハマった件という私の過去の記事があります。

この記事で上げられたの解決方法としては「dokerのvolumeを削除する」ことです。 これは試行錯誤する中で適当に実施したことで生まれた解決方法です。

「volumeを削除したらよくわからないけど動いた!先に進もう」となってしまうと、再度類似のことが発生した時も同じく試行錯誤しながら時間をかけて再び「volumeを削除してみよう」となるのかと思います。これでは過去の経験は何も活かされずにただ時間を浪費していることになります。

「なぜvolumeを削除することでエラーが解決されたのか?」のWhyを深掘りすることで問題の本質である「database.ymlのusernameをdocker-composeの初期起動時から変更してしまった」ことに気付くことができ、類似のことが発生したときに応用できるようになるのです。

アウトプットすることでより自分を成長させる

HowやWhyについて深堀りをする作業は、メモ帳などにアウトプットとすることでより再現性が高まり、クオリティが向上します。

人間のメモリはとても少ないため、時間が立つとすぐに物事を忘れてしまいます。そのため、メモ帳というストレージに保存して、知識のデータベースからすぐに引っ張り出せるようにしておくことがとても重要なのです。

特におすすめなのが、メモ帳をブログに公開するということです。

ブログに公開するということは第三者の目がありますので、プライベートなメモ帳よりもクオリティを上げなければならないというプレッシャーが発生します。このプレッシャーによりWhyの深堀りをより質の高いものにするという意識が自然に付くようになります。

ブログと言っても、あくまでも自分へのメモです。振り返ったときにすぐ知識にアクセスできるというデータベースを構築するのは後々とても大きな財産となります。 またブログとして公開することで副次的にセルフブランディングとなる将来的な資産にもなりえますので、一石二鳥なとても有効なアウトプット方法と言えるでしょう。

もしも同じようなブログがインターネット上にすでに存在したとしても問題有りません。

今日時点で執筆したのであれば最新状態の記事ということになりますでの、既存記事よりも価値が高いと捉えることも出来ますので、何も躊躇することはないのです。どんどん深堀り結果をアウトプットしましょう。

外注向けに特定ディレクトリのみ参照できるFTPユーザーを作成する方法 | ConoHa VPS+CentOS+vsftpd

外注先や他社とFTPを介してファイルを送受信する場合に、FTP専用のユーザを作成して共有する事があるかと思います。 その際、FTP専用ユーザーがサーバー内の全てのディレクトリにアクセスできるのはセキリティ上、好ましく有りません。

本記事では、「特定のディレクトリ配下のみアクセスできる」かつ「シェルにはログインさせない」FTP専用ユーザーの作成方法を解説します。

FTPサーバーの構築から説明していますので、すでに構築済みの方は「FTPユーザの作成」から読み進めていただければと思います。

環境情報

  • サーバー:ConoHa VPS
  • OS: CentOS 9

FTPサーバーの設定

ConoHa VPSのCentOSサーバーにはFTP(vsftpd)はインストールされていませんので、インストールや諸々の設定を行います。

FTPサーバーのインストールと起動サービスの登録

# rootでログイン
sudo yum install -y vsftpd
sudo systemctl enable vsftpd
sudo systemctl start vsftpd
sudo systemctl status vsftpd

# FTPユーザでアクセス可能とさせるディレクトリを作成
mkdir -m 777 /var/www/ftp_dir

# vsftpdの設定のために作成
touch /etc/vsftpd/chroot_list
mkdir /etc/vsftpd/user_conf

vsftpd.confの設定

  • vsftpd.confは以下の通り設定しております。
vi /etc/vsftpd/vsftpd.conf

vsftpd.confの設定内容

# 匿名ユーザのログインは許可しない
anonymous_enable=NO
# ローカルユーザによるログインを許可
local_enable=YES
# ファイルに変更を加える FTP コマンドの使用を許可
write_enable=YES
# ディレクトリ=755、ファイル=644
local_umask=022
# ログを取得する
xferlog_enable=YES
# ログの出力先
xferlog_file=/var/log/xferlog.log
connect_from_port_20=YES
# ユーザのchroot(ユーザーごとのルートディレクトリ)を有効にする
chroot_local_user=YES
# chroot_listの設定
chroot_list_file=/etc/vsftpd/chroot_list
# chroot_listにリストアップしたユーザーはchrootの対象から除外される
chroot_list_enable=YES
# IPv4で動作させる
listen=YES
# IPv6で待機しない
listen_ipv6=NO
# パッシブモードを有効
pasv_enable=YES
# IPアドレス
pasv_address=【FTPサーバーのIPアドレス】
# PASVで使用するポートの最小番号
pasv_min_port=60001
# PASVで使用するポートの最大番号
pasv_max_port=60010
# ls時にドットファイルを含む
force_dot_files=YES
# ファイルのタイムスタンプはユーザのローカルタイムを使用する
use_localtime=YES
# ユーザ毎の設定ファイルを置くディレクトリ
user_config_dir=/etc/vsftpd/user_conf
# 書き込み権限があるとchroot出来ない機能を無効にする
allow_writeable_chroot=YES

ファイアウォールの設定

  • 初期状態ではFTPはファイアウォールでブロックされてしまいますので、設定を変更します。
# FTPを開放
firewall-cmd --add-service=ftp --permanent

# Passive用ポートの開放
firewall-cmd --zone=public --add-port=60001-60010/tcp --permanent

# ファイアウォールの再起動
firewall-cmd --reload

サーバーの設定

  • 後述の-s /sbin/nologin指定によるユーザ作成をすると、CentOS 7以上だとFTP接続時に「530 Login incorrect.」で失敗してしまいます。 そのため以下の設定変更をする必要があります。
# rootでログイン
vi /etc/shells

# 下記を追記して保存します
/sbin/nologin

こうすることで、nologin を指定したユーザーでもFTP接続できるようになります。

FTPユーザの作成

① FTPユーザを作成する
# rootでログイン
useradd -s /sbin/nologin ftp-user
  • -s /sbin/nologin ログインシェルを実行させないための指定です。これによりsshによるログインが不可となります。
② パスワードの設定
passwd ftp-user
  • 任意のパスワードを設定します。
③ ftp-userのuser_confを配置
# rootでログイン
vi /etc/vsftpd/user_conf/ftp-user

# 下記を追記して保存
local_root=/var/www/ftp_dir
  • こうすることで、local_rootに指定したディレクトリがftp-userのルートディレクトリになります。

続いてルートディレクトリを作成しておきます

# rootでログイン
mkdir -m 777 /var/www/ftp_dir

接続確認

以上でFTPユーザの作成が完了しました。最後に接続確認を行います。

FTPで接続確認

ftp ftp-user@XXX.XXX.XXX.XXX
230 Login successful.
ftp>
  • 無事接続が成功し、ftpコマンドが使用可能となります。

SSHで接続確認

ssh ftp-user@XXX.XXX.XXX.XXX

This account is currently not available.
  • nologinの設定により、ログインシェルは無効となります。

Next.jsのfetch APIでqueryパラメータを使用する方法

Next.jsのgetServerSideProps等でfetch APIを使用してSSRする場合に、queryパラメータを扱う方法を説明します。

実装

query-stringというNPMライブラリを使用します。 query-stringを使用することで、安全にqueryパラメータを生成することができます。

import queryString from 'query-string';

export async function getServerSideProps(context) {
  // contextからqueryを取得します
  const { query } = context;

  // queryが空でない場合は、queryString.stringifyによりquery stringに変換します
  const qs = Object.keys(query).length === 0 ? '' : '?' + queryString.stringify(query);

  // fetch apiにquery stringを付与して結果を取得します
  const response = await fetch(`https://...com/products${qs}`
  const products = await response.json();
  return { props: {products} };
}

query-stringはstringifyメソッド以外にも、query stringをオブジェクト型に変換する処理や、Array型を任意の配列のフォーマットに合わせたquery stringを生成する処理などもありますので、とても便利なライブラリです。

FTPでCSVファイルをダウンロードしてアプリに取り込む処理の実装例 | Rails

FTPサーバーに配置されたCSVファイルを使ってRailsアプリで処理する機会がありましたので、実装例について説明いたします。

net/ftpを使用した実装例

RubyでFTPサーバー接続するためにはnet/ftpというライブラリを使用します。

ftp = Net::FTP.new
ftp.connect('接続先ホスト名')
ftp.login('FTP接続ユーザ', 'パスワード')
ftp.get('share/products.csv','tmp/products.csv')
ftp.close
  • ftp.connectでは、接続先のホスト名を指定します
  • ftp.loginでは、FTPユーザ名およびパスワードを指定します
  • ftp.getでは、ダウンロードしたいファイルのパスおよび、それを保存するローカルのファイルパスを指定します
  • ftp.closeで接続を終了します

CSV取り込み処理の実装例

ダウンロードしたCSVファイルを使用して、取り込み処理を書いていきます。

File.open('tmp/products.csv', 'r', encoding: Encoding::WINDOWS_31J) do |file|
  csv = CSV.new(file, headers: false, col_sep: ',')
  csv.each do |row|
    # rowに各レコードが配列で格納されているのでparse処理を書いていきます
    # 例)
    code = row[0]
    name = row[1]
    price = row[2]
    Product.create(code: code, name: name, price: price)
  end
end
# 取り込み後にローカルファイルを削除
File.delete('tmp/products.csv')

【超簡単】Next.jsのページ遷移時にLoadingを表示させる方法

Next.jsの初回アクセス後にnext/router等で画面遷移すると、getServerSidePropsの処理中が挟むことによって画面遷移の待ちが発生してしまいます。

フリーズしているような見た目になってしまうので、見た目としてあまり好ましくありません。 待機中にloadingしているように見せるライブラリがありましたので、今回はそちらを紹介します。

NProgressを使用したローディングの実装例

NProgressというライブラリを使用することでNext.jsの画面に簡単に以下のようなローディングバーを表示することができます。

Nprogressのローディングイメージ

インストール方法

yarn add nprogress

Typescriptの場合は以下もインストールします

yarn add -D @types/nprogress

実装例

インストールが完了しましたら、_app.tsに以下のコードを入れるだけでローディングバーを表示させることができます。

ステータス管理などを自前で用意する必要がありませんので導入がとてもラクです。簡易的なローディングであればこれで十分だと思います。

import Router from 'next/router';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';

Router.events.on('routeChangeStart', () => NProgress.start());
Router.events.on('routeChangeComplete', () => NProgress.done());
Router.events.on('routeChangeError', () => NProgress.done());

function MyApp() {
// ...以下省略

devise-token-authでemailではなくusernameでもログインさせる方法

email以外のキーを使った認証方法について、devise単体では同じような記事がいくつもありましたが、devise-token-authを交えた記事があまりなかったので作成しました。

How To: Allow users to sign in using their username or email address · heartcombo/devise Wiki · GitHub

上記の記事等を参考にしましたが、devise-token-auth側でdevise本体の挙動が色々とオーバーライドされており、そのままの設定では動作しない部分がありました。

右往左往しましたが、結果的には思っていたよりも少ない修正で実現できましたので、同様のことでお悩みの方の参考になれば幸いです。

usersテーブルにusernameを追加する

以下のようにユニーク制約を付与してstring型のusernameをusersテーブルに追加します。

class AddUsernameToUsers < ActiveRecord::Migration[7.0]
  def change
    add_column :users, :username, :string
    add_index :users, :username, unique: true
  end
end

Userモデルへの設定

authentication_keysの設定

deviseメソッドのauthentication_keysにemailとusernameを設定します。

  devise :database_authenticatable, :registerable, :rememberable, :trackable, :validatable,
         authentication_keys: %i[email username] # ←この部分を設定します

こうすることで、emailまたはusernameが認証キーとして設定されるようになります ログイン時はどちらか一方のキーで認証することが可能となります。

バリデーションの設定

このままでは、usernameに対してバリデーションが全く効いていない状態なので、以下のようにバリデーションを設定します。

validates :username, presence: true, uniqueness: true

emailに対しては:validatableが有効であれば自動でバリデーションチェックが走ります。

任意で細かなチューニング

基本的には上記の設定変更のみでemailまたはusernameでログインすることが可能となります。

ただし現状のままでは不完全な部分があります。

例えばemailとusernameに同じ値が設定できてしまうので考慮が必要です。 また、ユーザー情報変更(account_update)時にusernameが変更できませんので、そこの考慮もする必要があるかと思います。

必要に応じて以下の記事を参考にチューニングを行ってください。

github.com