Rails + OpenSearch の環境をDockerで作成する
はじめに
この記事では、RailsとOpenSearchをDockerを使用してセットアップする方法を解説します。
OpenSearchをdocker-composeで作成した際に、うまく繋がらなくて少し詰まったので、同じ詰まり方をしている人の参考になれば幸いです。
この記事の目的とゴール
ローカルでRailsとOpenSearchの動作確認ができる状態にすることを目標にします。
それ以外のことはやらないので、すでにできている人には旨味のない記事になると思います。
Dockerツールなどのインストール方法は省略します。
Railsはバージョン8のAPIモード、DBはPostgreSQLで作成します。(DBは使用しません)
Railsアプリケーションのセットアップ
まずはDockerでRailsアプリを作成するところまでを行います。
すでに作成済みの方は、この章を無視して次に進んでください。
初期ファイルの作成
まずはRailsが動く最小のdocker-compose.ymlを作成します。
今回はPostgreSQLをDBとして選択していますが、本記事とはあんまり関係ないのでなんでも良いと思います。
下記を作成していきます。
- Dockerfile
- docker-compose.yml
- Gemfile
- Gemfile.lock
Dockerfile
FROM ruby:3.3-slim
RUN apt-get update -qq && \
apt-get install -y \
build-essential \
libpq-dev \
git \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY Gemfile Gemfile.lock ./
RUN bundle install
docker-compose.yml
services:
app:
build: .
depends_on:
- db
volumes:
- .:/app
- app_bundle:/usr/local/bundle
environment:
- DATABASE_URL=postgres://user_name:password@db:5432
ports:
- "3000:3000"
command: tail -f /dev/null
db:
image: postgres:16.4
volumes:
- db_data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=user_name
- POSTGRES_PASSWORD=password
volumes:
db_data:
app_bundle:
tail -f /dev/null について
このコマンドは、コンテナを常時起動させておく魔法のコマンドです。
perprexityProくんの解説
このコマンドには2つの重要な役割があります:
コンテナの永続実行
tail -f /dev/nullは、コンテナを継続的に実行させ続けるためのテクニックです。
tail -fは指定されたファイルを継続的に監視するコマンド
/dev/nullは特殊なデバイスファイルで、データを送り込むと消失する「ブラックホール」として機能
実行の仕組み
このコマンドがコンテナを実行し続ける理由は以下の通りです:
/dev/nullは常に空のファイルとして存在し続ける
tail -fコマンドはファイルの終わりに到達しても終了せず、新しい出力を待ち続ける
これによりコンテナのメインプロセスが終了せず、コンテナが停止しない
このテクニックは、特にRailsのような開発環境で、コンテナを常時起動させておく必要がある場合によく使用されます。
2つ目の重要な役割を教えてほしいですが、めちゃくちゃわかりやすいですね。。
簡単にいうと、終わらないコマンドを使用して、コンテナを停止させないって感じです。
perprexityProくんの参考リンク(多いので上位5件のみ)
Gemfile
source "https://rubygems.org"
gem "rails", "~> 8.0.0"
Gemfile.lock
空でOKです。
docker composeコマンドを使用する
docker compose コマンドを使用してコンテナを立ち上げて、コンテナ内に侵入するところまでを行います。
docker compose build
docker compose up -d
docker compose exec app bash
rails new の実行
rubyのコンテナに入ったら、 rails new を実行して新しいRailsアプリケーションを作成します。
冒頭でも書いた通り、APIモードで作成しています。
この設定はこの記事にあまり影響しないので、お好きな設定で作成していただいて大丈夫だと思います。
rails new . --api --minimal --database=postgresql --skip-test
conflict README.md
Overwrite /app/README.md? (enter "h" for help) [Ynaqdhm] Y
...
conflict Gemfile
Overwrite /app/Gemfile? (enter "h" for help) [Ynaqdhm] Y
...
conflict Dockerfile
Overwrite /app/Dockerfile? (enter "h" for help) [Ynaqdhm] n
これで、Docker環境でRailsが動作するところまで実装できましたね。
試しに、コンテナの中で立ち上げたRailsにlocalhostから接続してみましょう。
# ActiveRecord::NoDatabaseErrorが発生するのを避ける
rails db:create
# rails起動
rails s -b 0.0.0.0
# control + c で停止
上記コマンドを実行後に http://localhost:3000 にアクセスすると下記の画面になると思います。

必要なgemの追加
OpenSearchに接続するためのGemと、ダミーデータをそれっぽく作成するためのGemを追加します。
opensearch-aws-sigv4
AWS公式ドキュメントが取り上げているgemになります。
とりあえずこれ使っておけば良いと思います。
AWSの公式ドキュメントで紹介されている箇所
gemリンク
Faker
ダミーデータをいい感じに作成してくれるgemになります。
Faker::Config.locale = :ja
上記を設定することで、日本語のいい感じの文章や名称を生成してくれます。
テストやダミーデータ投入に大変便利です。
例
app(dev)> Faker::Lorem.sentence
=> "きょうかいよわよわしいしえんするちあん。"
app(dev)> Faker::Lorem.sentence
=> "ぶきほうしゅう傑作おりめ。"
app(dev)> Faker::Book.genre
=> "料理・グルメ"
app(dev)> Faker::Book.author
=> "小島 茜"
app(dev)> Faker::Book.title
=> "『春と修羅』"
いい感じですね。
gemリンク
モデルの作成
OpenSearchに接続して動作確認するためのモデルを先に作成してしまいます。
今回はRDSを使わずに、ダミーの値を生成してOpenSearchに投入します。
# app/models/opensearch_service/base.rb
module OpensearchService
class Base
class << self
def client
@client ||= OpenSearch::Client.new(host: "http://host.docker.internal:9200")
end
# index(RDSで言うテーブル)の作成を行う
def create
body = {
mappings: {
dynamic: false, # Disable dynamic mapping
properties: mapping_properties
}
}
client.indices.create(index: index_name, body: body)
end
# indexの削除を行う
def delete
client.indices.delete(index: index_name)
end
# indexのdocument(RDSで言うレコード)を作成する
def create_document(obj:)
client.create(index: index_name, id: obj[:id], body: obj[:body])
end
# indexのdocumentを削除する
def delete_document(obj:)
client.delete(index: index_name, id: obj[:id])
end
end
end
end
# app/models/opensearch_service/indices/book_index.rb
module OpensearchService
module Indices
class BookIndex < OpensearchService::Base
class << self
def index_name
"books"
end
def mapping_properties
# 本来は色々analyzerなどを設定するが、今回は省略
{
name: { type: "text" },
author: { type: "text" },
genres: { type: "keyword" },
created_at: { type: "date" }
}
end
# 本来はDBから登録するが、今回はFakerを使ってランダムなデータを作成
def generate_obj
Faker::Config.locale = :ja
{
id: Random.rand(1000),
body: {
name: Faker::Book.title,
author: Faker::Book.author,
genres: [ Faker::Book.genre, Faker::Book.genre ],
created_at: Time.zone.now
}
}
end
end
end
end
end
これで完了です。下記のような形で操作できるコードになります。
(次の章で)OpensearchのDocker環境を整えたら、実際に実行できると思います。
# OpenSearchにindex作成
OpensearchService::Indices::BookIndex.create
# 登録用のデータ生成
obj = OpensearchService::Indices::BookIndex.generate_obj
# OpenSearchにdocument作成
OpensearchService::Indices::BookIndex.create_document(obj:)
# OpenSearchからdocument削除
OpensearchService::Indices::BookIndex.delete_document(obj:)
# OpenSearchからindex削除
OpensearchService::Indices::BookIndex.delete
OpenSearchとOpenSearchDashboardの追加
作成したDockerでRailsが動作する環境に、OpenSearchを加えていきます。
この章ではOpenSearchとOpenSearchDashboard(以降dashboard)の追加を行います。
dashboardは、OpenSearchのデータ内容を簡単に確認できるようなものになります。
完成系のdocker-compose.ymlは下記になります。
x-opensearch-settings: &x-opensearch-settings
image: opensearchproject/opensearch:2.17.1 # AWSの対応バージョンを指定
ulimits:
memlock:
soft: -1 # Set memlock to unlimited (no soft or hard limit)
hard: -1
nofile:
soft: 65536 # Maximum number of open files for the opensearch user - set to at least 65536
hard: 65536
x-opensearch-env: &x-opensearch-env
cluster.name: opensearch-cluster # Name the cluster
discovery.seed_hosts: opensearch-node1,opensearch-node2 # Nodes to look for when discovering the cluster
cluster.initial_cluster_manager_nodes: opensearch-node1,opensearch-node2 # Nodes eligible to serve as cluster manager
bootstrap.memory_lock: true # Disable JVM heap memory swapping
OPENSEARCH_JAVA_OPTS: "-Xms512m -Xmx512m" # Set min and max JVM heap sizes to at least 50% of system RAM
OPENSEARCH_INITIAL_ADMIN_PASSWORD: dummyPassword123! # 大文字小文字数字特殊文字を含む8文字以上のパスワードを指定
plugins.security.disabled: true # 認証を省略
services:
# railsとpostgresqlのcontainerは省略
opensearch-node1: # This is also the hostname of the container within the Docker network (i.e. https://opensearch-node1/)
<<: *x-opensearch-settings
environment:
<<: *x-opensearch-env
node.name: opensearch-node1 # Name the node that will run in this container
volumes:
- opensearch-data1:/usr/share/opensearch/data # Creates volume called opensearch-data1 and mounts it to the container
ports:
- 9200:9200 # REST API
- 9600:9600 # Performance Analyzer
networks:
- opensearch-net # All of the containers will join the same Docker bridge network
opensearch-node2:
<<: *x-opensearch-settings
environment:
<<: *x-opensearch-env
node.name: opensearch-node2
volumes:
- opensearch-data2:/usr/share/opensearch/data
networks:
- opensearch-net
opensearch-dashboards:
image: opensearchproject/opensearch-dashboards:2.17.1 # OpenSearchのバージョンに合わせる
ports:
- 5601:5601 # Map host port 5601 to container port 5601
expose:
- "5601" # Expose port 5601 for web access to OpenSearch Dashboards
environment:
OPENSEARCH_HOSTS: '["http://opensearch-node1:9200","http://opensearch-node2:9200"]' # Define the OpenSearch nodes that OpenSearch Dashboards will query
DISABLE_SECURITY_DASHBOARDS_PLUGIN: true # 認証を省略
networks:
- opensearch-net
volumes:
db_data:
app_bundle:
opensearch-data1:
opensearch-data2:
networks:
opensearch-net:
以降は変更のメモや解説になるので、読み飛ばしてもOKです。
完成系に至るまでの道のり
OpenSearchとdashboardのサンプルを公式から取得
上記にあるOpenSearchを docker compose で動作させるためのファイルをとりあえずコピペします。
# ...railsの設定...
# https://opensearch.org/docs/2.18/install-and-configure/install-opensearch/docker/#sample-docker-composeyml
opensearch-node1: # This is also the hostname of the container within the Docker network (i.e. https://opensearch-node1/)
image: opensearchproject/opensearch:latest # Specifying the latest available image - modify if you want a specific version
container_name: opensearch-node1
environment:
- cluster.name=opensearch-cluster # Name the cluster
- node.name=opensearch-node1 # Name the node that will run in this container
- discovery.seed_hosts=opensearch-node1,opensearch-node2 # Nodes to look for when discovering the cluster
- cluster.initial_cluster_manager_nodes=opensearch-node1,opensearch-node2 # Nodes eligible to serve as cluster manager
- bootstrap.memory_lock=true # Disable JVM heap memory swapping
- "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m" # Set min and max JVM heap sizes to at least 50% of system RAM
- OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_INITIAL_ADMIN_PASSWORD} # Sets the demo admin user password when using demo configuration, required for OpenSearch 2.12 and later
ulimits:
memlock:
soft: -1 # Set memlock to unlimited (no soft or hard limit)
hard: -1
nofile:
soft: 65536 # Maximum number of open files for the opensearch user - set to at least 65536
hard: 65536
volumes:
- opensearch-data1:/usr/share/opensearch/data # Creates volume called opensearch-data1 and mounts it to the container
ports:
- 9200:9200 # REST API
- 9600:9600 # Performance Analyzer
networks:
- opensearch-net # All of the containers will join the same Docker bridge network
opensearch-node2:
image: opensearchproject/opensearch:latest # This should be the same image used for opensearch-node1 to avoid issues
container_name: opensearch-node2
environment:
- cluster.name=opensearch-cluster
- node.name=opensearch-node2
- discovery.seed_hosts=opensearch-node1,opensearch-node2
- cluster.initial_cluster_manager_nodes=opensearch-node1,opensearch-node2
- bootstrap.memory_lock=true
- "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m"
- OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_INITIAL_ADMIN_PASSWORD}
ulimits:
memlock:
soft: -1
hard: -1
nofile:
soft: 65536
hard: 65536
volumes:
- opensearch-data2:/usr/share/opensearch/data
networks:
- opensearch-net
opensearch-dashboards:
image: opensearchproject/opensearch-dashboards:latest # Make sure the version of opensearch-dashboards matches the version of opensearch installed on other nodes
container_name: opensearch-dashboards
ports:
- 5601:5601 # Map host port 5601 to container port 5601
expose:
- "5601" # Expose port 5601 for web access to OpenSearch Dashboards
environment:
OPENSEARCH_HOSTS: '["https://opensearch-node1:9200","https://opensearch-node2:9200"]' # Define the OpenSearch nodes that OpenSearch Dashboards will query
networks:
- opensearch-net
volumes:
db_data:
app_bundle:
opensearch-data1:
opensearch-data2:
networks:
opensearch-net:
これでもOpenSearchとdashboardの間の動作確認はできると思います。
デフォルトのnameにする
container_nameを明示的に設定している様子です。
複数環境コピペしてローカルで動かすと、命名が競合してしまう可能性があるので削除します。
OpenSearchのパスワードを適当に設定する
dummy などの簡単なパスワードにしてしまうと、下記のようなエラーが発生してしまいます。
# コンテナ名を確認
docker compose ps -a
# 最新ログを5件確認
docker logs -f --tail=5 xxx-opensearch-node1-1
OpenSearch plugins dir: /usr/share/opensearch/plugins/
OpenSearch lib dir: /usr/share/opensearch/lib/
Detected OpenSearch Version: 2.17.1
Detected OpenSearch Security Version: 2.17.1.0
Password dummy failed validation: "Weak password". Please re-try with a minimum 8 character password and must contain at least one uppercase letter, one lowercase letter, one digit, and one special character that is strong. Password strength can be tested here: https://lowe.github.io/tryzxcvbn
これはパスワードが弱すぎるため発生するエラーになります。
セキュリティの設定をオフにしても発生するので、怒られないような適当な文字列を配置しておく必要があります。
docker logsはコンテナのログを確認するコマンドになります。
今回はとりあえず、dummyPassword123!で設定します。
認証・セキュリティ設定をオフにする
次に認証・セキュリティの設定をオフにします。
上記のままで http://localhost:5601 にアクセスすると username/passwordを求められます。
自分しかアクセスしないdashboardにこの設定がかかっていると面倒なので、設定で誰でもアクセスできるようにします。
OpenSearchのenvironmentにplugins.security.disabled: trueを追加します。
この設定をしない場合は、httpsのままなのでそのままでOKです。
重複設定をまとめる
node1とnode2の重複が多いのでymlの記法を活用してまとめます。
下記の記事が参考になると思います。
environmentについてですが、最初の記法のままだとアンカー記法で書いたものをマージする時にエラーになってしまいます。
なので、下記のような形式に書き換える必要があります。
# - node.name=opensearch-node1 → エラーになってしまう。
node.name: opensearch-node1 # エラーにならない
以上で完成になります。
完成系は最初に示した通りこちらです。
http://localhost:5601 にシークレットウィンドウからアクセスできたら完璧です。
RailsとOpenSearchの接続・動作確認
モデルの作成の章で作成したコードを使用して、実際にデータを投入して接続確認をしていきます。
その後にdashboardを使用して、OpenSearchがうまく動作するかも確認します。
OpenSearchにデータを投入する
コンテナに侵入してrails consoleからデータを投入していきます。
docker compose exec app bash
rails c
OpensearchService::Indices::BookIndex.create
=> {"acknowledged"=>true, "shards_acknowledged"=>true, "index"=>"books"}
OpensearchService::Indices::BookIndex.create_document(obj: OpensearchService::Indices::BookIndex.generate_obj)
=>
{"_index"=>"books",
"_id"=>"951",
"_version"=>1,
"result"=>"created",
"_shards"=>{"total"=>2, "successful"=>2, "failed"=>0},
"_seq_no"=>0,
"_primary_term"=>1}
OpensearchService::Indices::BookIndex.create_document(obj: OpensearchService::Indices::BookIndex.generate_obj)
=>
{"_index"=>"books",
"_id"=>"937",
"_version"=>1,
"result"=>"created",
"_shards"=>{"total"=>2, "successful"=>2, "failed"=>0},
"_seq_no"=>1,
"_primary_term"=>1}
OpensearchService::Indices::BookIndex.create_document(obj: OpensearchService::Indices::BookIndex.generate_obj)
=>
{"_index"=>"books",
"_id"=>"618",
"_version"=>1,
"result"=>"created",
"_shards"=>{"total"=>2, "successful"=>2, "failed"=>0},
"_seq_no"=>2,
"_primary_term"=>1}
dashboardで確認
登録したデータ全件を確認する
まずは登録したデータをdashboardから確認してみます。
以降はdashboardの機能で登録されたdocumentを確認する方法になります。
クエリを実行しても全件確認は可能なので、この章はスキップしても大丈夫です。
まずは、 http://localhost:5601 にアクセスします。

