rbenv+ruby-buildとunicornでもっとさくっとRedmine入れてみる

前にこういう記事書いたんですけど、Redmine プロジェクトは結構活発でついこの間 1.4.0 がリリースされてました。なんと Ruby1.9 系へ対応!Gemfile も提供されるようになってよりインストールが簡単になってたのでかっとなってやってみました。ついでにこちらもだいぶ枯れてきた rbenv+ruby-build を使い、HTTP サーバとしては unicorn を使ってみました。

rbenv+ruby-build のインストール

git は入っている前提ですが、超簡単です。

$ cd
$ git clone https://github.com/sstephenson/rbenv.git .rbenv
$ mkdir -p ~/.rbenv/plugins
$ cd ~/.rbenv/plugins
$ git clone https://github.com/sstephenson/ruby-build.git

あとは.bashrc とかに以下の記述をして再読込。

export PATH="$HOME/.rbenv/bin:$PATH"
eval "$(rbenv init -)"

これで rbenv コマンドが(タブ補完つきで)使うことができます。さて、早速 ruby を入れたいんですが、後ほどハマらないように先に OS のパッケージ管理で openssl の開発パッケージを入れておきます。

# yum install openssl-devel

Redmine1.4 からはついに ruby1.9 が使えるので 1.9.3 を入れてみましょう。

$ rbenv install 1.9.3-p125
$ rbenv rehash
$ rbenv versions
  1.9.3-p125

Redmine のダウンロード

tar.gz をダウンロードしてもいいんですが、有志の方が本家の svn を github に上げてくれいているのでそれを使ってみます。

$ cd
$ git clone https://github.com/redmine/redmine.git
$ cd redmine
$ git branch -a
  master
  remotes/origin/0.6-stable
  remotes/origin/0.7-stable
  remotes/origin/0.8-stable
  remotes/origin/0.9-stable
  remotes/origin/1.0-stable
  remotes/origin/1.1-stable
  remotes/origin/1.2-stable
  remotes/origin/1.3-stable
  remotes/origin/1.4-stable
  remotes/origin/HEAD -> origin/master
  remotes/origin/master

こういう感じでバージョンごとに stable の branch があるのでとりあえずこれに切り替えておけば多分 git pull するだけで stable の最新版を追いかけられる様な気がしました(勘で言ってます)。

$ cd ~/redmine
$ git checkout -b 1.4-stable remotes/origin/1.4-stable
$ git branch
* 1.4-stable
  master

ではここで先程ビルドしたバージョンの ruby を使う様に rbenv の設定をします。

$ cd ~/redmine
$ rbenv local 1.9.3-p125
$ ruby -v
ruby 1.9.3p125 (2012-02-16 revision 34643) [x86_64-linux]
$ rbenv which ruby
/home/riywo/.rbenv/versions/1.9.3-p125/bin/ruby

このディレクトリ配下にいるときだけこの設定は効果を発揮しますので、別のディレクトリに移ると元に戻ります。おもしろいですね。

$ cd ~
$ rbenv which ruby
/usr/bin/ruby

bundler のインストール

基本的に gem パッケージはアプリケーション毎にローカルに持つんですけど、それを実現するための bundler パッケージだけは先程ビルドした ruby 本体の中に入れてしまいます。rbenv shellとかでもいんですけど、先程rbenv localで設定したディレクトリで作業しちゃいます。

$ cd ~/redmine
$ rbenv which gem
/home/riywo/.rbenv/versions/1.9.3-p125/bin/gem
$ gem install bundler
$ rbenv rehash
$ bundle -v
Bundler version 1.1.3

これ以降、このバージョンの ruby に対しては直接gemコマンドを叩くことはありません。全て Gemfile という DSL に記述してbundleコマンドで、アプリケーションローカルにインストールしていきます。

Redmine の依存モジュールをインストール

Redmine1.4 からは Gemfile が提供されているので何も考えずに叩くだけです。ただし、普通に叩くと ruby 本体の方に入ってしまうので、オプションでインストール場所をローカルにしておきます。Imagemagick とかポスグレとかは無視。MySQL のクライアントライブラリは先に頑張って入れておいて下さい。

$ cd ~/redmine
$ bundle install --path vendor/bundle --without development test rmagick postgresql sqlite

簡単ですね!ついでに Unicorn も入れておきたいので Gemfile.local というファイルを作ってもう一回実行します。すでにインストール先は設定されてるのでオプション不要です。

$ cd ~/redmine
$ echo 'gem "unicorn"' > Gemfile.local
$ bundle install
$ bundle --verbose list

初めてのインストールの場合

Redmine を初めてインストールする場合はドキュメントに従ってゴニョゴニョします。一応コマンド書いてますが、最新のドキュメントを常に参照して下さい。

MySQL のユーザは適当に。

create database redmine character set utf8;
create user 'redmine'@'localhost' identified by 'my_password';
grant all privileges on redmine.* to 'redmine'@'localhost';

config/database.yml は以下の様に。mysql2を指定するのがポイントだそうです。

production:
  adapter: mysql2
  database: redmine
  host: localhost
  username: redmine
  password: my_password

あとはひたすらrakeコマンド。ただし、bundler で gem を入れているのでbundle execで実行します。終わったら試しに WEBrick で起動してブラウザで見てみましょう。

$ cd ~/redmine
$ bundle exec rake -- generate_session_store
$ RAILS_ENV=production bundle exec rake db:migrate
$ RAILS_ENV=production bundle exec rake  redmine:load_default_data
$ bundle exec script/server webrick -e production

1.2 とかからアップデートする場合

解説めんどいのでドキュメント見てください。。。大して難しくないです。rakeするときにbundle execするのをお忘れなく。

Unicorn で起動

あとはこれを unicorn で起動するだけです。起動方法のデファクトがよく分からなかったので、とりあえず慣れてる daemontools ならこんな感じかと。unicorn の config 自体はイマイチよく分かってないのでサンプルを適当にいじっただけ。

GitHub Javascript Strategy

Unless otherwise necessary (such as mobile development), the GitHub javascript codebase is based off jQuery. You can safely assume it will be included on every page.

File naming

  • All jquery plugins should be prefixed with jquery, such as jquery.facebox
  • All github-specific jquery plugins should be prefixed with jquery.github. Like jquery.github.repo_list.js
  • All page-specific files (that only run on ONE page) should be prefixed with page. page.billing.js

File structure

All javascript for the site should be put into a directory. Feel free to create new directories when it makes sense (gist, iphone, etc). There are a few directories that have special meanings:

  • common - Contains all files that are used and shared between the main site and sub-sites (like gist)
  • rogue - Files that are directly called by individual pages. Use for large libraries that are only used on one (or a very few) pages.
  • dev - Files that are hosted elsewhere (such as google) that you will need if you do not have an internet connection.

Inside of each directory, you should note the difference between plugins and files where possible and split them into different directories (see github).

Deployment

