Skip to content

Instantly share code, notes, and snippets.

@phund
Last active October 25, 2016 06:39
Show Gist options
  • Save phund/10fb1bd3a40dba2d19aeb03fa08bfb3c to your computer and use it in GitHub Desktop.
Save phund/10fb1bd3a40dba2d19aeb03fa08bfb3c to your computer and use it in GitHub Desktop.
Deploy meteor with forever, capistrano and monit on a EC2

How to deploy meteor app with forever capistrano 3.6.1 and monit (current test with meteor 1.4.1 and ubuntu server 14.04 - EC2)

Step 1: Install enviroment on server

  1. Install node, mongodb... (Please see https://gist.github.com/phund/7372628980516376dfe5eaf8a559428d for more detail)
  2. Install monit

Step 2: Setup capistrano in local

  1. Install ruby Please google for more details
  2. gem install capistrano
  3. cd your/meteor/project
  4. cap install

Step 3: Config capistrano

Because we need to build meteor app before run, we can't use git repository to deploy directly. So we need to config capistrano deploy from local (3.0 < capistrano < 3.7) Detail for deploy.rb

# config valid only for current version of Capistrano
lock '3.6.1'

set :application, 'webportal'

# Default branch is :master
# ask :branch, `git rev-parse --abbrev-ref HEAD`.chomp

# Default deploy_to directory is /var/www/my_app_name
set :deploy_to, '/home/ubuntu/www/webportal'

# Default value for :scm is :git
set :repository, "."
set :deploy_via, :copy

# Default value for :format is :airbrussh.
set :format, :pretty

# You can configure the Airbrussh format using :format_options.
# These are the defaults.
# set :format_options, command_output: true, log_file: 'log/capistrano.log', color: :auto, truncate: :auto

# Default value for :pty is false
# set :pty, true

# Default value for :linked_files is []
# append :linked_files, 'config/database.yml', 'config/secrets.yml'

# Default value for linked_dirs is []
# append :linked_dirs, 'log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'public/system'

# Default value for default_env is {}
# set :default_env, { path: "/opt/ruby/bin:$PATH" }

# Default value for keep_releases is 5
set :keep_releases, 5

# We will tell a white lie to Capistrano
set :scm, :git

# release id is just the commit hash used to create the tarball.
set :project_release_id, `git log --pretty=format:'%h' -n 1 HEAD`
# the same path is used local and remote... just to make things simple for who wrote this.
set :project_tarball_path, "/tmp/#{fetch(:application)}-#{fetch(:project_release_id)}.tar.gz"
set :build_tarball_path, "/tmp/#{fetch(:application)}-#{fetch(:project_release_id)}"

# We create a Git Strategy and tell Capistrano to use it, our Git Strategy has a simple rule: Don't use git.
module NoGitStrategy
  def check
    true
  end

  def test
    # Check if the tarball was uploaded.
    test! " [ -f #{fetch(:project_tarball_path)} ] "
  end

  def clone
    true
  end

  def update
    true
  end

  def release
    # Unpack the tarball uploaded by deploy:upload_tarball task.
    context.execute "tar -xf #{fetch(:project_tarball_path)} -C #{release_path}"
    # Remove it just to keep things clean.
    context.execute :rm, fetch(:project_tarball_path)
  end

  def fetch_revision
    # Return the tarball release id, we are using the git hash of HEAD.
    fetch(:project_release_id)
  end
end

# Capistrano will use the module in :git_strategy property to know what to do on some Capistrano operations.
set :git_strategy, NoGitStrategy

# Finally we need a task to create the tarball and upload it,
namespace :deploy do
  desc 'Update monit files'
  task :update_monit_files do |task, args|
    tarball_path = fetch(:project_tarball_path)
    build_path = fetch(:build_tarball_path)
    app_name = fetch(:application)
    default_environment =  fetch(:default_environment)
    on roles(:app) do
      start_script = <<-EOF
        #!/bin/bash
        echo "Start NodeJS App:"
        echo "  [+] Enter directory && install npm"
        cd #{current_path}/bundle/programs/server && npm install --production
        echo "  [+] Run Meteor script"
        cd #{current_path}/bundle && PORT=#{default_environment[:PORT]} MONGO_URL=#{default_environment[:MONGO_URL]} ROOT_URL=#{default_environment[:ROOT_URL]} METEOR_SETTINGS=$(cat #{shared_path}/#{default_environment[:METEOR_SETTINGS]}) forever start -a -l #{shared_path}/log/production.log -e #{shared_path}/log/error.log --pidFile #{shared_path}/tmp/pids/app.pid #{current_path}/bundle/main.js
      EOF
      location = fetch(:template_dir, "config/deploy") + "/#{fetch(:application)}_start.sh"
      File.open(location,'w+') {|f| f.write start_script }
      upload! "#{location}", "#{shared_path}/#{fetch(:application)}_start.sh"

      stop_script = <<-EOF
        #!/bin/bash
        echo "Stop NodeJS App:"
        echo "  [+] Enter directory and run stop script"
        cd #{current_path}/bundle && forever stop #{current_path}/bundle/main.js || true
      EOF

      location = fetch(:template_dir, "config/deploy") + "/#{fetch(:application)}_stop.sh"
      File.open(location,'w+') {|f| f.write stop_script }
      upload! "#{location}", "#{shared_path}/#{fetch(:application)}_stop.sh"

      execute :sudo, "monit reload"
    end
  end

  desc 'Create and upload project tarball'
  task :upload_tarball do |task, args|
    tarball_path = fetch(:project_tarball_path)
    build_path = fetch(:build_tarball_path)
    app_name = fetch(:application)
    default_environment =  fetch(:default_environment)
    # This will create a project tarball from HEAD, stashed and not committed changes wont be released.
   `meteor build  #{build_path} --architecture os.linux.x86_64 --server #{default_environment[:ROOT_URL]}; mv #{build_path}/#{app_name}.tar.gz #{tarball_path}`
    raise 'Error creating tarball.'if $? != 0

    on roles(:all) do
      upload! tarball_path, tarball_path, recursive: true
      upload! "#{default_environment[:METEOR_SETTINGS]}", "#{shared_path}/#{default_environment[:METEOR_SETTINGS]}"
    end
  end

    #TODO: Add stop task in upstart
  desc "Start Forever"
  task :start do
    on roles(:app) do
      default_environment =  fetch(:default_environment)
      execute "PORT=#{default_environment[:PORT]} MONGO_URL=#{default_environment[:MONGO_URL]} ROOT_URL=#{default_environment[:ROOT_URL]} METEOR_SETTINGS=$(cat #{shared_path}/#{default_environment[:METEOR_SETTINGS]}) forever start -a -l #{shared_path}/log/production.log -e #{shared_path}/log/error.log --pidFile #{shared_path}/tmp/pids/app.pid #{current_path}/bundle/main.js"
    end
  end


  desc 'Restart application'
  task :restart do
    on roles(:app), in: :sequence, wait: 5 do
      default_environment =  fetch(:default_environment)
      execute "forever stop #{current_path}/bundle/main.js || true"
      execute "PORT=#{default_environment[:PORT]} MONGO_URL=#{default_environment[:MONGO_URL]} ROOT_URL=#{default_environment[:ROOT_URL]} METEOR_SETTINGS=$(cat #{shared_path}/#{default_environment[:METEOR_SETTINGS]}) forever start -a -l #{shared_path}/log/production.log -e #{shared_path}/log/error.log --pidFile #{shared_path}/tmp/pids/app.pid #{current_path}/bundle/main.js"
    end
  end

  desc 'Restart application'
  task :stop do
    on roles(:app), in: :sequence, wait: 5 do
      execute "forever stop #{current_path}/bundle/main.js || true"
    end
  end

  desc "Build bundle app"
  task :npm_install do
    on roles(:app) do
      execute "cd #{current_path}/bundle/programs/server; npm install --production;"
    end
  end

  before :restart, 'deploy:npm_install'

end
before 'deploy:updating', 'deploy:upload_tarball'
after 'deploy:finished', 'deploy:update_monit_files'

We also need config host, user, key file in config/staging.rb / config/production.rb

cap staging deploy:check

SSH to server

cd <deploy_path>
mkdir repo
mkdir shared/tmp/pids
mkdir shared/log

Step 4: Config monit

  1. sudo nano /etc/monit/conf.d/app_name.conf
check host <host_name> address <host_ip>
  start program = "/bin/su -c '<path_to_share_deploy_forder>/<app_name>_start.sh' -s /bin/sh <deploy_user>"
  stop program  = "/bin/su -c '<path_to_share_deploy_forder>/<app_name>__stop.sh' -s /bin/sh <deploy_user>"
  if failed host localhost port <app_port> protocol HTTP request / with timeout 15 seconds then restart
  1. sudo monit restart

Step 5: Deployment

  1. cap staging deploy:check
  2. cap staging deploy

Step 6: Change deploy folder to untrack from Meteor

  1. Change name config folder to .cap
mv config .cap
  1. Edit cap file configuration paths
# default deploy_config_path is 'config/deploy.rb'
set :deploy_config_path, '.cap/deploy.rb'
# default stage_config_path is 'config/deploy'
set :stage_config_path, '.cap/deploy'

# Load DSL and set up stages
require "capistrano/setup"

# Include default deployment tasks
require "capistrano/deploy"

# Include tasks from other gems included in your Gemfile
#
# For documentation on these, see for example:
#
#   https://github.com/capistrano/rvm
#   https://github.com/capistrano/rbenv
#   https://github.com/capistrano/chruby
#   https://github.com/capistrano/bundler
#   https://github.com/capistrano/rails
#   https://github.com/capistrano/passenger
#
# require 'capistrano/rvm'
# require 'capistrano/rbenv'
# require 'capistrano/chruby'
# require 'capistrano/bundler'
# require 'capistrano/rails/assets'
# require 'capistrano/rails/migrations'
# require 'capistrano/passenger'


# default tasks path is `lib/capistrano/tasks/*.rake`
# (note that you can also change the file extensions)
# Load custom tasks from `lib/capistrano/tasks` if you have any defined
Dir.glob('.cap/tasks/*.rb').each { |r| import r }

Step 7: Check all work popertly

  1. Open web browser go <server_host>:
  2. Drink a coffee.

Step 8: Deploy mutiple apps(different meteor versions) to a server

COMMING SOON

Good Luck!!!

@minhtri9111199
Copy link

Thanks pro for this useful article.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment