🦓

Docker ComposeでRails+Vue3+Typescript+PostgreSQLの開発環境を構築

2024/10/12に公開

はじめに

RailsとVue3とTypescriptとPostgreSQLの開発環境をDocker Composeで設定と実行してみました。

Ruby: 3.2.1
Rails: 7.2.1

tl:dr;

  1. ディレクトリ構造
  2. バックエンドを構築する
    ・Dockerfileを作成する
    ・docker-compose.ymlを作成する
  3. データベースの接続情報を設定する
  4. フロントエンドを構築する
    ・viteを使ってプロジェクトをセットアップ
    ・ESLintを導入する
    ・Prettierを導入する
    ・Dockerfileを作成する
    ・docker-compose.ymlを追記する
  5. docker compose build
  6. docker compoup up

ディレクトリ構造

簡単ですが、現時点ではこのような構造になります。

.
├── api/
│   ├── Dockerfile
│   ├── Gemfile
│   ├── Gemfile.lock
│   ├── entrypoint.sh
├── front/
│   └── Dockerfile
└── docker-compose.yml
└── .gitignore

バックエンドを構築する

Rails APIモードで新規アプリを作成します。

rails new . --api --force --database=postgresql --skip-test --skip-system-test

ターミナルで上記コマンド実行後には、Railsアプリに関わるファイル一式が自動生成されます。

Dockerfileを作成する

Dockerfile を以下のようにします。
Ruby、Bundler などの依存パッケージすべてをコンテナー内部に含めてビルドされます。

rails newで生成されたデフォルトのDockerfileを使うこともできます。

Dockerfile
FROM ruby:3.2.1-alpine

ENV LANG="ja_JP.UTF-8"
ENV app="/app"

WORKDIR $app

# 環境変数の設定(ここでは本番環境)
ENV RAILS_LOG_TO_STDOUT="1" \
    RAILS_SERVE_STATIC_FILES="true" \
    RAILS_ENV="production" \
    BUNDLE_WITHOUT="development"

RUN apk update \
 && apk upgrade \
 && apk --update add \
      g++ \
      make \
      tzdata \
      gcompat \
      libpq-dev \
      postgresql-client \
      openssh \
      git \
      bash \
      nodejs \
      yarn \
 && cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime \
 && rm -rf /var/cache/apk/*

RUN gem update --system \
 && gem install bundler --no-document

COPY Gemfile Gemfile.lock ./

# パッケージをインストール
RUN bundle install --jobs 4 --retry 3


# アプリケーションファイルをコピー
COPY . .


# コンテナー起動時に毎回実行されるスクリプトを追加
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000

# イメージ実行時に起動させる主プロセスを設定
CMD ["rails", "server", "-b", "0.0.0.0"]

https://hub.docker.com/layers/library/ruby/3.2.1-alpine/images/sha256-fee8f90378b13a5ac5ce376ef173955048f65c088e6a9994c2ae84c35df87a4e?context=explore

Dockerコンテナが起動された際に実行されるエントリーポイント(起動スクリプト)を設定します。
CMDRUNコマンドを使用して、アプリケーションを起動するコマンドを指定しますが、ENTRYPOINTを使用することで、Dockerコンテナが常に特定のスクリプトを実行するように設定することができます。

entrypoint.sh
#!/bin/bash

# If running the rails server then create or migrate existing database
if [ "${*}" == "./bin/rails server" ]; then
  ./bin/rails db:prepare
fi

exec "${@}"

Railsアプリケーションのデータベースを作成またはマイグレーションを実行するためのdb:prepareタスクを実行します。
このスクリプトの目的は、特定の条件でRailsアプリケーションのデータベースをセットアップすることです。通常、RailsアプリケーションのデータベースをDockerコンテナ内で適切に設定するためには、データベースの作成やマイグレーションが必要です。
このスクリプトはそれを自動化しており、RailsアプリケーションがDockerコンテナ内で起動される際にデータベースを準備するためのスクリプトです。

docker-compose.ymlを作成する

docker-compose.yml
services:
  db:
    image: postgres
    restart: always
    volumes:
      - ./tmp/db:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: password
      TZ: "Asia/Tokyo"
    ports:
      - "5432:5432"

  web:
    build: ./api
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - ./api:/app
    ports:
      - "3000:3000"
    depends_on:
      - db

データベースとウェブという 2 つのアプリを含んだサービスが定義します。
そしてそれぞれの Docker イメージを定義します。
データベースは既存の PostgreSQL イメージにより動作します。ウェブアプリはカレントディレクトリ内に生成されます。

rm -f /myapp/tmp/pids/server.pid: これはサーバー内にserver.pidというファイルが先に存在していたときに、サーバーが再起動できなくなる問題を回避するものです。 このスクリプトは、コンテナーが起動されるたびに実行されることになります。

DBの接続情報を追加する

PostgreSQLの接続情報を追加します。

db/database.yml
default: &default
++  host: db
++  username: postgres
++  password: password
  # For details on connection pooling, see Rails configuration guide
  # https://guides.rubyonrails.org/configuring.html#database-pooling
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

フロントエンドを構築する

viteを使ってVue+Typescriptでアプリを作成します。

npm create vite@latest
Need to install the following packages:
create-vite@5.5.3
Ok to proceed? (y) y


> npx
> create-vite

✔ Project name: … vue-app
✔ Select a framework: › Vue
✔ Select a variant: › TypeScript

Scaffolding project in /Users/***/vue-app...