On deploy, each top level directory is bundled into a compressed file. Any files that are inside the directory (and it's subfolders) will be bundled into one file and available at bundle_[directory].js

Template inclusion

If you would like to include a bundle in your Rails templates, you reference bundles or individual files using the javascript_bundle and javascript_dev helpers.

Example

javascript_dev ['jquery', 'http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js']
javascript_bundle 'common', 'github', 'rogue/s3_upload'

In production, this will include:

  • http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js in production.
  • bundle_common.js
  • bundle_github.js
  • rogue/s3_upload.js

In development, this will include:

  • dev/jquery.js
  • All javascripts inside of the common directory
  • All javascripts inside the github directory
  • rogue/s3_upload.js
view raw _README.md hosted with ❤ by GitHub
RAILS_ROOT ||= ENV["RAILS_ROOT"]
namespace :bundle do
task :all => [ :js, :css ]
task :js do
compression_method = "closure"
require 'lib/js_minimizer' if compression_method != "closure"
closure_path = RAILS_ROOT + '/lib/closure_compressor.jar'
paths = get_top_level_directories('/public/javascripts')
targets = []
paths.each do |bundle_directory|
bundle_name = bundle_directory.gsub(RAILS_ROOT + '/public/javascripts/', "")
files = recursive_file_list(bundle_directory, ".js")
next if files.empty? || bundle_name == 'dev'
target = RAILS_ROOT + "/public/javascripts/bundle_#{bundle_name}.js"
if compression_method == "closure"
`java -jar #{closure_path} --js #{files.join(" --js ")} --js_output_file #{target} 2> /dev/null`
else
File.open(target, 'w+') do |f|
f.puts JSMinimizer.minimize_files(*files)
end
end
targets << target
end
targets.each do |target|
puts "=> bundled js at #{target}"
end
end
task :css do
yuipath = RAILS_ROOT + '/lib/yuicompressor-2.4.1.jar'
paths = get_top_level_directories('/public/stylesheets')
targets = []
paths.each do |bundle_directory|
bundle_name = bundle_directory.gsub(RAILS_ROOT + '/public/stylesheets/', "")
files = recursive_file_list(bundle_directory, ".css")
next if files.empty? || bundle_name == 'dev'
bundle = ''
files.each do |file_path|
bundle << File.read(file_path) << "\n"
end
target = RAILS_ROOT + "/public/stylesheets/bundle_#{bundle_name}.css"
rawpath = "/tmp/bundle_raw.css"
File.open(rawpath, 'w') { |f| f.write(bundle) }
`java -jar #{yuipath} --line-break 0 #{rawpath} -o #{target}`
targets << target
end
targets.each do |target|
puts "=> bundled css at #{target}"
end
end
require 'find'
def recursive_file_list(basedir, ext)
files = []
Find.find(basedir) do |path|
if FileTest.directory?(path)
if File.basename(path)[0] == ?. # Skip dot directories
Find.prune
else
next
end
end
files << path if File.extname(path) == ext
end
files.sort
end
def get_top_level_directories(base_path)
Dir.entries(RAILS_ROOT + base_path).collect do |path|
path = RAILS_ROOT + "#{base_path}/#{path}"
File.basename(path)[0] == ?. || !File.directory?(path) ? nil : path # not dot directories or files
end - [nil]
end
end
view raw bundle.rake hosted with ❤ by GitHub
require 'find'
module BundleHelper
def bundle_files?
Rails.production? || Rails.staging? || params[:bundle] || cookies[:bundle] == "yes"
end
def javascript_bundle(*sources)
sources = sources.to_a
bundle_files? ? javascript_include_bundles(sources) : javascript_include_files(sources)
end
# This method assumes you have manually bundled js using a rake command
# or similar. So there better be bundle_* files.
def javascript_include_bundles(bundles)
output = ""
bundles.each do |bundle|
output << javascript_src_tag("bundle_#{bundle}", {}) + "\n"
end
output
end
def javascript_include_files(bundles)
output = ""
bundles.each do |bundle|
files = recursive_file_list("public/javascripts/#{bundle}", ".js")
files.each do |file|
file = file.gsub('public/javascripts/', '')
output << javascript_src_tag(file, {}) + "\n"
end
end
output
end
def javascript_dev(*sources)
output = ""
sources = sources.to_a
sources.each do |pair|
output << javascript_src_tag(Rails.development? ? "dev/#{pair[0]}" : pair[1], {})
end
output
end
def stylesheet_bundle(*sources)
sources = sources.to_a
bundle_files? ? stylesheet_include_bundles(sources) : stylesheet_include_files(sources)
end
# This method assumes you have manually bundled css using a rake command
# or similar. So there better be bundle_* files.
def stylesheet_include_bundles(bundles)
stylesheet_link_tag(bundles.collect{ |b| "bundle_#{b}"})
end
def stylesheet_include_files(bundles)
output = ""
bundles.each do |bundle|
files = recursive_file_list("public/stylesheets/#{bundle}", ".css")
files.each do |file|
file = file.gsub('public/stylesheets/', '')
output << stylesheet_link_tag(file)
end
end
output
end
def recursive_file_list(basedir, extname)
files = []
basedir = RAILS_ROOT + "/" + basedir
Find.find(basedir) do |path|
if FileTest.directory?(path)
if File.basename(path)[0] == ?.
Find.prune
else
next
end
end
files << path.gsub(RAILS_ROOT + '/', '') if File.extname(path) == extname
end
files.sort
end
end
namespace :deploy do
desc "Shrink and bundle js and css"
task :bundle, :roles => :web, :except => { :no_release => true } do
run "cd #{current_path}; RAILS_ROOT=#{current_path} rake bundle:all"
end
end
after "deploy:update_code", "deploy:bundle"
view raw deploy.rb hosted with ❤ by GitHub
config.action_controller.asset_host = Proc.new do |source, request|
non_ssl_host = "http://assets#{source.hash % 4}.github.com"
ssl_host = "https://assets#{source.hash % 4}.github.com"
if request.ssl?
if source =~ /\.js$/
ssl_host
elsif request.headers["USER_AGENT"] =~ /(Safari)/
non_ssl_host
else
ssl_host
end
else
non_ssl_host
end
end
repo = Grit::Repo.new(RAILS_ROOT)
js = repo.log('master', 'public/javascripts', :max_count => 1).first
css = repo.log('master', 'public/stylesheets', :max_count => 1).first
ENV['RAILS_ASSET_ID'] = js.committed_date > css.committed_date ? js.id : css.id
view raw production.rb hosted with ❤ by GitHub

これで 8080 ポートで待ち受けているので、後は Apache なり nginx なりからリバースプロキシしてあげれば完成かなと思います。

おわりに

だいぶすっきりしたなぁという印象です。ruby は bundler も rbenv もよくできていて、スタンドアロンな環境をさくさく作れていい感じだと思います。

また、Redmine プロジェクトはこれだけの結構大きいアプリケーションなのに精力的に開発されていてすばらしいと思いました。ちっぽけですが少しでも貢献できればと思い記事をしたためました。