Docker for MacでRails5, MySQLとVue.jsの開発環境を作る

このエントリーを Google ブックマーク に追加
LINEで送る
Pocket

こちらのサイトの記事を参考にしました。
http://easyramble.com/rails-development-on-docker.html
http://qiita.com/jnchito/items/30ab14ebf29b945559f6

以下の手順は数ヶ月前にメモしたものだが今日再度試したらまだ動いた。
MacのOSはSierraでDockerのバージョンはVersion 17.09.0-ce-rc2-mac29。

準備

Docker for Mac をインストールして起動しておく。

空の作業ディレクトリを用意する。
作業ディレクトリ内にアプリ用のディレクトリを作成する。
MySQLのデータを保存するdbディレクトリも作成する。

$ mkdir ./web
$ mkdir ./db

docker-compose.ymlを以下の内容で作成する。

version: '2'
services:
  db:
    image: mysql
    environment:
      MYSQL_ROOT_PASSWORD: password
    ports:
      - "3306:3306"
    volumes:
      - ./db:/var/lib/mysql
      - /etc/localtime:/etc/localtime:ro
  web:
    build: ./web
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - ./web:/myapp
    ports:
      - "3000:3000"
      - "8080:8080"
    depends_on:
      - db

アプリ用ディレクトリ内にDockerfileを作成する

Dockerfile

FROM ruby:2.3.1
ENV LANG C.UTF-8
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev libxml2-dev libxslt1-dev libqt4-webkit libqt4-dev xvfb imagemagick libmagickcore-dev libmagickwand-dev
RUN apt-get install -y curl apt-transport-https wget && \
    curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
    echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
    apt-get update && apt-get -y install -y yarn
RUN curl -sL https://deb.nodesource.com/setup_7.x | bash - && apt-get install nodejs
RUN gem install bundler
WORKDIR /tmp
ADD Gemfile Gemfile
ADD Gemfile.lock Gemfile.lock
RUN bundle install
ENV APP_HOME /myapp
RUN mkdir -p $APP_HOME
WORKDIR $APP_HOME
ADD . $APP_HOME

アプリ用ディレクトリ内にGemfileを作成する。

source 'https://rubygems.org'
gem 'rails'

アプリ用ディレクトリ内に空のGemfile.lockを作成する。

$ touch web/Gemfile.lock

ここまでで、ディレクトリ構成は以下になる。

作業ディレクトリ
|-db
|-web
  |-Dockerfile
  |-Gemfile
  |-Gemfile.lock
|-docker-compose.yml

Railsアプリ生成

Railsアプリを作成する。

$ docker-compose run web rails new . --force --database=mysql --skip-bundle

Railsアプリが作成され、Gemfileが上書きされる。

Gemfileに以下を追記する。

gem 'therubyracer', platforms: :ruby
gem 'webpacker', '~> 2.0'

bundle installを実行する。

$ docker-compose run web bundle install

Gemfile.lockが上書きされる。

ビルド

ビルドする

$ docker-compose build

Railsのconfig/database.ymlを編集する。
password, hostを指定する。

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

Dockerコンテナを実行する。

$ docker-compose up

DBを作成するために、別のタブなどで以下を実行してwebコンテナのBashを起動する。

$ docker-compose exec web bash

bashでDBを作成する。

# rails db:create
Created database 'myapp_development'
Created database 'myapp_test'
# exit

この時点で
http://localhost:3000/
をブラウザで見るとRailsの初期画面が表示される。

Vue.js

これをサンプルに同じものが動くようにしていく。
http://jsfiddle.net/yyx990803/23qze30k/

上記の方法でwebコンテナのBashを実行してYarnがインストールされていることを確認

# yarn -v 

webコンテナのBashでwebpackerをインストールする

# rails webpacker:install

Vue.jsのインストール

# rails webpacker:install:vue

Controller, Viewを生成する

# rails g controller Home index

views/layouts/application.html.erb へ追記する。

<!DOCTYPE html>
<html>
  <head>
    <title>Myapp</title>
    <%= csrf_meta_tags %>

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <%= yield %>
    <%= javascript_pack_tag 'hello_vue' %>
  </body>
</html>

config/routes.rb

Rails.application.routes.draw do
  root to: 'home#index'
end

Dockerコンテナを一旦停止、起動する。

$ docker-compose up

別タブで以下を再度実行してwebコンテナのBashを起動する。

$ docker-compose exec web bash

webpack-dev-serverを起動する

# bin/webpack-dev-server

この時点で
http://localhost:3000/
をブラウザで見ると以下のように表示される。

Home#index

Find me in app/views/home/index.html.erb

Hello Vue!

サンプルのJavaScript, CSSなどを反映していく。
app/javascript/packs/hello_vue.jsを書き換える。

import Vue from 'vue/dist/vue.esm'
import App from './app.vue'
//
document.addEventListener('DOMContentLoaded', () => {
  const app = new Vue({
    el: '#demo',
    data: {
      searchQuery: '',
      gridColumns: ['name', 'power'],
      gridData: [
        { name: 'Chuck Norris', power: Infinity },
        { name: 'Bruce Lee', power: 9000 },
        { name: 'Jackie Chan', power: 7000 },
        { name: 'Jet Li', power: 8000 }
      ]
    },
    components: { 'demo-grid': App }
  })
})

app/javascript/packs/app.vueを書き換える。

<script>
export default {
  template: '#grid-template',
  replace: true,
  props: {
    data: Array,
    columns: Array,
    filterKey: String
  },
  data: function () {
    var sortOrders = {}
    this.columns.forEach(function (key) {
      sortOrders[key] = 1
    })
    return {
      sortKey: '',
      sortOrders: sortOrders
    }
  },
  computed: {
    filteredData: function () {
      var sortKey = this.sortKey
      var filterKey = this.filterKey && this.filterKey.toLowerCase()
      var order = this.sortOrders[sortKey] || 1
      var data = this.data
      if (filterKey) {
        data = data.filter(function (row) {
          return Object.keys(row).some(function (key) {
            return String(row[key]).toLowerCase().indexOf(filterKey) > -1
          })
        })
      }
      if (sortKey) {
        data = data.slice().sort(function (a, b) {
          a = a[sortKey]
          b = b[sortKey]
          return (a === b ? 0 : a > b ? 1 : -1) * order
        })
      }
      return data
    }
  },
  filters: {
    capitalize: function (str) {
      return str.charAt(0).toUpperCase() + str.slice(1)
    }
  },
  methods: {
    sortBy: function (key) {
      this.sortKey = key
      this.sortOrders[key] = this.sortOrders[key] * -1
    }
  }
}
</script>

app/home/index.html.erbを書き換える。

<!-- component template -->
<script type="text/x-template" id="grid-template">
  <table>
    <thead>
      <tr>
        <th v-for="key in columns"
          @click="sortBy(key)"
          :class="{ active: sortKey == key }">
          {{ key | capitalize }}
          <span class="arrow" :class="sortOrders&#91;key&#93; > 0 ? 'asc' : 'dsc'">
          </span>
        </th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="entry in filteredData">
        <td v-for="key in columns">
          {{entry[key]}}
        </td>
      </tr>
    </tbody>
  </table>
</script>

<!-- demo root element -->
<div id="demo">
  <form id="search">
    Search <input name="query" v-model="searchQuery">
  </form>
  <demo-grid
    :data="gridData"
    :columns="gridColumns"
    :filter-key="searchQuery">
  </demo-grid>
</div>

app/assets/stylesheets/home.scssを書き換える。

body {
  font-family: Helvetica Neue, Arial, sans-serif;
  font-size: 14px;
  color: #444;
}

table {
  border: 2px solid #42b983;
  border-radius: 3px;
  background-color: #fff;
}

