Objectives

This is Part 3 of a series called Hello, World: Blog.

By this point we’ve created a web server to host site content, and code that can generate site content locally. But we still don’t have a live website. To get there, we need a way to move our local code onto the server. We’ll accomplish this by creating a deployment process.

In this post, you’ll:

  • Learn about deployment.
  • Use Git to push site code to the server.
  • Use GitHooks to build your site content.

Prerequisites

What is Deployment?

Deployment is a catch-all term for a process that makes software (an application, a website, etc.) available to users. A blog can be considered deployed when it is viewable in a browser using our server’s IP address or domain.

For our blog, we could manually copy the contents of the _site directory generated by Jekyll into the /var/www/html directory on our server. That’s an example of a simple, manual deployment process. Manually copying files over SSH can be tedious, though. Let’s come up with something fancier and more automated.

This is the deployment workflow we’re after:

  • Draft posts locally.
  • Push local copy of site code to the server.
  • On the server, use the latest code to build the site content.
  • Host the site content on nginx.

Notice that we’re pushing the site code (the unbuilt Markdown) and building it on the server, instead of building it locally and pushing just the content. This allows us to automate the build step on the server, so we don’t have to execute it manually each time we want to update our blog.

Server - Install Build Dependencies

In the previous post we used jekyll build to locally build our site. In our new deployment process, we need to build the code on the server instead. This means we need to install some dependencies on the server.

Log into the server as your non-root user (as before, when running these commands, IP_ADDRESS should be replaced with the IP of your server).

$ ssh bannmoore@IP_ADDRESS

Then, install Ruby and a few other dependencies using apt-get.

Note: Using sudo with apt-get installs the dependencies for all users. This is important for a future step.

bannmoore@blog:~$ sudo apt-get update
bannmoore@blog:~$ sudo apt-get install ruby ruby-dev make build-essential

Next, we’ll need the .bashrc file. In Ubuntu (and other Unix-style systems), this file is loaded automatically when you start a new interactive terminal. We need to add some Ruby-specific configuration to this file so that our system knows where to look for gem installations.

Open .bashrc in Nano.

bannmoore@blog:~$ nano ~/.bashrc

Add these lines to the bottom of the file:

# Ruby exports

export GEM_HOME=$HOME/gems
export PATH=$HOME/gems/bin:$PATH

Close nano (CTRL-X Y ENTER), and reload the .bashrc file. This ensures our new configuration is loaded into the current terminal session.

bannmoore@blog:~$ source ~/.bashrc

Finally, use the Ruby gem command to install Jekyll and Bundler. Like with apt-get, we’ll use sudo to ensure that the gems are available to all users.

bannmoore@blog:~$ sudo gem install jekyll bundler

Server - Set up Git

In order to implement our deployment process, we’re going to use Git. Git is a “version control” system; this means that a Git codebase (or “repository”) contains the code as well as a full history of all changes made to that code. More importantly for us, Git provides distributed version control, which means that the full repository (history included) can exist on multiple machines simultaneously.

We’ll take advantage of this to create two repositories: one local, and one on the server. Then we’ll use Git’s command-line interface to “push” our local changes.

Create a Git User

If you’ve been following along, your Ubuntu server should have two users: root and non-root. The root user was created by default and shouldn’t be used directly. The non-root user has sudo privileges.

In this section, we’ll create a third user whose sole responsibility is Git. This is a good practice that follow the principle of lease privilege.

If you haven’t already, log into the server as your non-root user:

$ ssh bannmoore@IP_ADDRESS

Create a new user with username “git”. This process will be very similar to when we created our non-root user in the first part.

bannmoore@blog:~$ sudo adduser git

For the rest of this tutorial, we’ll refer to this as our “git” user.

Prepare the Web Root and Grant Permissions

Our site is currently serving a static page generated by Nginx when it was first installed. Nginx serves content from the /var/www/html directory.

We’ll need to remove that file so we can populate the directory with our Jekyll-generated site content. Here we use ls to figure out the file’s name, then use rm to remove it.

bannmoore@blog:~$ ls /var/www/html
index.nginx-debian.html
bannmoore@blog:~$ sudo rm /var/www/html/index.nginx-debian.html

Since the git user doesn’t have sudo privileges, it doesn’t have permission to edit the Nginx directory. Use chown to grant edit permission to /var/www/html.

sudo chown git:www-data /var/www/html

Create a Git Repository

While SSH-ed into the server, you can log into another user without closing the connection. Asthe non-root user, use the su command to open a subterminal as the git user.

bannmoore@blog:~$ su - git

Create a new folder in the home directory. In this tutorial I’ll call mine blog.git, though you can use any name you want. If you use another name, be sure to replace blog.git with that name in future commands.

git@blog:~$ mkdir ~/blog.git

Next we’ll initialize an empty Git repository inside blog.git using git init --bare.

git@blog:~$ cd ~/blog.git
git@blog:~/blog.git$ git init --bare
Initialized empty Git repository in /home/git/blog.git/

If you look at the contents of blog.git, you’ll see several new folders:

git@blog:~/blog.git$ ls
branches  config  description  HEAD  hooks  info  objects  refs

This empty repository will be the receiving point (also known as a “remote”) for the repository on our local machine, which we’ll get to shortly. For now, cd back to the home directory.

git@blog:~/blog.git$ cd ~
git@blog:~$ 

Create a Post-Receive Hook

Now we have a repository for our site code on the server: blog.git. But in order for Nginx to host our website, we need to build the site content and make sure it’s in /var/www/html. To accomplish this, we’re going to tell Git to run a specific script whenever new code is pushed from our local repository.

Git provides several hooks that execute scripts on certain triggers. The Post-Receive hook executes when new code is pushed, which is exactly what we want.

Hook scripts should be placed in the hooks directory of a git repository. Open a file called post-receive in Nano.

git@blog:~$ nano ~/blog.git/hooks/post-receive

Paste this into the file (if you named your git folder something other than blog.git, make sure you update it below before pasting):

#!/usr/bin/env bash

GIT_REPO=$HOME/blog.git
TMP_GIT_CLONE=/tmp/blog
PUBLIC_WWW=/var/www/html

git clone $GIT_REPO $TMP_GIT_CLONE
pushd $TMP_GIT_CLONE
bundle install --deployment
bundle exec jekyll build -d $PUBLIC_WWW
popd
rm -rf $TMP_GIT_CLONE

exit

This is a Bash script that does the following:

  1. Clone the repo to a temporary directory
  2. Build the production site content and place it in /var/www/html.
  3. Delete the temporary directory

Notice that the build line (bundle exec jekyll build -d $PUBLIC_WWW) is a little different than the one we’ve used locally (jekyll build). Bundler (bundle exec) tries to mitigate dependency hell situations in Ruby projects by tracking and managing gem versions. It’s a good idea to use this anytime you run a Ruby project in multiple environments (like a local and a server), to help make sure your build doesn’t fail due to a missing dependency after its pushed. We’re also specifying -d, or an output directory, for the built files. This places files in /var/www/html automatically.

Note: With a basic, uncustomized Jekyll site like we’re using, you really don’t need to worry about Bundler much. Jekyll relies on Ruby and installs some Gems, so using Bundler is a good practice, but shouldn’t interfere with your workflow.

Before testing our script, make sure we add execution permissions to it. Otherwise the system will refuse to “run” it.

chmod +x ~/blog.git/hooks/post-receive

That’s all we need to do from the server. Now we’re ready to switch back to our local environment, set up the local Git repository, and push to the server.

Local - Push Changes to the Server Repository

Log out of server completely. If you used su to login to the Git user, this means you’ll need to exit twice.

git@blog:~$ exit
logout
bannmoore@blog:~$ exit
logout
Connection to 67.205.157.148 closed.

Navigate to the Jekyll folder we created in the previous tutorial (mine is called “blog”). Inside the directory, initialize a Git repository. Notice we DO NOT use --bare here, because this repository is not empty.

$ cd blog
$ git init
Initialized empty Git repository in /Users/brittany/code/misc/blog/.git/

In order to push changes to the server repository, we need to add it as a “remote” of our local repository. Let’s create a new remote called “server”. In the command below, replace IP_ADDRESS with the IP of your server, and blog.git with the server’s Git repository name (which you created above).

$ git remote add server git@IP_ADDRESS:blog.git

In order to push the local code, we have to add at least one commit. These commands will add all the existing files to a new commit, and save it with the message “Initial commit.”.

$ git add .
$ git commit -m "Initial commit."

Now we’re ready to push! Run git push with the name of the remote (server) and the name of the Git branch (defaults to master with new repositories).

$ git push server master

This will generate a lot of terminal output, but it should end with something like this:

remote: Bundle complete! 4 Gemfile dependencies, 29 gems now installed.
remote: Bundled gems are installed into `./vendor/bundle`
remote: Configuration file: /tmp/blog/_config.yml
remote:             Source: /tmp/blog
remote:        Destination: /var/www/html
remote:  Incremental build: disabled. Enable with --incremental
remote:       Generating... 
remote:                     done in 0.631 seconds.
remote:  Auto-regeneration: disabled. Use --watch to enable.
remote: ~/blog.git
To 138.68.148.87:blog.git
 + 90663a6...487e590 master -> master (forced update)

Now, try to access your server IP address in the browser, and you should see your blog!

This is cause for celebration, but we’re not quite done yet. In the next post, we’ll go through some tips on how to make your server more secure.