It's common practice to work locally on a project and push revisions to a production server, but the step that people often skip is the staging server. A staging server is a mix between production and development; you get to test your app as if it were in production. Let's review some of the issues that you'll have to consider, as well as the steps needed to replicate a production Platform as a Service (PAAS).
It has happened more than once: I push a revision of my app into production, only to find a problem after it's public. These issues can be as simple as forgetting to add images to your repository, or it might be as large as changing a database's structure locally and forgetting to update the production database. Problems happen to everyone, especially when you have rushed deadlines. In these situations, it's a smart idea to setup a staging environment. The idea is to have a server that closely replicates the production environment, in order to test your app prior to publication.
Staging environments not only catch human errors, but also software-related issues.
You can find and fix these issues because the staging area has the same software as your production environment. This is in stark contrast to your local machine, where you may have different versions of software installed (e.g. PHP 5.3 vs PHP 5.4) or even different features. I've pushed code that contained calls to file_get_contents
only to find that the production server didn't support that function.
So how do you go about setting a staging server up? Well, the first step is a little reconnaissance.
It's All in the Details (Mostly)
Problems happen to everyone, especially when you have rushed deadlines.
Creating a staging environment is specific to your production environment. There is no magic solution that works in every situation. But most cases follow a similar pattern, and I'll cover all the key points as we go along.
It's fair to assume that most people deploy their apps with some kind of versioning tool (like GIT). On the odd chance that you're working on an old project that still uses FTP, sites like ftploy.com or deployHQ.com can act as a buffer between GIT and your server. Jeffrey Way put together an informative video detailing how to set that up.
Besides GIT, you have to think about the languages, software, and "special" features your production servers offer. I use a PHP-based PAAS, named Fortrabbit, because they offer up-to-date PHP, Apache, and GIT support. They also provide the ability to add a keyword in your GIT commit message that triggers Composer to install your project's dependencies.
This is the system I will setup in the rest of this article. Its standard feature-set, as well as the special composer feature, make Fortrabbit perfect for a wide variety of hosts. Just remember: this is not a magic solution, but the steps involved follow the same pattern you'd use to setup a staging environment for most projects. Tailor the process to your specific needs.
So without further ado, let's jump in.
Creating the Server
Creating a staging environment is specific to your production environment.
There are many different operating systems you can run on a server. Fortrabbit runs Debian Squeeze on their servers, and since we are trying to match them, I decided to run it as well.
I'll use Vagrant to set this up. Don't worry if you've never used Vagrant; we will not do anything advanced. Make sure you have VirtualBox and Vagrant installed (Vagrant is a CLI for VirtualBox; so VirtualBox is required).
Vagrant takes a virtual snapshot of an operating system as a base 'box', and you can then create multiple VMs from that image. So first, we need to download the base box for Debian Squeeze. I'm not exactly sure where my copy comes from, so I uploaded it to DropBox for you to download and use. To install, open up a terminal window and type:
vagrant box add debian https://dl.dropbox.com/u/30949096/debian.box
This adds the box to Vagrant with the name, 'debian'. We can now create an instance of this box for our staging area. First, let's create a new folder:
mkdir ~/staging_server cd ~/staging_server
Next, create the Vagrant config file by typing:
vagrant init debian
This creates a config file, named "VagrantFile", which contains all the settings for you server. It looks pretty crowded when you open it, but most of the lines are comments. You only need to uncomment the line that says: config.vm.network :bridged
. Deleting all other comments leaves you with a file that looks like this:
Vagrant::Config.run do |config| config.vm.box = "debian" config.vm.network :bridged end
These options tell Vagrant to create a new virtual machine based on our Debian Squeeze base box. It then sets the network mode to 'bridged'. A VM with a bridged network appears as a new physical machine to your router, so it automatically retrieves its own IP address. This lets you access the machine from any device on your network (possibly outside of your network if you configure your router).
Now, we can launch our VM with the command: "
vagrant up
" (without the quotes).
You should see the output of Vagrant creating your VM. If your computer has multiple NICs connected to the network, Vagrant will prompt you to pick the NIC to bridge.
We'll use SSH to login, but we'll need to use Vagrant's built-in "vagrant ssh
" command to login. According to Vagrant best practices, all boxes should have a user named "vagrant" with the password, for both root and vagrant, "vagrant". The vagrant user is added as a sudo
user who doesn't need to enter a password, so you can directly use sudo
commands.
Let's move on and set up the server's software.
The Software
Fortrabbit's setup includes:
- Apache 2.2
- PHP 5.4
- Composer
Additionally, they use the dotdeb repository to install the bulk of it. For those unfamiliar, dotdeb is a project by Guillaume Plessis that installs the most up-to-date versions of popular web server packages.
We're ready to begin. Ensure that your terminal window is open and logged into the server through SSH. First, add the dotdeb repo to APT (the package manager) by adding a new file to sources.d
directory:
sudo vim /etc/apt/sources.list.d/dotdeb.list
This opens a new file named dotdeb.list
in vim (a text editor). The name is not important because all files in this directory are read into APT. We need to add four lines to this file. If you have never used VIM, just type "i
" to enter insert mode and copy/paste the next four lines:
deb http://packages.dotdeb.org squeeze all deb-src http://packages.dotdeb.org squeeze all deb http://packages.dotdeb.org squeeze-php54 all deb-src http://packages.dotdeb.org squeeze-php54 all
To save, press the esc
key and type :wq
. That is the command to write and quit, which basically means save and exit.
Pressing enter saves the file and returns you to the command line.
We have now added the repos, but we still need to add the signature before we can use them. Type the following to add the GNU key:
curl http://www.dotdeb.org/dotdeb.gpg | sudo apt-key add -
This downloads the dotdeb key and adds it as a signed source. Now update APT to pull in the new package by typing:
sudo apt-get update
This can take a minute or so, but you'll have all the dotdeb packages listed in APT when it finishes. Because of how dotdeb is setup and how APT dependencies are loaded, we can install Apache and PHP at the same time by typing:
sudo apt-get install php5
With this single line, APT installs and configures Apache2 and PHP5. If you use the Fortrabbit's memcache add-on, then you can install it with:
sudo apt-get install memcached
But I'm not going to go into memcache in our example in this article.
Now we need to install the extensions that Fortrabbit uses. Run the following command:
sudo apt-get install php5-xdebug php5-tidy php5-sqlite php5-redis php5-pgsql \ php5-mysqlnd php5-memcache php5-memcached php5-mcrypt php5-imagick php5-http \ php5-gmp php5-gd php5-curl php5-apc php5-intl
The last thing we need to install is Composer. I am going to install it globally because we will use it in a few different locations. The commands to globally install Composer are:
curl -s https://getcomposer.org/installer | php sudo mv composer.phar /usr/local/bin/composer
The first command downloads and executes the installer; the second command moves Composer to the bin folder so that we can use it without declaring the path. Let's move on to the configuration.
Setting Up Apache
Chances are good that you may be working on more than one project. If this is the case, having a staging server for each project creates a lot of overhead. To allow for multiple sites on a single server, we need to add name-based virtual hosts to Apache, and should separate directories and repos for each project.
Let's begin with a virtual host.
I am going to continue using vim, but know that if you want to work in your own programs, you can either copy and paste or save it in the
staging_server
folder you created on your computer.
That folder is shared to your VM, and you can access the files in the vagrant
root directory. You can then use: sudo cp /vagrant/file newfile
or sudo mv /vagrant/filee newfile
to copy or move the files, respectively.
To create a new virtual host, we need to create a file in the /etc/apache2/sites-available/
directory. To do this with VIM, type the following:
sudo vim /etc/apache2/sites-available/demo.site
Inside, enter the following (remember press "i
" for insert mode):
<VirtualHost *:80> ServerAdmin [email protected] ServerName demo.dev DocumentRoot /var/www/demo <Directory /var/www/demo> Options Indexes FollowSymLinks MultiViews AllowOverride ALL Order allow,deny allow from all </Directory> ErrorLog ${APACHE_LOG_DIR}/demo.log LogLevel debug </VirtualHost>
The first line declares a virtual host that listens for requests on any IP at port 80. Next, we set the server's admin email and the server name. The email is for error reporting purposes, and the server name option tells Apache when to read this virtual host. Regular virtual hosts work off of IPs. For example, each vhost listens on a different IP; that's how Apache differentiates them.
Since you probably only have one IP, we can use name-based Virtual hosts, allowing you to provide a different name on the same IP.
In our example, any requests directed at demo.dev are picked up by this virtual host.
The next line sets the document's root folder. This is where Apache retrieves files for this vhost. The statements inside the Directory
directive sets permissions for this vhost. I won't go into too much detail, but we first set the directories Apache options, then we set what can be overridden in an .htaccess file, and finally we set who can access the site (everyone can in our case).
The last two lines tell Apache what to name the log file and what to write to the log. Our log file is called demo.log in the Apache log folder located at /var/log/apache2/
on this VM.
To enable this vhost, type the following:
sudo a2ensite demo.site
This creates a symlink between the file in the sites-available folder to a file in the sites-enabled folder. After running this command, you're prompted to restart Apache. You'll get an error if you try to restart Apache because we haven't created the site's directory. This is easily fixed buy creating the demo
folder that we referenced in the vhost file:
sudo mkdir /var/www/demo
We don't want the root user to own the folder, so change it to the vagrant user with the chown
command:
sudo chown -R vagrant:vagrant /var/www/demo
Now restart Apache:
sudo service apache2 restart</code>
Our new site should now be fully functional. Our next step is to setup GIT.
Following up With Some GIT Magic
Make sure that you are in the home directory by typing cd ~
. Create a new folder for the repo: mkdir demo.git
, enter the folder, and initialize a new, bare GIT repo:
cd demo.git git init --bare
A bare repo is essentially a standard repo without a working directory. If you want to learn more about GIT, check out Andrew Burgess' video series.
We now need the ability to push code to the site's folder, and there are many ways to accomplish this. But I like to make things look as close as possible to the service I am emulating. Here is a picture of fortrabbit's git process taken from their site:
You can see the push process goes through three steps. It displays an update message when it connects, and it deploys the site into the directory. The final step installs everything if the commit message contains the keywords "[trigger:composer]". Then after these three steps finish, you are shown the ">> All Done
Before we create the hooks, I want to talk about colors.
I do most of my work in the terminal, and more often than not, terminal apps leave everything the same color. Adding different colors to your app not only increases readability, but it also increases like-ability. So too better spread "color practices" in terminal apps, I will take a moment to discuss how they work.
Terminal Colors
Terminals come with sixteen ANSI colors that can be configured and used throughout the terminal. Here is a picture of the iTerm2 settings screen, showing the sixteen color slots:
You can access them in the terminal by typing the escape character, followed by the open square bracket, and then the color's code. You can see in this image that the colors are split into two lines: one labeled "Normal" and the other "Bright". The codes for the normal colors are the numbers 30-37 followed by the letter "m", and the bright colors are from 90-97 again followed by an m. You can test this in your terminal window using echo
. To create the escape character, type ctrl-v
followed by ctrl-[
. You will get a character that looks like ^[
, but it will not work if you just type "^[" (shift-6 and then open square bracket). So in the terminal window type:
echo "^[[31m Hello World ^[[0m"
Again, the first ^[
was not typed out but was created with ctrl-v
and then ctrl-[
. The 0m
character code is the reset code; it removes all formatting. This is because the color codes don't end after the next word, they continue until they receive a different code.
If done correctly, the above code outputs the words "hello world" in red (unless you have that slot set to a different color).
Throughout the rest of the tutorial, I will be adding colors to the commands. Feel free to follow along or omit them; they are not strictly required. But if you do want to use colors, feel free to use my color-helping class. Now, let's go back to writing the hooks.
Creating the Hooks
If you take another look at the Fortrabbit picture, you will see that it displays the message, "Step1: Updating repository" before updating the repo. To do this correctly, we have to put this message in the pre-receive hook, which executes before the repo is updated. In the terminal window type:
vim ~/demo.git/hooks/pre-receive
This opens the hook for editing. Add the following code:
#!/usr/bin/php <?php $yellow = "^[[33m"; $blank = "^[[0m"; echo $yellow . "Step1: Updating repository" . $blank . "\n"; ?>
The first line tells the OS that this is a PHP file and to run it as such. Then we assign a color and the reset sequence to variables. The last step echo
s the line.
The next hook is a little more complicated because it handles the rest of the actions. We'll take it step by step, so save the first hook (:wq) and open the next hook:
vim ~/demo.git/hooks/post-receive
The post-receive hook runs after the repo is updated. We have to first update the site and then check for the Composer trigger. Here is a skeleton of our program, leaving out any new logic:
#!/usr/bin/php <?php $yellow = "^[[33m"; $cyan = "^[[36m"; $blank = "^[[0m"; //print OK for first step echo " -> " . $cyan . "OK" . $blank . "\n"; echo $yellow . "Step2: Deploying" . $blank . "\n"; /*TODO: Deploy to site */ echo " -> " . $cyan . "OK" . $blank . "\n"; /*TODO: Check if commit has trigger */ echo $yellow . ">> All Done <<" . $blank . "\n"; ?>
This is just an outline to work from. The first thing we need to put in is the feature to pull the repo's new version into to the site's directory. Traditionally, I would simply type the following if I was in the folder:
git fetch origin git reset --hard origin/master
You could use something like git pull
, but doing so can cause errors. If a file was directly changed, or if you missed a commit, then git pull
won't either allow you do pull or delete your untracked files.
These are two simple commands, but you'll get an error if you try and run them from the hook.
GIT sets some environment variables prior to running the hooks. You can circumvent this with one of two solutions, and I will show you both.
The first is to override the environment variables, passing in the info directly. The second completely erases the variables. I will use the first option in this example and the second option when we work on the composer trigger. Replace the comment I added above where it says "TODO Deploy to site" with the following:
$git = "git --git-dir=/var/www/demo/.git/ --work-tree=/var/www/demo/"; exec("$git fetch -q origin"); exec("$git reset --hard origin/master");
This overrides the environment variables and calls the aforementioned functions. The added -q
parameter tells GIT to be "quiet", prohibiting GIT from echoing any messages.
Now we need to check for the Composer trigger. Let's break this into two steps. We first check for the trigger, and then we execute Composer. Replace the second TODO comment with the following:
$msg = exec("$git log -n 1 --format=format:%s%b"); if(strpos($msg, "[trigger:composer]") !== false){ echo $yellow . "Step3: Composer Hook" . $blank . "\n"; echo " -> Triggering install - get a " . $cyan . "coffee" . $blank . "\n"; //run composer echo " -> " . $cyan . "OK" . $blank . "\n"; }
The first line retrieves the commit message using the git log
command and passing in a special format to exclude any additional information. Next, we check if the string has the special trigger word, and echo the third step and OK message. We are checking for the Composer keyword, but you could implement multiple keywords for other features like: migrate in Laravel or run unit tests. Add anything to improve your workflow.
The last step is to execute Composer. Composer has two commands: composer install
and composer update
.
The install command does not read the
composer.json
file if it finds acomposer.lock
, and because some people might addcomposer.lock
to their .gitignore file, it's safer to runcomposer update
(it always looks at thecomposer.json
file.
The second issue is that sometimes Composer uses GIT to download packages, and these attempts fail because of the environment variables. So here is a good place to just remove the "GIT_DIR" environment variable. Replace the comment to run Composer with the following:
chdir("/var/www/demo"); putenv("GIT_DIR"); exec("composer update");
This code is straight forward. We move the PHP process to the site's folder, and then remove the GIT_DIR
environment variable so that GIT functions normally. The last line executes Composer.
Tying Up Loose Ends
We now have both hooks setup, but we are not completely ready to start using our server. First, we need to make these hooks executable:
chmod a+x ~/demo.git/hooks/pre-receive chmod a+x ~/demo.git/hooks/post-receive
Next, create a GIT repo in the site's folder. We would get an error if we try to run our hooks because the site's folder is not a GIT repo. To fix this type:
cd /var/www/demo git init git remote add origin /home/vagrant/demo.git
The first two lines create the repo, and then we add the bare repository as this repo's origin.
You should also add your public key to this server's authorized hosts so that you can access the server through GIT without a password. You can copy your public key by typing on your computer (not the VM):
cat ~/.ssh/id_rsa.pub | pbcopy
Then, simply paste it onto the server in the file ~/.ssh/authorized_keys
:
vim ~/.ssh/authorized_keys
Just add it to the file, but leave whatever is already inside.
Next, we need to add the IP of this server to our hosts file. To find the IP, type:
ip -4 -o addr show label eth*
This shows you the IP of all the network devices on this VM. Add the one that connects to your local network. If you can't determine which IP address to use, copy and paste one into your browser. If it connects, then you have the correct IP address.
Take the IP and add it to your computer's hosts file (not the VM's hosts file). The hosts file on a Mac is located at etc/hosts
:
sudo vim /etc/hosts
Next, add the following line:
#Format: IP_address site_name 192.168.0.110 demo.dev
If this works, you will be able to navigate to http://demo.dev
in your browser. While still on your Mac, create a folder and initialize a GIT repo:
mkdir ~/demo cd ~/demo git init echo "Hello World" > index.php git add . git commit -am "added index.php" git remote add staging [email protected]:demo.git git push staging master
If everything went well, you should see a reply from our GIT hooks. Navigating to http://demo.dev
should result in the "Hello World" message displayed in your browser.
Conclusion
So that's how you can create a staging environment that mimics the functionality of a typical PAAS. If you had trouble following along, or you need to setup multiple staging environments, I created a script that completely automates the process. For more information, you can visit stagr.gmanricks.com.
I hope you enjoyed the article. Feel free to ask any questions that you might have in the comments. Thank you for reading.
Comments