Deploy a Project to Your Server with Git

By Dave Ceddia

So you’ve got a local git repository, and you want to deploy it to your own server. Maybe the repo is kept in GitHub, maybe not. Maybe your server is on DigitalOcean, or AWS, or in your basement. It doesn’t much matter.

The goal: when you run git push, deploy the latest master branch to the server. Let’s set it up.

How to Deploy a Git Repo

There are lots of ways to do this. On one end of the spectrum is copying files with scp or rsync or even FTP. On the other, more modern side, there’s full-blown continuous deployment systems like CircleCI.

This approach sits in the middle. You don’t need to set up or sign up for any services. This is 100% DIY. But it’s using git to do the deploy, so it’s a step above just copying files, because you’re deploying a specific commit (not just whatever files are laying around).

Here’s a rough diagram of what will happen with this setup:

Deploy flow: git push to server, server runs post-receive hook, and moves files into place

On the server…

  • We’ll create a “bare” git repository, to receive the deployed files (bare_project.git)
  • The bare repo will not have our actual project files, which isn’t terribly useful, so…
  • A post-receive hook in there will check out the files into a “worktree” (a separate folder, deployed_project in the diagram)
  • We can customize the post-receive hook to do extra stuff too: install packages, run a build, restart a service, etc.

On your computer…

  • Add the server as a “remote” to the git repo, using the path to the bare repo on the server
  • Just git push and off it goes

Step by Step

1. Create the Bare Repo

Create a bare git repo on the server. Doesn’t matter where this goes, as long as you have permission to write to it. Your home directory would work fine. (e.g. /home/you/your_project.git). The “.git” at the end is not required, but it’s a good reminder that this directory is not a regular project.

ssh you@your-server
git init --bare /path/to/bare_project.git

(Try ls /path/to/bare_project.git and see what it contains)

2. Create the post-receive Hook

Create the file /path/to/bare_project.git/hooks/post-receive and add this:

#!/bin/sh

# Check out the files
git --work-tree=/var/www/deployed_project --git-dir=/path/to/bare_project.git checkout -f

This file needs to be executable so use chmod to make that happen. (don’t skip this step!)

chmod +x /path/to/bare_project.git/hooks/post-receive

3. Configure your local repo to push to the server

We’ll add a “remote” to your local repo. A remote is an endpoint that git can push to. The address can look like a URL (ssh://you@your-server/path/to/files) or it can be in the SSH format (you@your-server:/path/to/files)

When I set this up for myself, I put the bare repo in my home directory, so the path was me@my-server:my-project.git (no absolute path needed, since it’s right in the home directory).

Run this within your own local repo:

git remote add live 'you@your-server:/path/to/bare_project.git'
git push --set-upstream live master

The name “live” can be whatever you want (“prod”, “production”, “deploy”, etc.).

The second command is what binds your master branch to the live remote, so when you run git push, git knows where to push.

(You can verify that the remote was added correctly by running git remote -v)

Try it Out!

Run git push inside your local repo. Assuming everything is working right, you should see git push up the files, and it shouldn’t print any errors.

Then, log in to your server and make sure the project files were checked out in the /var/www/deployed_project location (wherever you put them).

Run Tasks After Deploy with Git

Your project is deployed now. Awesome!

But maybe there’s more you wanted to do, like restart a server, or run npm install to sync up packages, or some other thing.

The post-receive script you already have is the perfect place to do this.

Just beware that the “working directory” that the script runs in might not be where you think. Be sure to cd to the right place first. And the PATH might not be what it would normally be when you’re logged in, so referring to executables by their full path can be a good idea too.

Here’s an example of running npm install and restarting a service after each deploy:

#!/bin/sh

# Check out the files
git --work-tree=/var/www/deployed_project --git-dir=/path/to/bare_project.git checkout -f

# Install packages
cd /var/www/deployed_project
npm install

# Restart the web server
# (requires sudoers to allow this command without a password)
sudo /bin/systemctl restart my-project.service

If the npm command fails, login and run which npm, then use that full path in the command, like /usr/local/bin/npm install.

Run sudo commands from a post-receive hook

If the sudo command fails with an error about “no interactive terminal” or some such, that’s because it’s trying to ask for a password. That won’t work. But you can add a rule to the sudoers file that will let you run just this one command without a password.

Log in to your server over SSH and run sudo visudo. On my Ubuntu 18.04 system, I have a line that looks like:

%sudo   ALL=(ALL:ALL) ALL

That allows everyone in the sudo group to use sudo with a password. Add another line below it to make an exception for this command:

%sudo   ALL=(ALL:ALL) ALL
%sudo   ALL=(ALL:ALL) NOPASSWD: /bin/systemctl restart my-project.service

The command there must exactly match the one in your post-receive script. Save, quit, and give it another shot.