has_many :codes

Vito Botta's journal with tips and walkthroughs on web technologies and digital life

Setting up a Ubuntu server for Ruby and PHP apps

There are several guides on the Internet on setting up a Ubuntu server, but I thought I'd add here some notes on how to set up a server capable of running both Ruby and PHP apps at the same time. Ubuntu's latest Long Term Support (LTS) release is 14.04, so this guide will be based on that release.

I will assume you already have a a server with the basic Ubuntu Server Edition installed - be it a dedicated server or a VPS from your provider of choice - with just SSH access enabled and nothing else. We'll be bootstrapping the basic system and install all the dependencies required for running Ruby and PHP apps; I usually use Nginx as web server, so we'll be also using Phusion Passenger as application server for Ruby and fastcgi for PHP to make things easier.

First steps

Before anything else, it's a good idea to update the system with the latest updates available. So SSH into the new server with the IP and credentials you've been given and -recommended- start a screen session with

screen -S <session-name>  

Now change the root password with

passwd  

then open /root/.ssh/authorized_keys with and editor and ensure no SSH keys have already been added other than yours; if you see any keys, I recommend you comment them out and uncomment them only if you ever need to ask your provider for support.

Done that, as usual run:

apt-get update  
apt-get upgrade -y  

to update the system.

Next, edit /etc/hostname with vi or any other editor and change the hostname with the hostname you will be using to connect to this server; also edit /etc/hosts and add the correct hostname in there as well. Reboot:

reboot now  

SSH access

It's a good idea to use a port other than the default one for SSH access, and a user other than root. In this guide, we'll be:

  • using the example port 17239
  • disabling the root access and enabling access for the user deploy (only) instead
  • switching from password authentication to public key authentication for good measure.

Of course you can choose whichever port and username you wish.

For convenience, on your client computer (that is, the computer you will be connecting to the server from) edit ~/.ssh.config and add the following content:

Host my-server (or whichever name you prefer)  
  Hostname <the ip address of the server>
  Port 22
  User root

So you can more easily SSH into the new server with just

ssh my-server  

As you can see for now we are still using the default port and user until the SSH configuration is updated.

Unless your public key has already been added to /root/.ssh/authorized_keys during the provisioning of the new server, still on the client machine run

ssh-copy-id <hostname or ip of the server>  

to copy your public key over. You should now be able to SSH into your server without password.

Back on the server, it's time to setup the user which you will be using to SSH into the server instead of root:

adduser deploy  

Edit /etc/sudoers and add:

deploy  ALL=(ALL:ALL) ALL  

On the client, ensure you can SSH into the server as deploy using your key:

ssh-copy-id deploy@my-server  

You should now be able to login as deploy without password.

Now edit /etc/ssh/sshd_config and change settings as follows:

Port 17239  
PermitRootLogin no  
PasswordAuthentication no  
UseDNS no  
AllowUsers deploy  

This will:

  • change the port
  • disable root login
  • disable password authentication so we are forced to use public key authentication
  • disable DNS lookups so to speed up logins
  • only allow the user deploy to SSH into the system

Restart SSH server with:

service ssh restart  

Keep the current session open just in case for now. On the client, open again ~/.ssh/config and update the configuration of the server with the new port and user:

Host my-server (or whichever name you prefer)  
  Hostname <the ip address of the server>
  Port 17239
  User deploy

Now if you run

ssh my-server  

you should be in as deploy without password. You should no longer be able to login as root though; to test run:

ssh root@my-server date  

you should see an error:

Permission denied (publickey).  

Firewall

Now that SSH access is sorted, it's time to configure the firewall to lock down the server so that only the services we want (such as ssh, http/https and mail) are allowed. Edit the file /etc/iptables.rules and paste the following:

# Generated by iptables-save v1.4.4 on Sat Oct 16 00:10:15 2010
*filter
:INPUT ACCEPT
:FORWARD ACCEPT
:OUTPUT ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -d 127.0.0.0/8 ! -i lo -j DROP
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
-A INPUT -p tcp -m tcp --dport 80 -j ACCEPT
-A INPUT -p tcp -m tcp --dport 443 -j ACCEPT
-A INPUT -p tcp -m tcp --dport 587 -j ACCEPT
-A INPUT -p tcp -m state --state NEW -m tcp --dport 17239 -j ACCEPT
-A INPUT -m limit --limit 5/min -j LOG --log-prefix "iptables [Positive[False?]: " --log-level 7
-A INPUT -p icmp -m icmp --icmp-type 8 -j ACCEPT
-A INPUT -j LOG
-A INPUT -j REJECT --reject-with icmp-port-unreachable
-A OUTPUT -j ACCEPT
COMMIT  
# Completed on Sat Oct 16 00:10:15 2010
# Generated by iptables-save v1.4.4 on Sat Jun 12 23:55:23 2010
*mangle
:PREROUTING ACCEPT
:INPUT ACCEPT
:FORWARD ACCEPT
:OUTPUT ACCEPT
:POSTROUTING ACCEPT
COMMIT  
# Completed on Sat Jun 12 23:55:23 2010
# Generated by iptables-save v1.4.4 on Sat Jun 12 23:55:23 2010
*nat
:PREROUTING ACCEPT
-A PREROUTING -p tcp --dport 25 -j REDIRECT --to-port 587
:POSTROUTING ACCEPT
:OUTPUT ACCEPT
COMMIT  
# Completed on Sat Jun 12 23:55:23 2010

It's a basic configuration I have been using for some years. It locks all incoming traffic apart from SSH access, web traffic (since we'll be hosting Ruby and PHP apps) and mail. Of course, make sure you specify the SSH port you've chosen here if other than 17239 as in the example.

To apply the setting now, run:

iptables-restore < /etc/iptables.rules  

and verify with

iptables -L  

You should see the following output:

Chain INPUT (policy ACCEPT)  
target     prot opt source               destination  
ACCEPT     all  --  anywhere             anywhere  
DROP       all  --  anywhere             loopback/8  
ACCEPT     all  --  anywhere             anywhere             state RELATED,ESTABLISHED  
ACCEPT     tcp  --  anywhere             anywhere             tcp dpt:http  
ACCEPT     tcp  --  anywhere             anywhere             tcp dpt:https  
ACCEPT     tcp  --  anywhere             anywhere             tcp dpt:submission  
ACCEPT     tcp  --  anywhere             anywhere             state NEW tcp dpt:17239  
LOG        all  --  anywhere             anywhere             limit: avg 5/min burst 5 LOG level debug prefix "iptables [Positive[False?]: "  
ACCEPT     icmp --  anywhere             anywhere             icmp echo-request  
LOG        all  --  anywhere             anywhere             LOG level warning  
REJECT     all  --  anywhere             anywhere             reject-with icmp-port-unreachable

Chain FORWARD (policy ACCEPT)  
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)  
target     prot opt source               destination  
ACCEPT     all  --  anywhere             anywhere  

Now if you reboot the server, these settings will be lost, so you need to persist them in either of two ways:

1) open /etc/network/interfaces and add, in the eth0 section, the following line:

post-up iptables-restore < /etc/iptables.rules  

So the file should now look similar to the following:

auto eth0  
iface eth0 inet static  
        address ...
        netmask ...
        gateway ...
        up ip addr add 10.16.0.5/16 dev eth0
        dns-nameservers 8.8.8.8 8.8.4.4
        post-up iptables-restore < /etc/iptables.rules

OR,

2) Run

apt-get install iptables-persistent  

Either way, reboot now and verify again with iptables -L that the settings are persisted.

ZSH shell, editor (optional)

If you like me prefer ZSH over BASH and use VIM as editor, first install ZSH with:

apt-get install zsh git-core  
curl -L https://raw.github.com/robbyrussell/oh-my-zsh/master/tools/install.sh | sh  
ln -s ~/dot-files/excid3.zsh-theme ~/.oh-my-zsh/themes  

Then you may want to use my VIM configuration so to have a nicer editor environment:

cd; git clone https://github.com/vitobotta/dot-files.git  
cd dot-files; ./setup.sh  

I'd repeat the above commands for both the deploy user and root (as usual you can use sudo -i for example to login as root). Under deploy, you'll need to additionally run:

chsh  

and specify /usr/bin/zsh as your shell.

Dependencies for Ruby apps

You'll need to install the various dependencies required to compile Ruby and install various gems:

apt-get install build-essential curl wget  openssl libssl-dev libreadline-dev libmysqlclient-dev ruby-dev mysql-client ruby-mysql xvfb firefox libsqlite3-dev sqlite3 libxslt1-dev libxml2-dev  

You'll also need to install nodejs for the assets compilation (Rails apps):

apt-get install software-properties-common  
add-apt-repository ppa:chris-lea/node.js  
apt-get update  
apt-get install nodejs  

Next, as deploy:

Ensure the following lines are present in the shell rc files (.zshrc and .zprofile) and reload the shell so the new Ruby can be "found":

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

ruby -v should now output the expected version number, 2.2.4 in the example.

Optionally, you may want to install the rbenv-vars plugin for environment variables support with rbenv:

