This guide aims to get you started running a basic Ruby on Rails application on Ubuntu. It can be used for web applications with small usage and provides a cheap alternative, e.g. €5 a month on Digital Ocean, to services like Heroku which cost much more because of expensive add-ons. The setup includes:
- support for just a single Ruby on Rails web application
- database support (PostgreSQL)
- email support (sendmail)
- background job support
- multiple application processes (Unicorn)
- front-end http server with asset caching (Nginx)
- basic process management setup
Later on I will also be adding instructions for:
- deployment with git hooks
- SSL certificates
- automated backups
- monitoring
We'll get the application running as quickly as possible and then expand on that. The guide assumes basic linux command line knowdledge on things like permissions, package management, symbolic links, job control, editor usage and more.
Additional information on setting up Ubuntu as a server can be found in the Ubuntu Server Guide.
For this demo I've created a droplet on Digital Ocean, you can use whatever vps you like.
Create the droplet and save the ip address:
Hostname: demo_app.example.com
Image: Ubuntu 14.04 x64
Ip address: 93.184.216.119
Login as root:
> ssh [email protected]
change the password:
> passwd
Update & upgrade the system:
> sudo apt-get update && sudo apt-get upgrade
Add a user:
> adduser demo_user
After this I got the following warning:
perl: warning: Setting locale failed.
perl: warning: Please check that your locale settings:
LANGUAGE = (unset),
LC_ALL = (unset),
LC_CTYPE = "nl_NL.UTF-8",
LANG = "en_US.UTF-8"
are supported and installed on your system.
perl: warning: Falling back to the standard locale ("C").
We need to generate & reconfigure the locales to fix this:
> locale-gen nl_NL.UTF-8
> dpkg-reconfigure locales
(VRAAG: dit kan op verschillende manieren, dit zorgt ervoor dat je de sudoers file niet hoeft aan te passen maar is dit wel een net alternatief?) Add the user to the sudo-ers list so we don't need to login as root anymore:
> usermod -a -G sudo demo_user
We need to add the user to ssh and make it a bit more secure. You can update the config at /etc/ssh/sshd_config
.
Update the following line:
PermitRootLogin yes
to:
PermitRootLogin no
Add the new user at the end of the file, save the file and quit:
AllowUsers demo_user
Reload ssh to apply the new settings:
> reload ssh
Let's login with our newly created user. Quit the ssh connection (ctrl-d), add you public key and login as our new user:
> cat ~/.ssh/id_rsa.pub | ssh [email protected] "mkdir ~/.ssh; cat >> ~/.ssh/authorized_keys"
> ssh [email protected]
Ubuntu comes with UFW which stands for Uncomplicated FireWall. It provides as a friendly way to configure the underlaying iptables configuration. We only need to enable ssh, http and https:
> sudo ufw allow ssh
> sudo ufw allow http
> sudo ufw enable
Because we only have one app we also only need just one ruby version. We'll use Postmodern's ruby-install for that. It allows for installing specific ruby versions system wide in the /opt/rubies/
directory.
Building ruby and gems requires some libraries. Let's install them first:
> sudo apt-get install autoconf bison build-essential libssl-dev libyaml-dev libreadline6 libreadline6-dev zlib1g zlib1g-dev git
Then install ruby-install:
> wget -O ruby-install-0.4.3.tar.gz https://github.com/postmodern/ruby-install/archive/v0.4.3.tar.gz
> tar -xzvf ruby-install-0.4.3.tar.gz
> cd ruby-install-0.4.3/
> sudo make install
Install the ruby version you need:
> sudo ruby-install ruby 2.1.2
Create symbolic links for the ruby binaries
> sudo ln -s /opt/rubies/ruby-2.1.2/bin/* /usr/local/bin
Install the Bundler gem:
> sudo gem install bundler
Create symbolic links for the bundler binaries
> sudo ln -s /opt/rubies/ruby-2.1.2/bin/bundle* /usr/local/bin
To install PostgreSQL as your database server:
> sudo apt-get install postgresql
Because we're going to install the pg gem, we have some additional dependencies:
> sudo apt-get install postgresql-server-dev-9.3
(TESTEN: niet meer nodig na install van postgresql-server-dev?) Let's build the gem:
> sudo gem install pg -- --with-pg-config=/usr/bin/pg_config
(VRAAG: wat is eigenlijk best practice op productie servers; md5 of ident?)
Postgres' default authentication is using ident, but we'll use md5 instead, you can update the config at /etc/postgresql/9.3/main/pg_hba.conf
:
# "local" is for Unix domain socket connections only
local all all md5
Restart the database server
> sudo service postgresql restart
Run psql as the postgres user, create the user and database:
> sudo -u postgres psql
postgres=# CREATE USER apps2user WITH PASSWORD 'demo_password';
postgres=# CREATE DATABASE apps2_production OWNER apps2user;
We'll place our app directly in the users home directory. Other possible directories are /var/www
which is common but not entirely correct and /srv
which is for "site-specific data which is served by this system.". But for ease of use we'll stick with the home directory.
Checkout de demo app:
> cd ~ && git clone https://github.com/benvds/demo_app.git
Sprockets needs a javascript runtime, so lets install node:
> sudo apt-get install nodejs
Install gems into vendor:
> bundle install --path vendor
Migrate the database
> RAILS_ENV=production DB_PASSWORD=demo_password bin/rake db:migrate
Test drive the app:
> RAILS_ENV=production DB_PASSWORD=demo_password SECRET_KEY_BASE=demo_secret bin/rails server
Let's see it run (asset's won't work yet):
http://demo_app.example.com:3000
Let's get those assets to work and use Nginx as reverse proxy and for caching assets.
Install Nginx:
> sudo apt-get install nginx
Configure Nginx:
> sudo nano /etc/nginx/sites-available/default
Replace all with:
# /etc/nginx/sites-available/default
upstream webrick {
server 127.0.0.1:3000 fail_timeout=0;
}
server {
listen 80;
server_name apps2.pomodoro.nl;
# Application root, as defined previously
root /home/demo_user/demo_app/public;
location ^~ /assets/ {
gzip_static on;
expires max;
add_header Cache-Control public;
}
try_files $uri/index.html $uri @webrick;
location @webrick{
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://webrick;
}
error_page 500 502 503 504 /500.html;
client_max_body_size 4G;
keepalive_timeout 10;
}
And restart the server:
> sudo service nginx restart
Precompile the assets:
> bin/rake assets:precompile
Lets run the app again:
> RAILS_ENV=production DB_PASSWORD=demo_password SECRET_KEY_BASE=demo_secret bin/rails server
Our app is now available on port 80 and assets are available and cached:
http://demo_app.example.com
When things go wrong configuring nginx you'll see this in the command line:
Restarting nginx nginx [fail]
This means there is something wrong with the configuration. You can check the log for details at /var/log/nginx/error.log
.
Webrick is not a production server so we're going to use unicorn for this. It's a popular app server mainly because of it's unixy approach. It takes some more time to setup compared to an all-in solution like Phusion Passenger but people seem to get better results with unicorn for single app servers.
The repository comes with an unicorn configuration file at config/unicorn.rb
which is largely based on the default example. Our configuration looks like this:
app_name = "demo_app"
app_root = File.expand_path(File.join(File.dirname(__FILE__), '..'))
# Only necessary when the app dir gets changed on new releases, see:
# http://www.justinappears.com/blog/2-no-downtime-deploys-with-unicorn/
# Unicorn::HttpServer::START_CTX[0] = "#{app_root}/bin/unicorn"
#
# before_exec do |server|
# ENV["BUNDLE_GEMFILE"] = "#{app_root}/Gemfile"
# end
working_directory app_root
pid "#{app_root}/tmp/pids/unicorn.pid"
stderr_path "#{app_root}/log/unicorn.log"
stdout_path "#{app_root}/log/unicorn.log"
listen "/tmp/unicorn.#{app_name}.sock"
worker_processes Integer(ENV["WEB_CONCURRENCY"] || 3)
timeout Integer(ENV.fetch('WEB_TIMEOUT', 20))
preload_app true
before_fork do |server, worker|
defined?(ActiveRecord::Base) and
ActiveRecord::Base.connection.disconnect!
# This allows a new master process to incrementally
# phase out the old master process with SIGTTOU to avoid a
# thundering herd (especially in the "preload_app false" case)
# when doing a transparent upgrade. The last worker spawned
# will then kill off the old master process with a SIGQUIT.
old_pid = "#{server.config[:pid]}.oldbin"
if old_pid != server.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
end
end
end
after_fork do |server, worker|
defined?(ActiveRecord::Base) and
ActiveRecord::Base.establish_connection
end
Kill the rails server (ctrl-c) and let's use unicorn instead:
> RAILS_ENV=production DB_PASSWORD=demo_password SECRET_KEY_BASE=demo_secret bundle exec unicorn -p 5000 -c config/unicorn.rb
Let's put it in the background for now by stopping the process (ctrl-z) and resuming it in the background:
> bg $1
Update our nginx config at etc/nginx/sites-available/default
, with:
# /etc/nginx/sites-available/default
upstream unicorn {
server unix:/tmp/unicorn.demo_app.sock fail_timeout=0;
}
server {
listen 80;
server_name apps2.pomodoro.nl;
# Application root, as defined previously
root /home/demo_user/demo_app/public;
location ^~ /assets/ {
gzip_static on;
expires max;
add_header Cache-Control public;
}
try_files $uri/index.html $uri @unicorn;
location @unicorn{
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://unicorn;
}
error_page 500 502 503 504 /500.html;
client_max_body_size 4G;
keepalive_timeout 10;
}
Update the upstream server is now using a socket to connect to the unicorn master process. Restart ngnix:
> sudo service nginx restart
When things go wrong configuring unicorn you can find the log file at log/unicorn.log
:
If you want it's possible to optimize your unicorn workers.
(VRAAG: sendmail installeren lijkt voldoende te zijn maar online lees ik bijna overall dat ik postfix moet installeren, waarom?)
> sudo apt-get install sendmail
Were using sendmail but install postfix. The installation process will ask you two questions. Choose "internet site" (default) and set your domain name.
> sudo apt-get install postfix
The production environment needs te be configured to use sendmail. This is done in config/environments/production.rb
:
config.action_mailer.delivery_method = :sendmail
Sending emails shouldn't delay an app, therefore should but put in a background job. The demo app is using Collective Idea's Delayed Job. It can be run with:
> RAILS_ENV=production DB_PASSWORD=demo_password SECRET_KEY_BASE=demo_secret bin/rake jobs:work
Instead of starting all these processes and putting them in the background manually you'll need somethings better. Also setting environment variables through the command line is a hassle, we're gonna automate this with some process management.
We'll use Foreman for generating service scripts for our application server and background worker. It's uses a .env file for the environment variables en generates scripts for Ubuntu's Upstart which will make sure services restart whenever one crashes or the system reboots.
Foreman should be installed system wide so:
> sudo gem install foreman
And create symbolic link for the foreman binary
> sudo ln -s /opt/rubies/ruby-2.1.2/bin/foreman /usr/local/bin
Let's start by setting our environment variables in the .env
file:
RAILS_ENV=production
DB_PASSWORD=demo_password
SECRET_KEY_BASE=
Let's generate a secret key base:
> bin/rake secret
Copy the generated secret and add it as SECRET_KEY_BASE to the .env
file.
Our processes are defined in the Procfile:
web: bundle exec unicorn -c ./config/unicorn.rb
worker: bundle exec rake jobs:work
Check if it works by running:
> foreman start
If so, export the procfile to upstart:
> sudo foreman export upstart /etc/init -a demo_app -u demo_user
And start it up:
> sudo service demo_app start