Set up an automated deployment with Git hooks

This guide describes how to automatically deploy a PHP application when pushing commits to a server.

It assumes that you have performed the previous [nginx & PHP-FPM exercise]/course/512-nginx-php-fpm-deployment/.

:scroll: Legend

Parts of this exercise are annotated with the following icons:

  • :exclamation: A task you MUST perform to complete the exercise
  • :question: An optional step that you may perform to make sure that everything is working correctly, or to set up additional tools that are not required but can help you
  • :checkered_flag: The end of the exercise
  • :classical_building: The architecture of the software you ran or deployed during this exercise.
  • :boom: Troubleshooting tips: how to fix common problems you might encounter

:exclamation: Set up directories

๐Ÿ› ๏ธ

Connect to your cloud server with SSH for this part of the exercise.

Create two directories, todolist-automated and todolist-automated-repo, in your home directory:

$> cd
$> mkdir todolist-automated
$> mkdir todolist-automated-repo

The todolist-automated-repo directory will be the Git repository. Later you will add it as a remote in your local Git repository, so that you can push commits to it.

The todolist-automated directory will contain the currently deployed version of the code. The goal is that every time you push commits to the repository, this directory is automatically updated.

:exclamation: Update the todolist nginx configuration

In previous exercises you configured nginx to serve the PHP application from the todolist-repo directory. Edit that configuration:

$> sudo nano /etc/nginx/sites-available/todolist

Change todolist-repo to todolist-automated so that nginx looks for files in the correct directory.

Tell nginx to reload its configuration:

$> sudo nginx -s reload

The site at http://todolist.jde.archidep.ch may not work anymore. You may get a 404 Not Found error from nginx since there are no files in the todolist-automated directory yet.

:exclamation: Create a bare Git repository on the server

Git will not let you push commits to a normal repository with a working tree, so you need to use a bare repository instead, with only its Git directory:

$> cd ~/todolist-automated-repo
$> git init --bare
Initialized empty Git repository in /home/jde/todolist-automated-repo/
:books:

Remember that a Git repository has several parts: the Git directory where the projectโ€™s history is stored, and the working tree which contains the current version of the files you are working on.

A bare repository is a repository with only a Git directory and no working tree. The projectโ€™s files are not checked out. Itโ€™s used mostly on servers for sharing or automation. Read What is a bare repository? for more information.

:exclamation: Add a post-receive hook to the Git repository

Copy this script:

#!/usr/bin/env bash
set -e

echo Checking out latest version...
export GIT_DIR=/home/jde/todolist-automated-repo
export GIT_WORK_TREE=/home/jde/todolist-automated
git checkout -f main
cd "$GIT_WORK_TREE"

echo Deployment successful

This script will take the latest version of the code in the todolist-automated-repo repository and checkout a working tree in the todolist-automated directory (the one nginx is serving files out of).

Warning

If your repo has a master branch instead of a main branch, replace main by master in the git checkout -f main command in your hook.

:books:

Normally, when you use the git checkout command in a Git repository, it will use the .git directory of the repository as the Git directory, and the repository itself as the working tree.

By setting the GIT_DIR environment variable, you are instructing Git to use a different Git directory which could be anywhere (in this case, it is the bare repository you created earlier).

By setting the GIT_WORK_TREE environment variable, you are instructing Git to use a different directory as the working tree. The files will be checked out there.

Open the post-receive file in the repositoryโ€™s hooks directory:

$> nano hooks/post-receive

Paste the contents of the script your copied above.

:gem: Tip

Replace jde with your username in the GIT_DIR and GIT_WORK_TREE variables.

Exit with Ctrl-X and save when prompted.

Make the hook executable:

$> chmod +x hooks/post-receive

Make sure the permissions of the hook are correct:

$> ls -l hooks/post-receive
-rwxrwxr-x 1 jde jde 239 Jan 10 20:55 hooks/post-receive
:gem: Tip

It should have the x (execute) permission for owner, group and others.

:exclamation: Add the serverโ€™s Git repository as a remote

๐Ÿ› ๏ธ

Disconnect from your cloud server or open another terminal. The following steps happen on your local machine.

Go to the PHP todolist repository on your local machine:

$> cd /path/to/projects/php-todo-ex

As you have already seen with GitHub, Git can communicate over SSH. This is not limited to GitHub: you can define a remote using an SSH URL that points to your own server.

Add an SSH remote to the bare repository you created earlier (replace jde with your username and W.X.Y.Z with your serverโ€™s IP address):

$> git remote add archidep jde@W.X.Y.Z:todolist-automated-repo
:gem: Tip

Replace jde with your username and W.X.Y.Z with your serverโ€™s public IP address.

:books: More information

The format of the remote URL is <user>@<ip-address>:<relative-path>. Git can connect to your server over SSH using public key authentication just like when you use the ssh command. It will then look for a repository at the path you have specified, relative to your home directory.

:exclamation: Trigger an automated deployment

From your local machine, push the latest version of the main branch to the remote on your server:

$> git push archidep main
Enumerating objects: 36, done.
Counting objects: 100% (36/36), done.
Delta compression using up to 8 threads
Compressing objects: 100% (19/19), done.
Writing objects: 100% (36/36), 15.09 KiB | 15.09 MiB/s, done.
Total 36 (delta 16), reused 36 (delta 16)

remote: Checking out latest version...
remote: Deployment successful

To W.X.Y.Z:todolist-automated-repo
 * [new branch]      main -> main
Warning

If your repo has a master branch instead of a main branch, replace main by master in the git push archidep main command in your hook.

:gem: Tip

If you have set up your post-receive hook correctly, you will see the output of its echo commands displayed when you run git push. In the above example, they are the two lines starting with remote:.

The site at http://todolist.jde.archidep.ch should work again.

:exclamation: Check that the automated deployment worked on the server

๐Ÿ› ๏ธ

Reconnect to your cloud server or switch to a terminal where you are still connected.

Additionally, the todolist-automated directory should contain the latest version of the projectโ€™s files, as checked out by the post-receive hook:

$> ls ~/todolist-automated
LICENSE.txt  README.md  index.php  todolist.sql

:exclamation: Commit a change to the project and deploy it

๐Ÿ› ๏ธ

Back to your local machine again.

Using your favorite editor, make a visible change to the projectโ€™s index.php file.

:gem: Tip

For example, look for the <strong>TodoList</strong> tag in the <header> and change the title.

Commit and push your changes to the archidep remote (i.e. your server):

$> git add .

$> git commit -m "Change title"

$> git push archidep main
...
remote: Checking out latest version...
remote: Deployment successful
To W.X.Y.Z:todolist-automated-repo
   4ea6994..2faf028  main -> main

Visit http://todolist.jde.archidep.ch again. Your changes should have been deployed automatically!

:checkered_flag: What have I done?

You have created a bare Git repository on your server and pushed the PHP todolist to that repository. You have set up a Git hook: a shell script that is automatically executed every time a new commit is pushed. This script deploys the new version of the todolist by copying the new version to the correct directory.

This allows you to deploy new versions by simply pushing to the repository on your server. You could add any command you wanted to your deployment script.

:classical_building: Architecture

This is a simplified architecture of the main running processes and communication flow at the end of this exercise. Note that it has not changed compared to the previous exercises since we have neither created any new processes nor changed how they communicate:

Diagram