git clone https://github.com/sstephenson/rbenv-vars.git ~/.rbenv/plugins/rbenv-vars  
chmod +x ~/.rbenv/plugins/rbenv-vars/bin/rbenv-vars  

Dependencies for PHP apps

Install the various packages required for PHP-FPM:

apt-get install php5-fpm php5-mysql php5-curl php5-gd php5-intl php-pear php5-imagick php5-mcrypt php5-memcache php5-memcached php5-ming php5-ps php5-pspell php5-recode php5-snmp php5-sqlite php5-tidy php5-xmlrpc php5-xsl php5-geoip php5-mcrypt php-apc php5-imap  

MySQL

I am assuming here you will be using MySQL - I usually use the Percona distribution. If you plan on using some other database system, skip this section.

First, install the dependencies:

apt-get install curl build-essential flex bison automake autoconf bzr libtool cmake libaio-dev libncurses-dev zlib1g-dev libdbi-perl libnet-daemon-perl libplrpc-perl libaio1  
gpg --keyserver  hkp://keys.gnupg.net --recv-keys 1C4CBDCDCD2EFD2A  
gpg -a --export CD2EFD2A | sudo apt-key add -  

Next edit /etc/apt/sources.list and add the following lines:

deb http://repo.percona.com/apt trusty main  
deb-src http://repo.percona.com/apt trusty main  

Install Percona server:

apt-get update  
apt-get install percona-xtradb-cluster-server-5.5 percona-xtradb-cluster-client-5.5 percona-xtradb-cluster-galera-2.x  

Test that MySQL is running:

mysql -uroot -p  

Getting web apps up and running

