February 21, 2015

Deploying with Fabric

Dynamic applications require a better deployment process. Let's use Python's Fabric to handle deployment over SSH.

This SSH task runner can handle running commands locally and remotely to make a killer deployment process.### Setup I have a PHP (laravel) application to use, running on a local vagrant server.

We have a:

  1. Vagrant Server
  2. "Production" remote server

The production server has the deployed static site from previous videos.

A new application to deploy is on GitHub. This is a PHP application based on the framework Laravel. We want:

  1. Vagrant server to SSH into production server
  2. Production server updates from GitHub - it needs to connect to GitHub

Local Vagrant server has a ~/.ssh/config file setup (along with ssh keys) to connect to the remote server:

Host deploy-ex
    User deployer
    IdentityFile ~/.ssh/id_deployex
    IdentitiesOnly yes

The remote server user deployer has the id_deployex.pub key in the ~/.ssh/authorized_keys file.

The remote server contains a public/private key pair. The public key is copied into GitHub as a deploy key for our application repository. This lets our server push/pull to/from our application GitHub repository.

Install Fabric and Dependencies

sudo apt-get install -y python-pip python-dev
sudo pip install virtualenv

Create a Python virtual environment to run/install Fabric, named "dep-env".

cd /vagrant
virtualenv dep-env
cd dep-env
source bin/activate

Install fabric into this environment:

pip install fabric

Create the fab file to define our tasks:

cd /vagrant/application
vim fabfile.py

Here is the fabfile.py file:

from __future__ import with_statement
from fabric.api import local, env, settings, abort, run, cd
from fabric.contrib.console import confirm
import time

env.use_ssh_config = True
env.hosts = ['deploy-ex']
# env.hosts = ['']
# env.user = 'deployer'
# env.key_filename = '~/.ssh/id_deployex'

timestamp="release_%s" % int(time.time() * 1000)

def deploy():

def fetch_repo():
    with cd(code_dir):
        with settings(warn_only=True):
            run("mkdir releases")
    with cd("%s/releases" % code_dir):
        run("git clone %s %s" % (repo, timestamp))

def run_composer():
    with cd("%s/releases/%s" % (code_dir, timestamp)):
        run("composer install --prefer-dist")

def update_permissions():
    with cd("%s/releases/%s" % (code_dir, timestamp)):
        run("chgrp -R www-data .")
        run("chmod -R ug+rwx .")

def update_symlinks():
    with cd(code_dir):
        run("ln -nfs %s %s" % (code_dir+'/releases/'+timestamp, app_dir))
        run("chgrp -h www-data %s" % app_dir)

We include needed Python libraries, and enviroment/variables.

The env.use_ssh_config directive will tell Python to use the ~/.ssh/config file in conjunction with the env.hosts variable (which is an array of hosts).

Each function here is a Fabric task we can run. The deploy task (function) will use each of the other functions in order to deploy the site.

The with settings(warn_only=True): tells Fabric to warn on an error, but don't stop execution. We want this when it creates the directory releases, which will already exist after the first run of this task.

This deployment will clone the repository newly on each deployment. It will then switch out the newly pulled repository with the older site files. It rotates the old files out and the new files in using Symlinks.

We can run this using the fab command:

# From our local computer
# to run the deploy task from the fabfile
# found in this current directory
fab deploy

Test Deploying Changes

After our initial deployment, we test add a route to the Laravel application in the app/routes.php file.

# Add this route
Route::get('/test', function()
    Return Request::header();

This shows all the headers of the HTTP request made to this URL. We can see it work locally, but then we can simply deploy this new update:

# Update Git repository
git commit --all
git commit -m "new route shows http request headers"
git push origin master

# Deploy using fabric:
fab deploy


If you're curious, this is the Nginx configuration used to host the PHP application:

server {
    listen 80;

    root /var/www/application/public;

    index index.html index.htm index.php;

    # Make site accessible from ...

    access_log /var/log/nginx/app.com-access.log;
    error_log  /var/log/nginx/app.com-error.log error;

    charset utf-8;

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

    location = /favicon.ico { log_not_found off; access_log off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    error_page 404 /index.php;

    # pass the PHP scripts to php5-fpm
    # Note: .php$ is susceptible to file upload attacks
    # Consider using: "location ~ ^/(index|app|app_dev|config).php(/|$) {"
    location ~ .php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+.php)(/.+)$;

        # With php5-fpm:
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param LARA_ENV production; # Environment variable for Laravel

    # Deny .htaccess file access
    location ~ /\.ht {
        deny all;

All Topics