Deploy your Hugo site through SSH with Travis

Static site generation leverages website content management through version-control systems. With the help of CI tools, we can set up automatic deployment via SSH. This article shows how to do such deployment using Hugo and Travis CI.

After having moved my blog from WordPress to Hugo, my next objective was to have it built and deployed automatically to my own webhosting server whenever I push a commit to GitHub. The main reasons to use this process are these:

  • I want that any change or addition to the website are as easy to deploy as a simple git commit;
  • I want my builds to be reproducible;
  • I want a clean state for each build;
  • I don’t want to be dependent on an external hosting service (no lock-in);
  • I don’t want to have to worry about a computer crash or about my own Internet connection.

I use this process for about a year to deploy the MoodleBox documentation website (about 200 pages), and for a few days for deploying this blog (more than 600 pages!). It works great 👍

There are a lot of articles explaining how to deploy your static website to GitHub pages or similar alternatives, but very few deal with the deployment on your own infrastructure.1

The tools

  • Hugo – Generates my static website
  • GitHub – Hosts the source code and raw contents of the website
  • Travis CI – CI service which builds and deploy the website
  • rsync – Transfers the Hugo generated static website to the web hosting server

The build

Building a web site with Hugo is as simple as calling hugo. We have first to instruct Travis to get Hugo in its virtual machine to build our website. I choose to get the last version of Hugo, specifiying the version needed through the environment variable HUGO_RELEASE. Thus my .travis.yml reads:

dist: xenial
sudo: required

language: node_js
node_js:
  - "lts/*"

install:
  - wget "https://github.com/gohugoio/hugo/releases/download/v${HUGO_RELEASE}/hugo_extended_${HUGO_RELEASE}_Linux-64bit.deb"
  - sudo dpkg -i hugo*.deb

script:
  - hugo version # prints the Hugo version, just in case.
  - hugo # build the website

env:
  global:
    - PRODUCTION=true
    - HUGO_RELEASE=0.54.0

The whole thing should be pretty clear to people using Travis. Now let’s see how to deploy it through SSH.

The deployment

We’ll now deploy the public directory generated by Hugo to the hosting web server through a secure SSH connection. As we want to do this automatically, without having to type in a password to log into the server, we have to use a pair of RSA keys. The public key will be added to the trusted keys of the web server, and the private key will be given to Travis, so that it can copy the files.

All of this should obviously be done securely so that the private key is not displayed to the public. And fortunately, Travis CI Client supports encryption, enabling us to do exactly that. Let’s install the Travis client2, typing gem install travis.

Encryption keypair configuration

We can now generate a dedicated RSA keypair (in case of problem, easier to revoke), by cd’ing to the project’s directory and running ssh-keygen:

$ cd <my-blog-project>
$ ssh-keygen -t rsa -b 4096 -C 'build@travis-ci.org' -f ./deploy_rsa

This creates the private key: deploy_rsa and the public key: deploy_rsa.pub. Be careful! The private key should never be published openly. Don’t commit anything yet to your git repository.

We can now encrypt the private key using the Travis CI Client, after having first logged into Travis:

$ travis login --org
$ travis encrypt-file deploy_rsa --add

The Travis client encrypted the deploy_rsa private key into the file deploy_rsa.enc. Thanks to the -add option, it added at the same time a decryption key, stored as an environment variable, to the .travis.yml file of the project, with some other lines which will decrypt it during the run.

We copy now the public key to the hosting web server and delete it from the local computer, as well as the unencrypted version of the private key:

$ ssh-copy-id -i deploy_rsa.pub <deploy-user>@<web-server-host>
$ rm deploy_rsa deploy_rsa.pub

We then commit the encrypted private key deploy_rsa.enc to the git repository:

$ git add deploy_rsa.enc
$ git commit -m "Add private key"
$ git push

Finally, we edit .travis.yml, moving the decryption line from the before_install: section to the before_deploy: section, and adding decrypting commands as follows:

