Git Deployment and You: Static Site Edition

This is how I am currently deploying the build of my static website to staging and production domains. It requires basic use of the CLI, Git, and SSH. But once you’re set up, a single command will build and deploy.

What you Will Need:

Once on the Server

Set up password-less SSH access

First, you need to SSH into your server, and provide the password if prompted.

ssh user@hostname

If there is no ~/.ssh directory in your user’s home directory, create one: mkdir ~/.ssh.

Next, you need to copy your public SSH key (see “Generate an SSH key” above) to the server. This allows you to connect via SSH without having to enter a password each time.

From your local machine – assuming your public key can be found at ~/.ssh/id_rsa.pub – enter the following command, with the correct user and hostname. It will append your public key to the authorized_keys file on the remote server.

ssh user@hostname 'cat >> ~/.ssh/authorized_keys' < ~/.ssh/id_rsa.pub

If you close the connection, and then attempt to establish SSH access, you should no longer be prompted for a password.

Now Create the Remote Directories

You need to have 2 directories for each domain you want to host. One for the Git repository, and one to contain the checked out build.

For example, if your domain were somesite.com and you also wanted a staging environment, you’d create these directories on the server:

mkdir ~/somesite.com ~/somesite.git
mkdir ~/staging.somesite.com ~/staging.somesite.git

Then You Can Initialize the Bare Git Repository

Create a bare Git repository on the server. This is where you will push the build assets to, from your local machine. But you don’t want the files served here, which is why it’s a bare repository.

cd ~/somesite.git
git init --bare

Repeat this step for the staging domain, if you want.

Write a Post-Receive Hook

A post-receive hook allows you to run commands after the Git repository has received commits. In this case, you can use it to change Git’s working directory from somesite.git to somesite.com, and check out a copy of the build into the somesite.com directory.

The location of the working directory can be set on a per-command basis using GIT_WORK_TREE, one of Git’s environment variables, or the --work-tree option.

cat > hooks/post-receive
#!/bin/sh
WEB_DIR=/path/to/somesite.com

# remove any untracked files and directories
git --work-tree=${WEB_DIR} clean -fd

# force checkout of the latest deploy
git --work-tree=${WEB_DIR} checkout --force

Make sure the file permissions on the hook are correct.

chmod +x hooks/post-receive

If you need to exclude some files from being cleaned out by Git (e.g., a .htpasswd file), you can do that using the --exclude option. This requires Git 1.7.3 or above to be installed on your server.

git --work-tree=${WEB_DIR} clean -fd --exclude=<pattern>

Repeat this step for the staging domain, if you want.

On your local machine

Now that the server configuration is complete, you want to deploy the build assets (not the source code) for the static site.

The build and deploy tasks

I’m using a Makefile, but use whatever you feel comfortable with. What follows is the basic workflow I wanted to automate.

  1. Build the production version of the static site.

    make build
  2. Initialize a new Git repo in the build directory. I don’t want to try and merge the new build into previous deploys, especially for the staging domain.

    git init ./build
  3. Add the remote to use for the deploy.

          cd ./build
          git remote add origin ssh://user@hostname/~/somesite.git
          
  4. Commit everything in the build repo.

            cd ./build
            git add -A
            git commit -m "Release"
  5. Force-replace the remote master branch, creating it if missing.

    cd ./build git push -f origin +master:refs/heads/master
  6. Tag the checked-out commit SHA in the source repo, so I can see which SHA’s were last deployed.

    git tag -f production

Using a Makefile:

BUILD_DIR := ./build
STAGING_REPO = ssh://user@hostname/~/staging.somesite.git
PROD_REPO = ssh://user@hostname/~/somesite.git

install:
npm install

\# Deploy tasks
staging: build git-staging deploy
@ git tag -f staging
@ echo "Staging deploy complete"

prod: build git-prod deploy
@ git tag -f production
@ echo "Production deploy complete"

\# Build tasks
build: clean
\# whatever your build step is
\# Sub-tasks

clean:
@ rm -rf $(BUILD_DIR)

git-prod:
@ cd $(BUILD_DIR) && git init && git remote add origin $(PROD_REPO)

git-staging:
@ cd $(BUILD_DIR) && git init && git remote add origin $(STAGING_REPO)

deploy:
@ cd $(BUILD_DIR) && git add -A && git commit -m "Release" && git push -f origin +master:refs/heads/master

.PHONY: install build clean deploy git-prod git-staging prod staging

To deploy to staging:

make staging

To deploy to production:

make prod

Using Make, it’s a little bit more hairy than usual to force push to master, because the cd commands take place in a sub-process. You have to make sure subsequent commands are on the same line. For example, the deploy task would force push to your source code’s remote master branch if you failed to join the commands with && or ;!