Done. Now run:

  cd vue-app
  npm install
  npm run dev

サーバーを立ち上げて以下のような画面が表示されればOKです。
これでVue+Typescriptのアプリの雛形が作成されました。

https://vite.dev/guide/

ESLintを導入する

ESLintを導入するためにこちらのパッケージをインストールします。

ESLintはJavaScript/TypeScript のソースコードがコーディングルールに則っているかを検証するためのツールです。

ESLint と Prettier にはそれぞれコード整形のためのルールが存在するため、競合する可能性があります。
そのため、コード整形は Prettier に任せ、Prettierと競合するESLintのルールを無効化するために、eslint-config-prettierをインストールします。

npm install --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-vue typescript-eslint eslint-config-prettier

ESLintの設定用eslint.config.jsを作成します。
公式にあるサンプルを使います。

eslint.config.js
import js from '@eslint/js'
import eslintPluginVue from 'eslint-plugin-vue'
import ts from 'typescript-eslint'
import prettierConfig from 'eslint-config-prettier'
export default ts.config(
  js.configs.recommended,
  ...ts.configs.recommended,
  ...eslintPluginVue.configs['flat/recommended'],
  prettierConfig,
  {
    files: ['*.vue', '**/*.vue'],
    languageOptions: {
      parserOptions: {
        parser: '@typescript-eslint/parser'
      }
    }
  }
)

https://eslint.vuejs.org/user-guide/

ESLint用スクリプトを追加します。

package.json
  "scripts": {
++    "lint": "eslint . -c .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --config eslint.config.js",
  },

npm run lintを使えるようになりました。

npm run lint

> vue-app@0.0.0 lint
> eslint . -c .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --config eslint.config.js

Prettierを導入する

Prettierはソースコードを整形するツールです。
1 行のあたりの最大文字数、 セミコロンの有無、 インデント幅などの整形ルールを設定すると、任意のタイミングでコードを自動整形してくれます。

npm install --save-dev prettier @types/eslint-config-prettiere eslint-plugin-prettier

Prettierの設定用.prettierrcを作成します。

.prettierrc
{
  "printWidth": 120,
  "semi": false,
  "singleQuote": true,
  "trailingComma": "none",
  "tabWidth": 2
}

コード整形から排除したいファイルがあれば.prettierignoreファイルを作成し、ファイルやディレクトリを指定することができます。

Prettier用スクリプトを追加します。

package.json
  "scripts": {
++  "format": "prettier --write \"src/**/*.{js,ts,vue,json}\"",
  },

npm run formatを使えるようになりました。

npm run format

> vue-app@0.0.0 format
> prettier --write "src/**/*.{js,ts,vue,json}"

src/App.vue 47ms (unchanged)
src/components/HelloWorld.vue 21ms
src/main.ts 2ms (unchanged)
src/vite-env.d.ts 2ms (unchanged)

/frontにDockerfileを作成する

