7
6

Sinatra + Dockerで簡単なToDoアプリ

Last updated at Posted at 2023-10-31

はじめに

今回、Dockerを勉強するついでに同じRubyのWEBアプリケーションフレームワークであるSinatraを触ってみたいと思い、Docker+Sinatraで簡単なToDoアプリの開発を行いました。

開発環境での動作しか確認していませんが、開発にあたって学んだこと、失敗したことなどを備忘録的に書き記そうと思います。

開発環境

  • M2 MacbookAir
  • ターミナル:Alacritty
  • エディタ:Vim
  • Docker:23.0.5, build bc4487a
.ruby-version
3.1.4
Gemfile.lock
GEM
  remote: https://rubygems.org/
  specs:
    mustermann (3.0.0)
      ruby2_keywords (~> 0.0.1)
    nio4r (2.5.9)
    pg (1.5.4)
    puma (6.4.0)
      nio4r (~> 2.0)
    rack (2.2.8)
    rack-protection (3.1.0)
      rack (~> 2.2, >= 2.2.4)
    ruby2_keywords (0.0.5)
    sinatra (3.1.0)
      mustermann (~> 3.0)
      rack (~> 2.2, >= 2.2.4)
      rack-protection (= 3.1.0)
      tilt (~> 2.0)
    tilt (2.3.0)

PLATFORMS
  aarch64-linux

DEPENDENCIES
  pg
  puma
  sinatra

BUNDLED WITH
   2.4.21

導入

環境構築

  1. 作業用ディレクトリの作成

    $ mkdir sinatra_test && cd $_
    $ mkdir app
    

    $_は直前の引数を取得するコマンド。今回でいうsinatra_test

  2. Gitの監視下に置き、.gitignoreを作成する

    $ git init
    $ curl https://raw.githubusercontent.com/github/gitignore/main/Ruby.gitignore -o .gitignore
    

    curlコマンドで行っている処理の解説

    curlコマンドの-oオプションはcurlで指定したURLの実行結果をファイルに出力することができる。つまり、今回のコマンドではgithub上のファイルをコピーして.gitignoreに出力する。

    .gitignoreはテンプレートが用意されており、A collection of .gitignore templatesから対象の言語やフレームワークに合わせてテンプレートをコピーすることができる。

    今回はRuby.gitignoreを選択

    スクリーンショット 2023-10-26 22.44.59.png

    コード右上のRawをクリックし、URLをコピーしてcurlコマンドを実行する

    スクリーンショット 2023-10-26 22.50.16.png

  3. Dockerfileとcompose.ymlを作成する

    $ touch Dockerfile compose.yml
    

  4. Gemfileを生成する

    $ gem install bundler
    
    # 私のフォルダ構成に合わせるためにapp/に移動していますが、移動する必要はありません。
    # ただし移動しない場合Dockerfileの記述が変わります。
    $ cd app
    
    $ bundle init
    

Dockerの設定

Dockerコンテナ(作業環境)を作成する為の指示を記載したDockerイメージの設計図。
docker buildコマンドでDockerfileを元にDockerイメージを作成できる。