First install Nginx with Passenger for Ruby support (also see this:

apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 561F9B9CAC40B2F7  
apt-get install apt-transport-https ca-certificates  

Edit /etc/apt/sources.list.d/passenger.list and add the following:

deb https://oss-binaries.phusionpassenger.com/apt/passenger trusty main  

Update sources:

chown root: /etc/apt/sources.list.d/passenger.list  
chmod 600 /etc/apt/sources.list.d/passenger.list  
apt-get update  

Then install Phusion Passenger for Nginx:

apt-get install nginx-extras passenger  

Edit /etc/nginx/nginx.conf and uncomment the passenger_root and passenger_ruby lines, making sure the latter points to the version of Ruby installed with rbenv, otherwise it will point to the default Ruby version in the system. Make the following changes:

user deploy;  
worker_processes auto;  
pid /run/nginx.pid;

events {  
     use epoll;
     worker_connections 2048;
     multi_accept on;
}

http {  
     sendfile on;
     tcp_nopush on;
     tcp_nodelay on;
     keepalive_timeout 65;
     types_hash_max_size 2048;
     server_tokens off;
     …
     passenger_root /usr/lib/ruby/vendor_ruby/phusion_passenger/locations.ini;
     passenger_ruby /home/deploy/.rbenv/shims/ruby;
     passenger_show_version_in_header off;
}

Restart nginx with

service nginx restart  

Test that nginx works by opening http://the_ip_or_hostname in your browser.

For PHP apps, we will be using fastcgi with unix sockets. Create for each app a profile in /etc/php5/fpm/pool.d/, e.g. /etc/php5/fpm/pool.d/myapp. Use the following template:

[<app name>]
listen = /tmp/<app name>.php.socket  
listen.backlog = -1  
listen.owner = deploy  
listen.group = deploy

; Unix user/group of processes
user = deploy  
group = deploy

; Choose how the process manager will control the number of child processes.
pm = dynamic  
pm.max_children = 75  
pm.start_servers = 10  
pm.min_spare_servers = 5  
pm.max_spare_servers = 20  
pm.max_requests = 500

; Pass environment variables
env[HOSTNAME] = $HOSTNAME  
env[PATH] = /usr/local/bin:/usr/bin:/bin  
env[TMP] = /tmp  
env[TMPDIR] = /tmp  
env[TEMP] = /tmp

; host-specific php ini settings here
; php_admin_value[open_basedir] = /var/www/DOMAINNAME/htdocs:/tmp

To allow communication between Nginx and PHP-FPM via fastcgi, ensure each PHP app's virtual host includes some configuration like the following:

location / {  
     try_files $uri /index.php?$query_string;
}     

location ~ \.php$ {  
     fastcgi_split_path_info ^(.+\.php)(/.+)$;     
     fastcgi_pass unix:/tmp/<app name>.php.socket;    
     fastcgi_index index.php;
     include fastcgi_params;
     fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name;
}

Edit /etc/php5/fpm/php.ini and set cgi.fix_pathinfo to 0. Restart both FPM and Nginx:

service php5-fpm restart  
service nginx restart  

Congrats, you should now be able to run both Ruby and PHP apps.

Backups

There are so many ways to backup a server.... what I usually use on my personal servers is a combination of xtrabackup for MySQL databases and duplicity for file backups.

As root, clone my admin scripts:

cd ~  
git clone https://github.com/vitobotta/admin-scripts.git  
apt-key adv --keyserver keys.gnupg.net --recv-keys 1C4CBDCDCD2EFD2A  

Edit /etc/apt/sources.list and add:

deb http://repo.percona.com/apt trusty main  
deb-src http://repo.percona.com/apt trusty main  

Proceed with the installation of the packages:

apt-get update  
apt-get install duplicity xtrabackup  

Next refer to this previous post for the configuration.

Schedule the backups with crontab -e by adding the following lines:

MAILTO = <your email address>

00 02 * * sun /root/admin-scripts/backup/duplicity.sh full  
00 02 * * mon-sat /root/admin-scripts/backup/duplicity.sh incr  
00 13 * * * /root/admin-scripts/backup/xtrabackup.sh incr  

Mailing

  • install postfix and dovecot with
apt-get install postfix dovecot-common mailutils  
  • run dpkg-reconfigure postfix and set the following:

    • General type of mail configuration -> Internet Site
    • System mail name -> same as the server's hostname
    • Root and postmaster email recipient -> your email address
    • Force synchronous updates on mail queue -> no
    • Local networks -> leave default
    • Mailbox size limit (bytes) -> set 10485760 (10MB) or so, to prevent the default mailbox from growing with no limits
    • Internet protocols to use -> all
  • SMTP authentication: run

postconf -e 'home_mailbox = Maildir/'  
postconf -e 'smtpd_sasl_type = dovecot'  
postconf -e 'smtpd_sasl_path = private/auth'  
postconf -e 'smtpd_sasl_local_domain ='  
postconf -e 'smtpd_sasl_security_options = noanonymous'  
postconf -e 'broken_sasl_auth_clients = yes'  
postconf -e 'smtpd_sasl_auth_enable = yes'  
postconf -e 'smtpd_recipient_restrictions = permit_sasl_authenticated,permit_mynetworks,reject_unauth_destination'  
  • TLS encryption: run
mkdir /etc/postfix/certificate && cd /etc/postfix/certificate  
openssl genrsa -des3 -out server.key 2048  
openssl rsa -in server.key -out server.key.insecure  
mv server.key server.key.secure  
mv server.key.insecure server.key  
openssl req -new -key server.key -out server.csr  
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt

postconf -e 'smtp_tls_security_level = may'  
postconf -e 'smtpd_tls_security_level = may'  
postconf -e 'smtp_tls_note_starttls_offer = yes'  
postconf -e 'smtpd_tls_key_file = /etc/postfix/certificate/server.key'  
postconf -e 'smtpd_tls_cert_file = /etc/postfix/certificate/server.crt'  
postconf -e 'smtpd_tls_loglevel = 1'  
postconf -e 'smtpd_tls_received_header = yes'  
postconf -e 'myhostname = <hostname>'  
  • SASL

    • edit /etc/dovecot/conf.d/10-master.conf, and uncomment the following lines so that they look as follows (first line is a comment so leave it…commented out):

              # Postfix smtp-auth
              unix_listener /var/spool/postfix/private/auth {
                   mode = 0666     
              }
      
  • edit /etc/dovecot/conf.d/10-auth.conf and change the setting auth_mechanisms to "plain login"
  • edit /etc/postfix/master.cf and a) comment out smtp, b) uncomment submission
  • restart postfix: service postfix restart
  • restart dovecot: service dovecot restart
  • verify that all looks good
root@nl:/etc/postfix/certificate# telnet localhost  587  
Trying 127.0.0.1...  
Connected to localhost.  
Escape character is '^]'.  
220 <hostname> ESMTP Postfix (Ubuntu)  
ehlo <hostname>  
250-<hostname>  
250-PIPELINING  
250-SIZE 10240000  
250-VRFY  
250-ETRN  
250-STARTTLS  
250-AUTH PLAIN LOGIN  
250-AUTH=PLAIN LOGIN  
250-ENHANCEDSTATUSCODES  
250-8BITMIME  
250 DSN  

Test email sending:

echo "" | mail -s "test" <your email address>  

There's a lot more that could be done, but this should get you started. Let me know in the comments if you run into any issues.

Author image
About Vito Botta
Espoo, Finland Website
I am a passionate developer based in Espoo, Finland, where I work as Lead Software Engineer for OnApp. My roles as architect, coder and technology enthusiast overlap each other here on this web log.