# 公式のNodeランタイムをベースイメージとして使用する
FROM node:18-alpine

# 作業ディレクトリを設定する
WORKDIR /app

# package.jsonとpackage-lock.jsonを作業ディレクトリにコピーする
COPY package*.json ./

# パッケージをインストールする
RUN apk --no-cache update && npm install

# プロジェクトのファイルとフォルダを現在の作業ディレクトリ('app'フォルダ)にコピーする
COPY . .

EXPOSE 5173

CMD ["npm", "run", "dev"]

docker-compose.yml/front用の記述を追加します。

docker-compose.yml
services:
++  front:
++    build:
++      context: ./front
++      dockerfile: Dockerfile
++    volumes:
++      - ./front:/app
++      - /app/node_modules
++    ports:
++      - "5173:5173"
++    command: npm run dev
++
++volumes:
++  postgres_data:

vite.config.tsを編集する

vite.config.tsにこちらの記述を追加します。
host: Viteサーバーがリッスンすべき IP アドレスを指定します。 0.0.0.0 もしくは true に設定します。

vite.confitg.ts
  server: {
    host: true,
    watch: {
      usePolling: true
    }
  }

https://ja.vite.dev/config/server-options


セットアップが完了しました。
コンテナをビルドと起動します。

docker compose build

docker compose build
✗ docker compose build
docker compose build
[+] Building 32.9s (26/26) FINISHED                                             docker:desktop-linux
 => [web internal] load build definition from Dockerfile                                        0.0s
 => => transferring dockerfile: 1.16kB                                                          0.0s
 => [front internal] load build definition from Dockerfile                                      0.0s
 => => transferring dockerfile: 477B                                                            0.0s
 => [web internal] load metadata for docker.io/library/ruby:3.2.1-alpine                        2.1s
 => [front internal] load metadata for docker.io/library/node:18-alpine                         1.2s
 => [front internal] load .dockerignore                                                         0.0s
 => => transferring context: 2B                                                                 0.0s
 => [front 1/5] FROM docker.io/library/node:18-alpine@sha256:02376a266c84acbf45bd19440e08e48b1  0.0s
 => [front internal] load build context                                                         1.7s
 => => transferring context: 121.84MB                                                           1.7s
 => [web internal] load .dockerignore                                                           0.0s
 => => transferring context: 801B                                                               0.0s
 => [web 1/9] FROM docker.io/library/ruby:3.2.1-alpine@sha256:04ad4930356641f944d81037b32b5a34  2.4s
 => => resolve docker.io/library/ruby:3.2.1-alpine@sha256:04ad4930356641f944d81037b32b5a345c3d  0.0s
 => => sha256:e449c52ab15a86b642acc1f3d08fcc3d495eba58eb4b7b67c5b97787ce7691a9 4.12MB / 4.12MB  0.6s
 => => sha256:9ba7ad0f163b30834342d9da89d2cc78f2da66af5c514c6f0f2294f505d9e707 219B / 219B      0.7s
 => => sha256:04ad4930356641f944d81037b32b5a345c3debfce017559d65131b5d11b9b4e1 1.65kB / 1.65kB  0.0s
 => => sha256:23c585ea5bd244e9815b8353669550ae3d4edd06c015abcda71366168193970e 1.36kB / 1.36kB  0.0s
 => => sha256:e3e366d951988e0ea0bc116b16410c4aaf790b22603afd99aec2cfe0d4579103 7.25kB / 7.25kB  0.0s
 => => sha256:c41833b44d910632b415cd89a9cdaa4d62c9725dc56c99a7ddadafd6719960f9 3.26MB / 3.26MB  0.4s
 => => extracting sha256:c41833b44d910632b415cd89a9cdaa4d62c9725dc56c99a7ddadafd6719960f9       0.1s
 => => sha256:c83cd891d23f0c16a723e092511e103981be1130bbbda7930cc82628ed9835 31.76MB / 31.76MB  1.8s
 => => extracting sha256:e449c52ab15a86b642acc1f3d08fcc3d495eba58eb4b7b67c5b97787ce7691a9       0.2s
 => => sha256:cbbeecc62e2e9cd8e49deb0430769514d740fbe77b56b16dc257439e50167660 172B / 172B      1.0s
 => => extracting sha256:9ba7ad0f163b30834342d9da89d2cc78f2da66af5c514c6f0f2294f505d9e707       0.0s
 => => extracting sha256:c83cd891d23f0c16a723e092511e103981be1130bbbda7930cc82628ed9835c6       0.5s
 => => extracting sha256:cbbeecc62e2e9cd8e49deb0430769514d740fbe77b56b16dc257439e50167660       0.0s
 => [web internal] load build context                                                           0.5s
 => => transferring context: 50.14kB                                                            0.5s
 => CACHED [front 2/5] WORKDIR /app                                                             0.0s
 => CACHED [front 3/5] COPY package*.json ./                                                    0.0s
 => CACHED [front 4/5] RUN apk --no-cache update && npm install                                 0.0s
 => CACHED [front 5/5] COPY . .                                                                 0.0s
 => [front] exporting to image                                                                  0.0s
 => => exporting layers                                                                         0.0s
 => => writing image sha256:495e0f39fe342643caae18d8301744c8a2e6be2a8e0a188dc924150708f9911b    0.0s
 => => naming to docker.io/library/xxx-app-front                                          0.0s
 => [front] resolving provenance for metadata file                                              0.0s
 => [web 2/9] WORKDIR /app                                                                      0.2s
 => [web 3/9] RUN apk update  && apk upgrade  && apk --update add       g++       make       t  8.3s
 => [web 4/9] RUN gem update --system  && gem install bundler --no-document                     4.5s
 => [web 5/9] COPY Gemfile Gemfile.lock ./                                                      0.0s
 => [web 6/9] RUN bundle install --jobs 4 --retry 3                                            14.8s
 => [web 7/9] COPY . .                                                                          0.0s
 => [web 8/9] COPY entrypoint.sh /usr/bin/                                                      0.0s
 => [web 9/9] RUN chmod +x /usr/bin/entrypoint.sh                                               0.1s
 => [web] exporting to image                                                                    0.5s
 => => exporting layers                                                                         0.5s
 => => writing image sha256:3e61e1301805ac153b45e19b99a2693d7953adc6fd0982d6ce8549036b7b9d57    0.0s
 => => naming to docker.io/library/xxx-app-web                                            0.0s
 => [web] resolving provenance for metadata file

