Provisionando Aplicações Rails com Mina, Unicorn e Nginx

Depois de um tempo usando Passenger, migrei para o Unicorn, nesse post irei descrever o setup que tenho utilizado para provisionar aplicações Rails em produção desde então.

Como o título já diz, uso Nginx como servidor http, mas nesse post não mostrarei como instalá-lo, você pode ver a wiki oficial do projeto pra isso.

Vamos começar adicionando a gema do Unicorn em nosso Gemfile:

group :production do
  gem 'unicorn'
end

Como uso Unicorn apenas em produção, então deixo dentro do bloco production.

Criaremos agora um arquivo de configuração do Unicorn, geralmente colocamos em config/unicorn.rb, com o seguinte conteúdo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# Set your full path to application.
app_dir = File.expand_path('../../', __FILE__)
shared_dir = File.expand_path('../../../shared/', __FILE__)

# Set unicorn options
worker_processes 1
preload_app true
timeout 30

# Fill path to your app
working_directory app_dir

# Set up socket location
listen "#{shared_dir}/sockets/unicorn.sock", :backlog => 64

# Loging
stderr_path "#{shared_dir}/log/unicorn.stderr.log"
stdout_path "#{shared_dir}/log/unicorn.stdout.log"

# Set master PID location
pid "#{shared_dir}/pids/unicorn.pid"

before_fork do |server, worker|
  defined?(ActiveRecord::Base) and ActiveRecord::Base.connection.disconnect!
  old_pid = "#{server.config[:pid]}.oldbin"
  if File.exists?(old_pid) && server.pid != old_pid
    begin
      sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
      Process.kill(sig, File.read(old_pid).to_i)
    rescue Errno::ENOENT, Errno::ESRCH
      # someone else did our job for us
    end
  end
end

after_fork do |server, worker|
  defined?(ActiveRecord::Base) and ActiveRecord::Base.establish_connection
end

before_exec do |server|
  ENV['BUNDLE_GEMFILE'] = "#{app_dir}/Gemfile"
end

O arquivo é auto explicativo e bem fácil de entender, você pode alterar as configurações como quiser. Uma coisa importante que devemos atentar aqui é o caminho onde o socket ficará armazenado (linha 14), precisaremos dele a seguir.

Agora, vamos configurar nossa entrada no servidor, eu costumo criar um arquivo com o nome da aplicação (nesse caso my_app) dentro da pasta sites-enabled no diretório do Nginx.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# /opt/nginx/sites-enabled/my_app 
upstream my_app {
  # socket location
  server unix:/home/user/my_app/shared/sockets/unicorn.sock fail_timeout=0;
}
server {
  listen 80;
  server_name  my_app.com www.my_app.com;
  root /home/user/my_app/current/public/;

  location / {
    # if file does not exist, use the last, in this case, @app
    try_files $uri $uri/index.html $uri.html @app;
  }
  
  location @app {
    # must match the name of upstream directive which is defined above
    proxy_pass http://my_app;
    proxy_redirect off;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_set_header Host $http_host;
    # below line is only required for HTTPS
    # proxy_set_header   X-Forwarded-Proto https;
  }
 
  # cache control
  location ~ ^/(assets|images|javascripts|stylesheets|swfs|system)/ {
    expires max;
    add_header Cache-Control public;
    add_header Last-Modified "";
    add_header ETag "";
    break;
  }
}

Veja a linha 4, ela deve apontar para o caminho onde o socket do Unicorn está.

Com isso já conseguimos executar nossa aplicação. Vamos então configurar o Mina para fazer deploy.

O setup é bem rápido e fácil, veja o guia oficial para isso.

Após fazer um deploy, precisamos reiniciar o servidor pra recarregar a aplicação, o Unicorn armazena o número do processo rodando num arquivo pid que configuramos no unicorn.rb acima, precisamos matar esse processo e iniciar o servidor de aplicação novamente, isso fica fácil com a gema mina-unicorn pois ela faz tudo pra nós, só precisamos inclui-la, passar o caminho do arquivo pid e chamar a tarefa unicorn:restart, veja abaixo:

# Gemfile
gem 'mina-unicorn', :require => false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# config/deploy.rb
require 'mina/bundler'
require 'mina/rails'
require 'mina/git'
require 'mina/rvm'
require 'mina/unicorn'

set :domain, 'XXX.XXX.XX.XXX' # ip address
set :deploy_to, '/home/user/my_app'
set :repository, 'git@github.com:user/my_app.git'
set :branch, 'master'
set :user, 'user' # Username in the server to SSH to.

set :unicorn_pid, "#{deploy_to}/shared/pids/unicorn.pid"

# Set forward_agent to use local SSH keys
set :forward_agent, true

# Manually create these paths in shared/ (eg: shared/config/database.yml) in your server.
# They will be linked in the 'deploy:link_shared_paths' step.
set :shared_paths, ['config/database.yml', 'log']

# This task is the environment that is loaded for most commands, such as
# `mina deploy` or `mina rake`.
task :environment do
  # For those using RVM, use this to load an RVM version@gemset.
  invoke :'rvm:use[ruby-2.1.2]'
end

# Put any custom mkdir's in here for when `mina setup` is ran.
# For Rails apps, we'll make some of the shared paths that are shared between
# all releases.
task :setup => :environment do
  queue! %[mkdir -p "#{deploy_to}/shared/log"]
  queue! %[chmod g+rx,u+rwx "#{deploy_to}/shared/log"]

  queue! %[mkdir -p "#{deploy_to}/shared/config"]
  queue! %[chmod g+rx,u+rwx "#{deploy_to}/shared/config"]

  queue! %[touch "#{deploy_to}/shared/config/database.yml"]
  queue  %[echo "-----> Be sure to edit 'shared/config/database.yml'."]
end

desc "Deploys the current version to the server."
task :deploy => :environment do
  deploy do
    # Put things that will set up an empty directory into a fully set-up
    # instance of your project.
    invoke :'git:clone'
    invoke :'deploy:link_shared_paths'
    invoke :'bundle:install'
    invoke :'rails:db_migrate'
    invoke :'rails:assets_precompile'
    invoke :'deploy:cleanup'

    to :launch do
      invoke :'unicorn:restart'
    end
  end
end

Pronto, temos nosso setup concluído (não esqueça de alterar acima com dados da sua aplicação). Deixe sua opinião nos comentários.

Até mais.

Written on September 19, 2014

Share: