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.
- An Ubuntu server set up with nginx and a non-root user
- A local directory containing Jekyll-generated site content
- [optional] Some familiarity with Git is helpful, but not required
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 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.
.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 (
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
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
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
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
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:
- Clone the repo to a temporary directory
- Build the production site content and place it in
- 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
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
git@blog:~$ exit logout bannmoore@blog:~$ exit logout Connection to 126.96.36.199 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 188.8.131.52: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.