Dockerfile
    # syntax=docker/dockerfile:1
    
    FROM ruby:3.1.4
    
    RUN apt-get update -qq && \
        apt-get install -y build-essential libpq-dev && \
        apt-get clean && \
        rm -rf /var/lib/apt/lists/*
    
    WORKDIR /app
    COPY app/Gemfile* ./
    RUN gem install bundler && bundle install
    COPY app/ .
    EXPOSE 3000
    CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]

Dockerfileについて

CMDコマンド以外のコマンドはイメージ作成時(build)に実行される

# syntax=docker/dockerfile:1

Dockerのreferenceで**docker/dockerfile:1** の使用を推奨されているため記述。

FROM ruby:3.1.4

FROMコマンドはビルドに用いるベースイメージを公開リポジトリから取得するためのコマンド。今回はローカルの環境に合わせてRubyの3.1.4を取得しているが、環境に応じて複数のベースイメージを取得することもできる。

RUN apt-get update -qq && \
    apt-get install -y build-essential libpq-dev && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

apt-get update -qq

パッケージのリストをアップデートしてインストール可能な最新パッケージのリストを取得。-qqオプションをつけることでエラーメッセージ以外のメッセージを抑制できる。

apt-get install

先ほど取得した最新パッケージリストに従ってインストールを行なっており、-yオプションで全ての問いに自動でyを入力する様にしている。

build-essential

開発に必須のビルドツールを提供しているパッケージで、Ruby自体のコンパイルや、CやC++で書かれた部分を含むgemのコンパイルを行うことができる。

libpq-dev

PostgreSQLクライアントのヘッダやライブラリを含むパッケージで、Linuxベースの開発環境でPostgreSQLを利用する際に必要になる可能性が高いパッケージ。

apt-get clean,rm -rf /var/lib/apt/lists/*

後処理で、apt-get cleanはダウンロードしたパッケージファイルを削除しており、rm -rf /var/lib/apt/lists/*はリポジトリのパッケージリストを削除している。

後処理がない場合はビルドが高速化されるが、イメージファイルが大きくなってしまう。

WORKDIR /app
COPY app/Gemfile* ./
RUN gem install bundler && bundle install
COPY app/ .
EXPOSE 3000
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]

WORKDIR /app

コンテナ内での作業ディレクトリの設定。このコマンドは指定したディレクトリが存在しない場合、新規作成する。今回の場合、appというフォルダをルートフォルダの直下に作成している。

COPY app/Gemfile* ./

ローカルのapp直下にあるGemfileと名のつくファイル(GemfileとGemfile.lock)を作業ディレクトリの直下にコピー。

RUN gem install bundler && bundle install

コンテナ内でbundlerのインストールとgemのインストールを実行。

COPY app/ .

ローカルのappフォルダにあるファイルを全て作業ディレクトリにコピー。

EXPOSE 3000

コンテナの公開ポートを3000に設定。

  • CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]

RUNコマンドと違い、コンテナ開始時(start)あるいはコンテナ作成時(run)に実行され、Gemfile.lockに書かれたバージョンのpumaを起動する。-Cオプションで、設定ファイルにconfig/puma.rbを読み込む。

compose.yamlについて

複数のコンテナを使うDockerアプリケーションを定義・実行するためのツール。以前はPythonで開発されたものがDockerと別ツールとして用意されていたが、Goで再開発されDockerコマンドに統合された。(docker-composeからdocker composeになった)

また、以前はdocker-compose.ymlというファイル名だったが、compose.yamlが推奨になりversionの記載も非推奨になった。

compose.yaml
services:
  web:
    build: .
    ports:
      - "3000:3000"
    depends_on:
      - db
    environment:
      - DATABASE_URL=postgresql://user:password@db:5432/mydb
      - TZ=Asia/Tokyo
    volumes:
      - ./app:/app
  db:
    image: postgres:latest
    environment:
      - POSTGRES_DB=mydb
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
    volumes:
      - pg-data:/var/lib/postgresql/data

volumes:
  pg-data:
  • services:
    アプリケーションを動かすための各要素をServiceと呼ぶ。各コンテナの設定について定義。
    • web:(コンテナ名)
      アプリケーションを動かすために使っている各要素の名前を書く。dockerのログに表示されるので、app:,web:,db:など分かりやすい名前が望ましい。

      • build .
        コンテナイメージを作成するための構築情報を指定する。Dockerfileの格納場所を指す。
      • ports
        今回はホスト:コンテナの形で指定。プロトコルを指定することもできる。
      • depends_on
        サービス間の起動順と終了順の依存関係を書く。今回の例ではwebの前にdbが作成される(終了は逆順)。
      • environment
        コンテナ内の環境変数を定義する。
        DATABASE_URL:(user_name):(password)@db:(DB_port)/(DB_name)
        TZ:Asia/Tokyo
      • volumes
        サービスコンテナがアクセスできる様にする必要があるホスト上のパスか、名前付きのボリュームを定義する。
    • db:
      PostgreSQLを動かすためのコンテナ。

      • image
        コンテナの下になるベースイメージを指定する。
        今回はDockerfileのFROMと同様、公式で用意されたpostgreSQLのイメージ
    • volumes
      コンテナ間で共有されるデータの保存場所を定義する。
      今回はdb:のvolumes:で定義したpg-dataがwebとdbで共有するデータの保存場所としている。

Sinatra/Puma/PostgreSQLの導入

Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

gem "sinatra"
gem "puma"
gem "pg"

Pumaについて

Pumaはマルチスレッドで動作するコンカレントなWEBサーバを提供するgem。
Railsには標準で導入されておりrails sで動作するWEBサーバはデフォルトではPuma。

Sinatraはリクエストを1つずつしか処理できないため、DBを操作する様なレスポンスに時間がかかる処理の合間に別のリクエストが来た場合処理できないという弱点がある。

PumaはWEBサーバだが、アプリケーションサーバとしても動作することができ、nginxなど他のWEBサーバを介さなくてもHTMLリクエストを受け取りSinatraの処理をすることができる。

  • Rackのサーバ起動設定・Pumaの設定(config.ru・config/puma.rb)

    # config.ru
    require './app'
    run Sinatra::Application
    
    # config/puma.rb
    workers 3
    preload_app!
    
    port ENV.fetch("PORT") { 3000 }
    

    config.ruはPuma公式ドキュメントのFrameworksを参照
    config/puma.rbに関して、workersは複数のプロセスを1つの単位にまとめたもの。
    Pumaではthreadsでスレッド数を設定する。
    スレッドは1つにつき1つのリクエストを処理し、1つのワーカーは複数のスレッド(デフォルトでは最大で5つ)を持つことができる。
    preload_app!はfork前にアプリをロードすることで、メモリ消費量を減らせる
    portでlocal環境のport番号を3000に変更

Sinatraのコード(タスク管理アプリ)

Sinatraではルーティングテーブルに

Sinatraではルーティングテーブルにビジネスロジックが直接関連づけられる。
つまり、ルーティングテーブルとして作成するファイル(今回はapp.rb)によってControllerなどを介さずにデータの処理が行われ、Viewファイルに受け渡す。

app.rb
# frozen_string_literal: true

require 'sinatra'
require 'pg'

enable :method_override

get '/' do
  conn = db_connection
  @tasks = conn.exec('SELECT * FROM tasks')
  conn.close

  erb :"tasks/index"
end

post '/tasks/create' do
  data = JSON.parse(request.body.read)
  title = data['title']

  conn = db_connection
  conn.exec_params('INSERT INTO tasks (title) VALUES ($1)', [title])
  conn.close

  content_type :json
  { status: 'success', message: 'Task added successfully' }.to_json
end

delete '/tasks/:taskId' do
  task_id = params[:taskId]

  conn = db_connection
  conn.exec_params('DELETE FROM tasks WHERE id = ($1)', [task_id])
  conn.close

  content_type :json
  { status: 'success', message: 'Task deleted successfully' }.to_json
end

def db_connection
  PG.connect(
    dbname: 'mydb',
    user: 'user',
    password: 'password',
    host: 'db'
  )
end
views/tasks/index.erb
<!DOCTYPE html>
<html>
  <head>
    <meta charset='UTF-8'>
    <title>Sinatra_test</title>
    <script src='/javascripts/app.js'></script>
  </head>
  <body>
    <h1>タスク管理アプリ</h1>
    <input type='text' id='input-task-form'>
    <button id='add-task-button'>追加</button>
    <table>
      <thead>
        <tr>
          <th>タイトル</th>
          <th>作成日時</th>
          <th></th>
        </tr>
      </thead>
      <tbody>
        <% @tasks.each do |task| %>
          <tr>
            <td><%= task['title'] %></td>
            <td><%= Time.parse(task['created_at']).strftime('%Y-%m-%d %H:%M:%S') %></td>
            <td><button id=<%= "delete-task-button#{task['id']}" %> data-task-id=<%= task['id'] %>>削除</button></td>
          </tr>
        <% end %>
      </tbody>
    </table>
  </body>
</html>
public/javascripts/app.js
document.addEventListener('DOMContentLoaded', () => {
  const inputForm     = document.getElementById('input-task-form');
  const addButton     = document.getElementById('add-task-button');
  const deleteButtons = document.querySelectorAll('[id^="delete-task-button"]');

  addButton.addEventListener('click', async () => {
    const taskContent = inputForm.value;
    try {
      const response = await fetch('/tasks/create', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({title: taskContent})
      });
      const data = await response.json();
      location.reload();
    } catch (error) {
      console.error('Error:', error);
    }
  });

  document.body.addEventListener('click', async (event) => {
    if (event.target.matches('[id^="delete-task-button"]')) {
      const button = event.target;
      const taskId = button.getAttribute('data-task-id');
      try {
        const response = await fetch(`/tasks/${taskId}`, {
          method: 'DELETE'
        });
        const data = await response.json();
        location.reload();
      } catch (error) {
        console.error('Error:', error);
      }
    }
  });
});

困ったこと・直面したエラー

  • docker compose upした後(=Pumaサーバを起動した後)の変更が、一度buildしなおさないと反映されない
    ⇒ compose.yamlのwebにvolumesを追加していなかったため。

    volumesを追加すると、指定したローカルのフォルダとコンテナ上のフォルダがボリュームマウントされる。ボリュームマウントとは、コンテナ内の特定のパスをホストマシン(コンテナ外)のファイルシステムにマッピングする機能で、ホストマシン上でのファイルの変更がリアルタイムでコンテナ内に反映される。

  • Error: SyntaxError: Unexpected token 'a', "application/json" is not valid JSON

    app.rbで戻り値がJSON形式でなかったため

    app.jsで、await response.json()としているので、JavaScriptはJSON形式のデータが返ってくるまで待機している。一方でapp.rbに下記コードが無かったため、JSON以外のデータが返ってきているというエラーが出ていた。

    content_type :json
      { status: 'success', message: 'Task added successfully' }.to_json
    
  • アプリ上でタスクを作成したときにIDが重複しているため作成できないエラーが出た

    ダミーデータを作成する際にPostgreSQLで直接作成し、その際にIDを手打ちで作成したため

    IDはSERIALなど自動で一意な番号が振られる型にするので原則手打ちで入力しない。今回はPostgreSQLにアクセスし、ダミーデータを全て削除してからアプリ上でタスクを作成したら問題なく作成できた。

参考にしたサイト

Docker関係

基本的にはDockerドキュメントSinatra公式を参照

Sinatra関係

その他(Puma/PostgreSQL/JavaScript/汎用コマンド)

7
6
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
7
6