th {
  background-color: #42b983;
  color: rgba(255,255,255,0.66);
  cursor: pointer;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

td {
  background-color: #f9f9f9;
}

th, td {
  min-width: 120px;
  padding: 10px 20px;
}

th.active {
  color: #fff;
}

th.active .arrow {
  opacity: 1;
}

.arrow {
  display: inline-block;
  vertical-align: middle;
  width: 0;
  height: 0;
  margin-left: 5px;
  opacity: 0.66;
}

.arrow.asc {
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-bottom: 4px solid #fff;
}

.arrow.dsc {
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 4px solid #fff;
}

この時点で
http://localhost:3000/
をブラウザで見るとサンプルと同じものが表示されるはず。

さらにtemplateやCSSをapp.vueに含めるように変更してみる。

app/views/layouts/application.html.erb
stylesheet_pack_tagを追加する。

<!DOCTYPE html>
<html>
  <head>
    <title>Myapp</title>
    <%= csrf_meta_tags %>

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <%= yield %>
    <%= javascript_pack_tag 'hello_vue' %>
    <%= stylesheet_pack_tag 'hello_vue' %>
  </body>
</html>

app/views/home/index.html.erb
template部分を削除する。

<!-- demo root element -->
<div id="demo">
  <form id="search">
    Search <input name="query" v-model="searchQuery">
  </form>
  <demo-grid
    :data="gridData"
    :columns="gridColumns"
    :filter-key="searchQuery">
  </demo-grid>
</div>

app/assets/stylesheets/home.scss
bodyだけ残して削除する。

// Place all the styles related to the Home controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
body {
  font-family: Helvetica Neue, Arial, sans-serif;
  font-size: 14px;
  color: #444;
}

app/javascript/packs/app.vue
templateとstyle scopedを追加する。
template: ‘#grid-template’, を削除する

<template>
  <table>
    <thead>
      <tr>
        <th v-for="key in columns"
          @click="sortBy(key)"
          :class="{ active: sortKey == key }">
          {{ key | capitalize }}
          <span class="arrow" :class="sortOrders&#91;key&#93; > 0 ? 'asc' : 'dsc'">
          </span>
        </th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="entry in filteredData">
        <td v-for="key in columns">
          {{entry[key]}}
        </td>
      </tr>
    </tbody>
  </table>
</template>

<script>
export default {
  replace: true,
  props: {
    data: Array,
    columns: Array,
    filterKey: String
  },
  data: function () {
    var sortOrders = {}
    this.columns.forEach(function (key) {
      sortOrders[key] = 1
    })
    return {
      sortKey: '',
      sortOrders: sortOrders
    }
  },
  computed: {
    filteredData: function () {
      var sortKey = this.sortKey
      var filterKey = this.filterKey && this.filterKey.toLowerCase()
      var order = this.sortOrders[sortKey] || 1
      var data = this.data
      if (filterKey) {
        data = data.filter(function (row) {
          return Object.keys(row).some(function (key) {
            return String(row[key]).toLowerCase().indexOf(filterKey) > -1
          })
        })
      }
      if (sortKey) {
        data = data.slice().sort(function (a, b) {
          a = a[sortKey]
          b = b[sortKey]
          return (a === b ? 0 : a > b ? 1 : -1) * order
        })
      }
      return data
    }
  },
  filters: {
    capitalize: function (str) {
      return str.charAt(0).toUpperCase() + str.slice(1)
    }
  },
  methods: {
    sortBy: function (key) {
      this.sortKey = key
      this.sortOrders[key] = this.sortOrders[key] * -1
    }
  }
}
</script>

<style scoped>
table {
  border: 2px solid #42b983;
  border-radius: 3px;
  background-color: #fff;
}

th {
  background-color: #42b983;
  color: rgba(255,255,255,0.66);
  cursor: pointer;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

td {
  background-color: #f9f9f9;
}

th, td {
  min-width: 120px;
  padding: 10px 20px;
}

th.active {
  color: #fff;
}

th.active .arrow {
  opacity: 1;
}

.arrow {
  display: inline-block;
  vertical-align: middle;
  width: 0;
  height: 0;
  margin-left: 5px;
  opacity: 0.66;
}

.arrow.asc {
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-bottom: 4px solid #fff;
}

.arrow.dsc {
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 4px solid #fff;
}
</style>

http://localhost:3000/ をブラウザで見るとサンプルと同じものが表示されるはず。