This is how I am 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.

TL;DR: Push the static build to a remote, bare repository that has a detached working directory (on the same server). A post-receive hook checks out the files in the public directory.

Prerequisites

  • A remote web server to host your site.
  • SSH access to your remote server.
  • Git installed on your remote server (check with git --version).
  • Generate an SSH key if you need one.

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.

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 example.com and you also wanted a staging environment, you’d create these directories on the server:

mkdir ~/example.com ~/example.git

mkdir ~/staging.example.com ~/staging.example.git

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 ~/example.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 example.git to example.com, and check out a copy of the build into the example.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/example.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/~/example.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.example.git
PROD_REPO = ssh://user@hostname/~/example.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 ;!

I push my site’s source code to a private repository on BitBucket. One of the nice things about BitBucket is that it gives you the option to prevent deletions or history re-writes of branches.

If you have any suggested improvements, let me know on Twitter.