docker compose buildを実行しコンテナ環境を構築します。

docker compoup upによりアプリを起動

docker compose up
docker compose up
[+] Running 4/4
 ✔ Network rails-vue-app_default    Created                                                                             0.0s
 ✔ Container rails-vue-app-front-1  Created                                                                             0.6s
 ✔ Container rails-vue-app-db-1     Created                                                                             0.1s
 ✔ Container rails-vue-app-web-1    Created                                                                             0.0s
Attaching to db-1, front-1, web-1
db-1     | The files belonging to this database system will be owned by user "postgres".
db-1     | This user must also own the server process.
db-1     |
db-1     | The database cluster will be initialized with locale "en_US.utf8".
db-1     | The default database encoding has accordingly been set to "UTF8".
db-1     | The default text search configuration will be set to "english".
db-1     |
db-1     | Data page checksums are disabled.
db-1     |
db-1     | fixing permissions on existing directory /var/lib/postgresql/data ... ok
db-1     | creating subdirectories ... ok
db-1     | selecting dynamic shared memory implementation ... posix
db-1     | selecting default "max_connections" ... 100
db-1     | selecting default "shared_buffers" ... 128MB
db-1     | selecting default time zone ... Asia/Tokyo
db-1     | creating configuration files ... ok
front-1  |
front-1  | > vue-app@0.0.0 dev
front-1  | > vite
front-1  |
front-1  | Re-optimizing dependencies because lockfile has changed
front-1  |
front-1  |   VITE v5.4.9  ready in 246 ms
front-1  |
front-1  |   ➜  Local:   http://localhost:5173/
front-1  |   ➜  Network: use --host to expose
web-1    | Bundle complete! 8 Gemfile dependencies, 81 gems now installed.
web-1    | Gems in the group 'development' were not installed.
web-1    | Bundled gems are installed into `/usr/local/bundle`
db-1     | running bootstrap script ... ok
db-1     | performing post-bootstrap initialization ... ok
db-1     | syncing data to disk ... ok
db-1     |
db-1     |
db-1     | Success. You can now start the database server using:
db-1     |
db-1     |     pg_ctl -D /var/lib/postgresql/data -l logfile start
db-1     |
db-1     | initdb: warning: enabling "trust" authentication for local connections
db-1     | initdb: hint: You can change this by editing pg_hba.conf or using the option -A, or --auth-local and --auth-host, the next time you run initdb.
db-1     | waiting for server to start....2024-10-19 21:09:06.564 JST [48] LOG:  starting PostgreSQL 17.0 (Debian 17.0-1.pgdg120+1) on aarch64-unknown-linux-gnu, compiled by gcc (Debian 12.2.0-14) 12.2.0, 64-bit
db-1     | 2024-10-19 21:09:06.566 JST [48] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
db-1     | 2024-10-19 21:09:06.574 JST [51] LOG:  database system was shut down at 2024-10-19 21:09:06 JST
db-1     | 2024-10-19 21:09:06.581 JST [48] LOG:  database system is ready to accept connections
db-1     |  done
db-1     | server started
db-1     |
db-1     | /usr/local/bin/docker-entrypoint.sh: ignoring /docker-entrypoint-initdb.d/*
db-1     |
db-1     | waiting for server to shut down...2024-10-19 21:09:06.768 JST [48] LOG:  received fast shutdown request
db-1     | .2024-10-19 21:09:06.770 JST [48] LOG:  aborting any active transactions
db-1     | 2024-10-19 21:09:06.772 JST [48] LOG:  background worker "logical replication launcher" (PID 54) exited with exit code 1
db-1     | 2024-10-19 21:09:06.774 JST [49] LOG:  shutting down
db-1     | 2024-10-19 21:09:06.775 JST [49] LOG:  checkpoint starting: shutdown immediate
db-1     | 2024-10-19 21:09:06.790 JST [49] LOG:  checkpoint complete: wrote 3 buffers (0.0%); 0 WAL file(s) added, 0 removed, 0 recycled; write=0.004 s, sync=0.001 s, total=0.016 s; sync files=2, longest=0.001 s, average=0.001 s; distance=0 kB, estimate=0 kB; lsn=0/14E4F98, redo lsn=0/14E4F98
db-1     | 2024-10-19 21:09:06.802 JST [48] LOG:  database system is shut down
db-1     |  done
db-1     | server stopped
db-1     |
db-1     | PostgreSQL init process complete; ready for start up.
db-1     |
db-1     | 2024-10-19 21:09:06.883 JST [1] LOG:  starting PostgreSQL 17.0 (Debian 17.0-1.pgdg120+1) on aarch64-unknown-linux-gnu, compiled by gcc (Debian 12.2.0-14) 12.2.0, 64-bit
db-1     | 2024-10-19 21:09:06.884 JST [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
db-1     | 2024-10-19 21:09:06.884 JST [1] LOG:  listening on IPv6 address "::", port 5432
db-1     | 2024-10-19 21:09:06.885 JST [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
db-1     | 2024-10-19 21:09:06.892 JST [62] LOG:  database system was shut down at 2024-10-19 21:09:06 JST
db-1     | 2024-10-19 21:09:06.900 JST [1] LOG:  database system is ready to accept connections
web-1    | => Booting Puma
web-1    | => Rails 7.2.1.1 application starting in production
web-1    | => Run `bin/rails server --help` for more startup options
web-1    | Puma starting in single mode...
web-1    | * Puma version: 6.4.3 (ruby 3.2.1-p31) ("The Eagle of Durango")
web-1    | *  Min threads: 3
web-1    | *  Max threads: 3
web-1    | *  Environment: production
web-1    | *          PID: 1
web-1    | * Listening on http://0.0.0.0:3000
web-1    | Use Ctrl-C to stop

RailsとViteサーバーがそれぞれを立ち上げましたらOKです。

終わりに

RailsとVueとTypescriptとPostgreSQLの開発環境をDocker Composeで構築してみました。
誰かの参考になれば嬉しいです。

Discussion