Deployment workflow

Since hugo is so fast, it is easy to write and develop your site on a local machine. However, when it comes to deploying, I am still uploading the files to my VPS by hand.

So, do you guys have some tips and tricks for a fast and easy deployment strategy? Do you use rsync, bittorent sync, git, something else?

1 Like

I deploy to github, so it has to be git. If I was deploying to a server, I’d probably use rsync.

I deploy using git and a post-commit hook. Locally, I do this:

git add content/
git commit -m "revised some-file"
git push deploy master

And git on the web server catches the changes, then runs hugo (on the server), to generate a fresh public copy into the web root.

It’s an adaption of this strategy meant for Jekyll.

I’d be happy to write it out in more detail if you’re interested.


@acodispo It would be great if you could write it out. I think a lot of people would benefit from detailed steps.


For I use SSH + RSYNC. I wrote a little function in my shell rc file (~/.zshrc) to do it for me.

function spf13prodbuild {
    rm -rf /tmp/spf13
    hugo -s /path-to-spf13-source/ -d /tmp/spf13
    rsync -az --force --progress -e "ssh" /tmp/spf13/ spf13:/server-path-to-spf13/

@acodispo, How have you managed to run Hugo on GitHub server? Or do you run git on some other server?

For all those who’ve indicated an interest, I wrote out how I set up my current deploy process:

I apologize for the roughness of it; consider it a first draft. I’d appreciate your comments or questions, so that I can clarify the rough points. I did run through these directions myself just now and they seemed to work fine, for the most part.

The only major stumbling point is in step 1, setting up a Hugo site. I couldn’t figure out how to include themes via submodule in the bare remote on my server (my site was first built before Hugo had themes). If anybody can help me with what to do at that point, I’d appreciate it. The workaround is to create your own layout files and not use a theme.

@abirger, I actually host my site on a shared server, Dreamhost.

When I am ready to publish for public consumption I use an alternative launch of hugo from a “production” bash script to direct the Hugo rendering to an alternate directory that is set to automatically sync (goodsync) to an S3 bucket that is referecened by my domain url.

Although I wish my script could also change the baseurl in the config file for the production run. So at this time I edit that first and the edit it back. There were other discussions about getting Hugo CL to accept alternate config files. To me that would work nicely with the way I do things.

What has been decided on development/producntion workflows?

You can specify baseurl ( and most options ) as flags and they will override the values in the config file.

You can also pass in a flag to point to a different config file.
Environment variable support to come soon.

Why do you need to change the baseurl for production? When running locally, hugo will automatically swap out your baseurl’s domain for localhost:1313, so you shouldn’t need a different baseurl at all.


I’ve been using Grunt for things and love it. So I can run “grunt dev” and “grunt deploy” and so on. Of course this requires Node.js (but only on your machine, not your server). You also easily gain the ability to run various commands on things, like minifying CSS, JS, etc. The other cool thing is I pop open a web browser when I type “grunt dev” so it starts Hugo and opens up to my site (see the “open:devserver” command).

For example (Gruntfile.js):

module.exports = function(grunt) {


    shell: {
      options: {
        stdout: true
      server: {
        command: 'hugo server --theme=redlounge --buildDrafts --watch'
      build: {
        command: 'hugo -d build/ --theme=redlounge'
      deploy: {
        command: 'rsync -az --force --progress -e "ssh" build/'

    open: {
       devserver: {
         path: 'http://localhost:1313'

  grunt.registerTask('dev', ['open:devserver', 'shell:server']);

  grunt.registerTask('build', ['shell:build']);

  grunt.registerTask('deploy', ['shell:build', 'shell:deploy']);

Here’s my package.json so if anyone wants, they could basically copy this and run npm install. Then have The same grunt commands to use. Ensure this goes alongside your Hugo site. Or in other words, “public” and “themes” and “layouts” should all be sitting next to your Gruntfile.js and package.json.

  "name": "my-site",
  "version": "0.0.1",
  "homepage": "",
  "devDependencies": {
    "grunt": "^0.4.5",
    "grunt-contrib-concat": "^0.4.0",
    "grunt-contrib-copy": "^0.5.0",
    "grunt-contrib-less": "^0.11.3",
    "grunt-contrib-uglify": "^0.5.0",
    "grunt-css": "^0.5.4",
    "grunt-open": "^0.2.3",
    "grunt-shell": "^0.7.0",
    "grunt-shell-spawn": "^0.3.0",
    "matchdep": "^0.3.0"

For people who use gh-pages on GitHub, I highly sugges using the approach/script below instead of the subtree method. This creates much less comit-noise (avoide the double commit …):


@spf13 first post here - first, thank you for Hugo. It seems like a great environment, with a vibrant community.

That rsync deploy makes sense to me, but, are you keeping any part of your site’s source tree in a git repo as well? How are you getting around the theme submodule issue mentioned in this thread.

Thank you in advance,

I use travis-ci with github to deploy. It works well for me.

Here is my write up on it -> (i know its a plug)

I’ve tried the tutorial many times, and always the CSS files don’t seem to load. I’m using the subtree method here:

Demo site should be here:

Can anyone tell me what I’m doing wrong? Seems canonical links are in the demo code, but not my headers. I’ve followed these instructions exactly, so many times. It must be something simple I’m missing, thanks.

Hi @cloudunicorn,

I took a quick look at your site, and here are my observations:

  1. Your site is at rather than

  2. In the rendered HTML, the stylesheets are listed as folllows:

     <link rel="stylesheet" href="/css/poole.css">
     <link rel="stylesheet" href="/css/syntax.css">
     <link rel="stylesheet" href="/css/lanyon.css">

    so the browser tries to look for etc., but of course found nothing. Try accessing instead. :slight_smile:

  3. You are using the Lanyon theme that you fetched from, right? If so, please take a look inside themes/lanyon/layouts/partials/head.html and see how the links to the stylesheets are defined?

Yes, you got it. The current Hugo port of Lanyon theme assumes that your website is placed in the root directory of your domain. Since you have placed your blog under the /hblog subdirectory, that theme does not work out of the box.

How to fix it? Try using the hyde or herring-cove themes instead. These were ported by Hugo’s author @spf13 himself, so this subdirectory issue has been taken care of. :wink:

Both hyde and herring-cove work great, right? Good! Now, as an exercise for you, please examine the partials in hyde and herring-cove, and figure out what you need to do to get the Lanyon theme to do the same, i.e. to be able to load your CSS properly.

So, no, I don’t think the CSS-fails-to-load issue has anything to do with . Your blog has been deployed correctly. It is just that you’ll need to tweak your “partials” to generate the desired HTML output to load the CSS files properly.

Hope this helps!


Thanks so much for the detailed response.

Is the demo not at a subdirectory? The URL is baseurl: “” as seen here:

and live site is at

And demo specifically says to use Lanyon. Only pointing out in case I’m not alone in following the demo instructions and running into this?

I fixed my issue by adding {{ .Site.BaseUrl }} to the css links so they read:

<link rel="stylesheet" href="{{ .Site.BaseUrl }}/css/poole.css">
<link rel="stylesheet" href="{{ .Site.BaseUrl }}/css/syntax.css">
<link rel="stylesheet" href="{{ .Site.BaseUrl }}/css/lanyon.css">

I don’t think the demo makes it obvious in any way this needs to be done, and it very much appears that the site will be served to a subdirectory of your github account.

I’ve seen lots of discussion about how to make Hugo approachable to ‘normals’ and this is where I’d start! If I can I’ll try to fork and edit myself.

Hi @cloudunicorn

Thank you for discovering these issues with our tutorials! You are absolutely right that we need to make Hugo approachable to newcomers, and fixing such problems our tutorials should be a top priority for our community indeed.

I am sorry that I spoke before I read—I did not read through the whole tutorial myself, and did not know it specifically mentions the use of the Lanyon theme.

Looking deeper, it seems @spencerlyon2, who contributed the original tutorial very early in March 2014, had ported the Lanyon theme himself, rather than using tummychow’s referenced by hugoThemes. My assumptions in my last post were incorrect.

Anyhow, moving forward, we have a few action items to work on:

  1. Fix issues in especially caveats when hosting in a subdirectory and how the {{ .Site.BaseUrl }} needs to be added to layouts/partials/*.html or layouts/chrome/head_includes.html as necessary. Also, if possible, update to Hugo v0.12 conventions.

  2. Submit an Issue or Pull Request to @spencerlyon2 to correct issues in, adding {{ .Site.BaseUrl }} to layouts/chrome/head_includes.html, and preferably converting layouts/chrome to layouts/partials.

  3. Submit an Issue or Pull Request to to add {{ .Site.BaseUrl }} to his layouts/partials/head.html too.

  4. [New info] Hugo v0.10 has canonifyurls = true as default, but for yet unknown reasons, Hugo v0.11 and beyond now has canonifyurls = false as the default, opposite to what is currently documented on This is the root cause of @cloudunicorn’s ordeal. Need to fix both code and doc. (Filed as Issue #802: The default of canonifyurls = true got reversed silently since v0.11)

Any volunteers? :slight_smile:

Thank you @cloudunicorn for your comments and contributions! Your suggestions help our Hugo community to grow and become better!


1 Like

Looking yet deeper into this, it seems that the real reason @cloudunicorn ran into problem was not because @spencerlyon2 forgot to put in {{ .Site.BaseUrl }}, but rather, it was not necessary to add {{ .Site.BaseUrl }} with Hugo v0.9 or Hugo v0.10, the latest stable version when @spencerlyon2 wrote his tutorial, because Hugo v0.10 by default “canonify”, i.e. automatically add the baseurl to all relative URLs!

  • Hugo v0.9 (2013-11-15): “Canonify” or “canonicalize” all relative URLs. Not configurable.

  • Hugo v0.10 (2014-03-01): Added canonifyurls option to keep URLs relative. The default is canonifyurls = true as documented, i.e. same as Hugo v0.9.

    commit 438c2198923022a3e4d299b06a7df18268041dd8
    Author: Phil Pennock
    Date: Fri Jan 3 18:36:53 2014 -0500

    Add canonifyurls config option.

    Be able to inhibit AbsURL canonicalization of content, on a site configuration basis. Advantages of being able to inhibit this include making it easier to rendering on other hostnames, and being able to include resources on http or https depending on how this page was retrieved, avoiding mixed-mode client complaints without adding latency for plain http.

  • Hugo v0.11 (2014-05-29): Somehow, the default seems to have become canonifyurls = false without anyone noticing, and without documentation change. Bug?

  • Hugo v0.12 and v0.13-DEV as of today: Ditto.

I wonder what happened… Hmm… More investigation needed, to find out which commit between v0.11 and v0.12 is responsible for the undocumented behaviour change, i.e. bug.

So, yet another possible answer to your question, @cloudunicorn, is simpler: Rather than adding {{ .Site.BaseUrl }} manually, you may alternatively add canonifyurls: true to your config.yaml, which will result in a correct and identical HTML with the correct URLs to your stylesheets.

1 Like