before_deploy:
  - openssl aes-256-cbc -K $encrypted_<...>_key -iv $encrypted_<...>_iv -in deploy_rsa.enc -out /tmp/deploy_rsa -d
  - eval "$(ssh-agent -s)"
  - chmod 600 /tmp/deploy_rsa
  - ssh-add /tmp/deploy_rsa
  - echo -e "Host ${DEPLOY_HOST}\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config

This ensures that the private key is decrypted and loaded into memory before the deploy is done. The last line permits that the deploy process doesn’t hang waiting for user input.

Deployment configuration

We’re now ready to setup the deployment, which will be done with rsync.

As a security measure, we’d like to keep secret the web hosting server, the user used to do the deployment and the directory where the website resides. We encrypt these as environment variables using the Travis client, just like we did before with the private key.

$ travis encrypt DEPLOY_HOST=<web-server-host> --add
$ travis encrypt DEPLOY_USER=<deploy-user> --add
$ travis encrypt DEPLOY_DIRECTORY=<deploy-directory> --add

These commands will add three new lines to the env.global: section of your .travis.yml file, beginning with - secure: followed by a long string of random looking ASCII characters.

env:
  global:
    - PRODUCTION=true
    - HUGO_RELEASE=0.54.0
    - secure: "<long string of random looking ASCII chars>"
    - secure: "<another long ASCII string>"
    - secure: "<and another long ASCII string>"

We’re now ready to add at the end of .travis.yml the commands to transfer the website data to the web hosting server:

deploy:
  provider: script
  skip_cleanup: true
  script: rsync -r --quiet --delete ${TRAVIS_BUILD_DIR}/public/ ${DEPLOY_USER}@${DEPLOY_HOST}:${DEPLOY_DIRECTORY}
  on:
    branch: master

The option skip_cleanup keeps the result of the build before the transfer. The --delete part cleans the directory before we copy the new version of the site we just built.

If you followed these instructions, you should have a .travis.yml looking like that:

dist: xenial
sudo: required

language: node_js
node_js:
  - "lts/*"

install:
  - wget "https://github.com/gohugoio/hugo/releases/download/v${HUGO_RELEASE}/hugo_extended_${HUGO_RELEASE}_Linux-64bit.deb"
  - sudo dpkg -i hugo*.deb

script:
  - hugo version # prints the Hugo version, just in case.
  - hugo # build the website

before_deploy:
  - openssl aes-256-cbc -K $encrypted_<...>_key -iv $encrypted_<...>_iv -in deploy_rsa.enc -out /tmp/deploy_rsa -d
  - eval "$(ssh-agent -s)"
  - chmod 600 /tmp/deploy_rsa
  - ssh-add /tmp/deploy_rsa
  - echo -e "Host ${DEPLOY_HOST}\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config

deploy:
  provider: script
  skip_cleanup: true
  script: rsync -r --quiet --delete ${TRAVIS_BUILD_DIR}/public/ ${DEPLOY_USER}@${DEPLOY_HOST}:${DEPLOY_DIRECTORY}
  on:
    branch: master

env:
  global:
    - PRODUCTION=true
    - HUGO_RELEASE=0.54.0
    - secure: "<a long string of random looking ASCII chars ending with an equal sign>"
    - secure: "<another long ASCII string ending with an equal sign>"
    - secure: "<and another long ASCII string ending with an equal sign>"

That’s all folks! Push now a commit to the master branch of your git repo, and your website is automatically deployed to your host by Travis.

Update April 5, 2020: Fix rsync deployment command.


  1. One of these few articles is authored by János Rusiczki, and describes how to deploy a Jekyll blog through SSH↩︎

  2. Travis CI Client is written in Ruby, so you’ll have to install Ruby on your computer to do the encryption process. At the end of the whole procedure, Travis CI Client can be deleted from your computer, as well as the Ruby installation. ↩︎