Explore on my ownを選択します。

左上のハンバーガーメニューを選択し、DashboardsManagement を選択します。
Index patterns を選択し、Create index pattern を選択します。
index pattern 名を決めろと言ってくるので、今回は適当に books を入力します。
次に進むと、いろいろなことに使用する時間フィールドを決めろと言ってきます。
今回は一覧を見られればいいので、 I don't want to use the time filter を選択します。
右下の Create index pattern をクリックして作成を終了します。
左上のハンバーガーメニューを選択し、Discover を選択します。
そうすると先ほど投入したデータが確認できると思います。

うまくデータ投入できていますね。🎉
クエリを試してみる
まずは、 http://localhost:5601 にアクセスします。

左上のハンバーガーメニューを選択し、Dev Toolsを選択します。
左に下記のクエリを貼り付けて実行ボタン(GETの右に出る三角)を押すと、右に結果が出力されます。
GET /books/_search
{
"query": {
"range": {
"created_at": {
"gte": "2023",
"lte": "2024-12-31"
}
}
}
}

試しにauthorで検索してみましょう。
GET /books/_search
{
"query": {
"match": {
"author": "内田"
}
}
}

うまく検索できました 🎉
まとめ
以上で手元にRails+OpenSearchの動作する環境ができたと思います!
紹介した上記のサンプルコードは公開リポジトリにあげておきます。
OpenSearchの案件が来た時の、色々忘却した未来の自分への良い資料ができたと思います。
僕の記憶は80分しかもたない。
ついでに、この記事が誰かのお役に立てれば幸いです。👋
参考資料URL一覧
- https://www.networkworld.com/article/947278/sending-data-into-the-void-with-dev-null.html
- https://qiita.com/tag1216/items/7ce35b7c27d371165e56
- https://stackoverflow.com/questions/49268430/what-does-dev-null-means-in-docker-compose-yaml-while-mounting-a-volume
- https://qiita.com/reflet/items/dd65f9ffef2a2fcfcf30
- https://askubuntu.com/questions/306047/what-does-the-script-dev-null-do
- https://docs.aws.amazon.com/ja_jp/opensearch-service/latest/developerguide/serverless-clients.html#serverless-ruby
- https://github.com/opensearch-project/opensearch-ruby-aws-sigv4
- https://github.com/faker-ruby/faker
- https://opensearch.org/docs/2.18/install-and-configure/install-opensearch/docker/#sample-docker-composeyml
- https://docs.docker.jp/engine/reference/commandline/logs.html
- https://opensearch.org/docs/latest/security/configuration/disable-enable-security/
- https://tech-blog.cloud-config.jp/2024-02-26-yaml-anchor-mergekey
Discussion