Easily create a beautiful, responsive website or blog using the Hugo static website framework!

Do it entirely with no-cost tools and services, and use simple Markdown syntax for powerful content editing in any text editor.

This guide will cover the entire process, including using Hugo in a Docker container for content creation and localhost testing; Bitbucket Git for version control and content uploads; Bitbucket Pipelines for instant builds and deployment in the cloud; and Bitbucket.io to host it all in one place.

Overview

Several components are combined together to develop and deploy a static Hugo website hosted at Bitbucket.io; accessible from the top-level URL: https://{your-bitbucket-username}.bitbucket.io.

When I set this up, my objectives were to implement an easy, streamlined, workflow that allowed me to focus on content creation rather than website, database, server and/or domain maintenance. The components that make all this possible are:

  • Docker; for running Hugo on client (localhost). If you don’t want to use Docker, Hugo can also be installed directly to localhost.
  • Hugo; as the static website development framework. I use Hugo rather than it’s slightly better-known alternative, Jekyll, because Hugo sites compile in (milli)seconds rather than minutes.
  • Bitbucket; as the Git version control system and file host. Content will be created on client workstations, then uploaded to the cloud via git push (and associated commands). A simple ‘master-development’ branching model will be used to manage source and output files.
  • Bitbucket Pipelines; as the ‘continuous-integration/continuous-deployment’ (CI/CD) framework. Pipelines will enable automatic deployment of newly pushed content to the Hugo website.
  • Bitbucket.io (aka Bitbucket Cloud / Bitbucket Pages); as the web host. Other web hosts could easily be used as an alternative.

This guide is for Linux users, though would probably be almost directly applicable for Mac users as well. I use QubesOS with Debian 9 (Stretch) templates for all my development work. The commands included here will also work in regular Debian or Ubuntu environments.

Let’s get started!

Table of Contents

Accounts

This guide requires a Bitbucket account for deployment and hosting.

I use Bitbucket rather than Github because Bitbucket provides both public and private repositories without charge; and because Bitbucket Pipelines and Bitbucket.io (static website hosting) are fantastic services that are also included. If you don’t have an account yet, start by signing up for free!

For your reference, my blog also uses several external services to integrate with my online presence. This guide does not require these services to complete your website deployment, but you might find them useful.

  • Gravatar for managing my online identity on websites and forums.
  • Disqus for providing services to obtain user comments and feedback.
  • Cloudinary as a content delivery network (CDN) for images on my site. I could also host them on Bitbucket, but I choose to use a CDN instead to ensure that my images load as quickly as possible.

Client Setup

The client, aka localhost, is the workstation where users write content and then push to the cloud. The client will need Hugo to be installed (either via Docker, or directly), and a linked repository on a Bitbucket account.

Git and Bitbucket

Configure Git

After obtaining a Bitbucket account, install and configure Git on the client workstation to interact with the account. Also install cURL to interact with the Bitbucket REST API.

# Install Git and cURL
sudo apt-get install -y git curl

# To simplify multiple successive steps, assign your Bitbucket username to a terminal variable.
#   Substitute the {} characters and contents to match your own. 
#   (In my case, the next line becomes username="0x666f6f".)
username="{your-bitbucket-username}"

# Link client to Bitbucket.
git config --global user.email "{your-bitbucket-email-address}"
git config --global user.name "$username"

Set up Bitbucket repository

Set up read-write SSH access to the Bitbucket account. This is required to easily push new content to the cloud.

# Generate the SSH key:
ssh-keygen -t rsa

# To simplify the next step, assign the public key to a terminal variable.
key=$(cat ~/.ssh/id_rsa.pub | cut -d " " -f 1,2)

# Add the SSH  key to your Bitbucket account using the Bitbucket REST API. 
#   (Alternatively, this can be done via the Bitbucket web interface.)
#   NB: This command requires the "$username" variable from the previous step. The only element
#   that (optionally) needs editing is the name for your client/localhost workstation. 
#   (In my case, I named my client "d9-workstation".) 
curl -X POST -u "$username" -H "Content-Type: application/json" -d '{"key":"'"$key"'", "label":"d9-workstation"}' "https://api.bitbucket.org/2.0/users/${username}/ssh-keys"

Create the Bitbucket.io repository to host the static website. The Bitbucket hosting service requires the repo to follow the naming convention {your-bitbucket-username}.bitbucket.io. We intentionally create a private repository as there is no need to publicly allow access to the configuration or generated output files.

# Create the repository using the Bitbucket REST API. (Or use the web interface.)
#   Other than the "scm" tag, all other JSON attributes, including "website" are optional.
curl -X POST -v --user "$username" -H "Content-Type: application/json" https://api.bitbucket.org/2.0/repositories/${username}/${username}.bitbucket.io -d '{"scm":"git", "is_private":"true", "fork_policy":"no_public_forks", "language":"markdown", "website":"https://${username}.bitbucket.io"}'

Set up Git branching model

This project uses a simple ‘master-development’ branching model.1

We start by creating a project working directory on the client, in which we check out (local copy) the remote repository. By default, Git will clone the master branch. We then create and check out a new branch called hugo. All subsequent push operations from the directory will be directed to the hugo branch rather than the master.

Here’s how we use this model:

  1. Drafts and finalised content will be pushed from the client to the hugo branch in the remote repository;
  2. Whenever a new commit is pushed to the hugo branch, a Bitbucket Pipelines workflow will be activated;
  3. The Pipeline will use a specified Docker image (in the Cloud) to run the Hugo generator to convert the source files into the static website output;
  4. The Pipeline will conclude by checking out the master branch of the host repository, and pushing the static output to the repository. The website will now be accessible at “https://{your-bitbucket-username}.bitbucket.io”!

Let’s set it up.

# Create the project working directory on the client.
#   I used the same name as my repo, though it's not necessary for them to match.
mkdir -p ~/Git/0x666f6f.bitbucket.io
cd ~/Git/0x666f6f.bitbucket.io

# Create a local Git repository.
#   This will create a '.git' sub-directory and associated Git metadata.
git init

# Connect the local repository to the remote repository.
#   This step re-uses the {username} variable from earlier steps.
git remote add origin git@bitbucket.org:${username}/${username}.bitbucket.io.git

# Initialise the 'master' branch with an initial commit. 
git commit --allow-empty -m "Initializing master branch"
git push -u origin master

# Create the 'hugo' branch.
git checkout --orphan hugo

# Initialise the 'hugo' branch (thereafter the default target for successive commits).
git reset --hard
git commit --allow-empty -m "Initializing hugo branch"
git push --set-upstream origin hugo

Done!

Docker and Hugo

With our Git and Bitbucket repository configuration complete, it’s time to set up Hugo!

The next steps involve using Docker to establish a Hugo installation on the client workstation. This Docker instance will be used to generate the framework for a new Hugo site. The same Docker image will be used to subsequently generate templates for new posts and pages for the website.

Docker

Docker is a popular container-based virtualization product. It is commonly used to package together workflows or groups of installed components into ‘images’ that can be reliably re-created and shared. I use a Docker container to host my Hugo installation on my client. While Hugo can also be installed directly to the client, I do this with Docker as an easy shortcut to a fully functioning Hugo installation. As a bonus, the installed components are also separated from my main operating system environment, so the Docker approach is both convenient and more secure. Perfect!

To proceed, take a slight detour and apply my guide for installing Docker on Debian and QubesOS. (If you’re using Debian but not QubesOS, the guide will still apply.)

Next, obtain a Docker image that contains the required components; a Hugo installation. While a personal Dockerfile can be developed, there’s nothing unique about our requirements, so I use an existing public image from Docker Hub. I selected the jguyomard/hugo-builder image because it uses a recent Hugo version (v0.40, at time of writing); provides a non-root user; includes the minify minifier/packer to optimise file sizes; and is based on an Alpine Linux core to keep the image size extremely compact (only 20MB!).

Using a terminal on the client/workstation, download it:

# Pull the latest version of the 'jguyomard/hugo-builder' image. 
docker pull jguyomard/hugo-builder:latest

That’s it! Hugo is now installed. We can test the image and its Hugo installation with the hugo help command:

# Test the Hugo installation.
docker run --rm --interactive --tty jguyomard/hugo-builder hugo help

# 'docker run' is a Docker command to create an interactive terminal session in the container.
# '--rm'
#   Automatically remove the container when it exits.
# '--interactive'
#   Keep STDIN open even if not attached.
# '--tty'
#   Allocate a pseudo-TTY.
#
# 'hugo help' is a Hugo command to access its help documentation via a terminal. 

Hugo

Time to set up our Hugo site!

The Docker container with the Hugo binary will be used to create the site structure, new posts, etc, and ultimately build the Hugo website. Config and output files on the client (Docker host) will be mounted as a directory volume to the “/src” directory in the container using the -v {host_directory}:{container_mountpoint} arguments.

Command: hugo new site

We’ll create a site with the Hugo command hugo new site {site_name}, arbitrarily selecting “hugo-src” as the top level directory where the site files will be generated. The Docker command mounts the host PWD at /src in the container using the -v|--volume flag; executes the Hugo command as user “hugo”, which creates a non-root subdirectory (on the client) containing the Hugo website source files.

# Start by changing the working directory to the root of the cloned repository.
cd ~/Git/0x666f6f.bitbucket.io

# Create a new site under the "hugo-src" directory.
docker run --rm -it -v $(pwd):/src --user hugo jguyomard/hugo-builder hugo new site hugo-src

# Here's what the subdirectory looks like after this command:
#
#  ../0x666f6f.bitbucket.io
#    └── hugo-src
#        ├── archetypes
#        │   └── default.md
#        ├── config.toml
#        ├── content
#        ├── data
#        ├── layouts
#        ├── static
#        └── themes

Done! Our site framework is ready to be edited.

Command: hugo server

We can even use this framework as-is to generate a Hugo website. Let’s see what it looks like by using the hugo server command to generate and host the output on http://localhost:1313. Expect nothing other than a blank white html page!

The handy thing about this command is that the server can be left running while you edit your content files. Whenever changes are made, the Hugo server will detect them automatically and re-generate the site to reflect changes. (Remember: this is just a preview and is only available via localhost.)

# Change to the root directory of the Hugo source files.
cd ~/Git/0x666f6f.bitbucket.io/hugo-src

# Execute the "hugo server" command.
# -w | --watch
#   Monitor for changes to the /src directory; regenerate when detected.
# -D | --buildDrafts
#   Include pages and posts in the output that contain the "draft: true" tags.
# --bind=0.0.0.0
#   Interface to bind the server instance.
docker run --rm -it -v $(pwd):/src -p 1313:1313 --u hugo jguyomard/hugo-builder hugo server --watch --buildDrafts --bind=0.0.0.0

# Load it in the browser
firefox-esr http://localhost:1313

Themes

Time to make it more interesting by adding a theme, and then some content. Here’s a great place to discover some options. I’m currently using the superb Hugo-Tranquil-Peak theme.

# Change to the root directory of the Hugo source files.
cd ~/Git/0x666f6f.bitbucket.io/hugo-src

# Add the theme as a submodule
git submodule add https://github.com/kakawait/hugo-tranquilpeak-theme.git themes/tranquil-peak

Wondering about how git submodule works? Check out this excellent explanation.

Hugo configuration: config.toml

Time to do some basic Hugo site and theme configuration. This is all managed in the config.toml file in the root of the source files directory.

This file can be created and edited from the template provided by the hugo new site command, but it’s easier to incorporate theme-specific options by incorporating the sample file provided by the theme instead.

In the code block below, we’ll use SED to edit most of the parameters from the command line. The SED syntax sed -i -e '\|searchterm| s|= .*|replace_with|' foo.cfg is an instruction to search file.cfg for all lines starting with “searchterm” and replace replace everything after (.*) the space and equals character (" =") with the “replace_with” values.

# Change to the root directory of the Hugo source files.
cd ~/Git/0x666f6f.bitbucket.io/hugo-src/`

# Optionally, backup the current config.toml.
mv ./config.toml{,.backup}

# Copy the "config.toml" file from the example site supplied by the theme.
cp ./themes/tranquil-peak/exampleSite/config.toml .

# Edit the primary website attributes.

#> baseURL = "https://0x666f6f.bitbucket.io/"
sed -i -e '\|baseURL =| s|= .*|= "https://0x666f6f.bitbucket.io/"|' ./config.toml

#> languageCode = "en-us"
sed -i -e '\|languageCode =| s|= .*|= "en-us"|' ./config.toml

#> title = "/dev/foo"
sed -i -e '\|title =| s|= .*|= "/dev/foo"|' ./config.toml

#> theme = "tranquil-peak". 
# NB: This must match the directory name under .../themes.
sed -i -e '\|theme =| s|= .*|= "tranquil-peak"|' ./config.toml

To further customise the Tranquil Peak attributes, refer to the user docs (recommended!).

Add content: hugo new

Let’s add some content: create a post and page under their respective subsections. Tranquil Peak, like many other themes, uses different metadata to differentiate “posts” from “pages”; usually related to inclusion/exclusion of social media/comments functionality.

# Change to the root directory of the Hugo source files.
cd ~/Git/0x666f6f.bitbucket.io/hugo-src/`

# Create a "foo.md" post under the "post/" subsection. 
docker run --rm -it -v $(pwd):/src -u hugo jguyomard/hugo-builder hugo new post/foo.md

# Create a "bar.md" post under the "page/" subsection. 
docker run --rm -it -v $(pwd):/src -u hugo jguyomard/hugo-builder hugo new page/bar.md

Both posts and pages are created as Markdown documents. They can be edited directly with a regular text editor, or alternatively with a dedicated Markdown editor. Any editor that provides a preview window for viewing Markdown syntax editing in realtime is recommended.

I personally use JupyterLab because it’s got a reasonable built-in Markdown Preview function, and is also my go-to editor for other coding projects. (I must admit however that the preview function doesn’t perfectly display all the gucci CSS functions that Tranquil Peak provides!) Needless to say, I also run it using Docker: docker run -it --rm -p 8888:8888 -v $(pwd):/opt/app/data mikebirdgeneau/jupyterlab.

Generate site (on localhost): hugo --destination /public

Optional - Generate the site on localhost. This step is not required for the “continuous integration” setup we’re using, as the site will be generated in the Cloud using Bitbucket Pipelines. You can nevertheless use this method to explore the generated output.

When the hugo command is run without any arguments, the static website will be generated using the contents of the “/src” directory; with the final output available (by default) in the “/public” directory.

# Change to the root directory of the Hugo source files.
cd ~/Git/0x666f6f.bitbucket.io/hugo-src/`

# Create an output directory. This is necessary in advance to ensure the owner is non-root.
mkdir -p ~/Git/0x666f6f.bitbucket.io/hugo-output

# Generate your site
docker run --rm -it -v $(pwd):/src -v $(pwd)/../hugo-output:/public -p 1313:1313 -u hugo jguyomard/hugo-builder hugo --destination /public

Shortcuts: bash aliases for Docker commands: hugo, hugo server --bind 0.0.0.0

For ease of use, you can create bash aliases to access Hugo commands without having to remember the additional Docker parameters. Add the following to your ~/.bashrc or ~/.bash_alias files. You’ll need to reload the file (. ~/.bashrc) or logout and back in again, after which they become permanently accessible.

# Alias for the "hugo" base command. (Combine with "hugo new site", "hugo new post/baz.md" etc)
alias hugo='docker run --rm -it -v $(pwd):/src -u hugo jguyomard/hugo-builder hugo'

# Alias to run a localhost server of the Hugo site
alias hugo-server='docker run --rm -it -v $(pwd):/src -p 1313:1313 -u hugo jguyomard/hugo-builder hugo server --bind 0.0.0.0'

Alternatively, use docker-compose (sudo apt-get install docker-compose) with a docker-compose.yml file in the root of the hugo project directory. This configuration approach can be used to execute Hugo commands: docker-compose run hugo, docker-compose up server, etc. See: this for an example file. (I had to change the version tag from “3.4” to “2.0” to get it working.)

CI/CD with Bitbucket Pipelines

Now for the fun stuff! Let’s configure Bitbucket Pipelines to automatically catch any new posts or pages we commit to the hugo branch of our repository; re-build our Hugo website, and push the results to our master branch and our website on the open Internet!

Activate the Pipelines service

Log in to Bitbucket, open the website repo, navigate to repo Settings, select the Settings option in the PIPELINES section. Activate the “Enable Pipelines” option.

Generate SSH key pairs

These will be used to connect the Piplelines environment to the website repository (both branches).

  1. Generate the Pipeline keys (for use in the Pipelines Docker environment):

    • Bitbucket -> website repo -> Settings -> PIPELINES -> SSH keys -> “Generate Keys”.
    • Copy the public key.
  2. Access the “Bitbucket settings” option (settings for the Bitbucket account rather than just the repository):

    • Bitbucket -> Settings -> SECURITY -> SSH Keys -> “Add Key”.
    • Paste the key and arbitrarily name it.

Create the pipelines file: bitbucket-pipelines.yml

We will create the Bitbucket Pipelines configuration file in the local (desktop) clone of the repository, then push it to the Cloud. Placing the file in the root of the repository will result in Bitbucket automatically executing the instructions in the file in a similar way to the instructions in a Dockerfile.

The initial working directory of your pipeline script should always be the directory containing your project files. The absolute path for that directory is also available in the BITBUCKET_CLONE_DIR environment variable.

# Change to the root directory of the website repository.
cd ~/Git/0x666f6f.bitbucket.io/

# Download a copy of the file from my public repository.
wget https://bitbucket.org/0x666f6f/0x666f6f-public/raw/e823666cb5465fa633794f6dac35656005af89a4/0x666f6f.bitbucket.io/bitbucket-pipelines.yml

The contents of the file will be as follows. NB! You need to edit this.

  • Change the four instances of ‘{your-bitbucket-username}’ to match your own. (In my case, they read ‘0x666f6f’ - no curly brackets!)
  • Also, change the single instance of ‘{your-bitbucket-email-address}’ to match your own.

There’s no need to worry about your email address on the open Internet in this situation, as the file will be hosted in your private repository.

File: bitbucket-pipelines.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# We'll use the 'jguyomard/hugo-builder' Docker image in this Pipeline.
image: jguyomard/hugo-builder

pipelines:
  branches:
    hugo:
      - step:
          script:
            # Change working directory to 'hugo-src', a subfolder of root of the current branch (hugo).
            - cd "${BITBUCKET_CLONE_DIR}/hugo-src"

            # Update/Initialise any submodules in the current branch.
            # This will clone the submodule contents into their directories within the pipeline environment.  
            - git submodule update --init --recursive

            # Clone the master branch into the 'public' directory.
            # This will enable updates to the master branch; namely the Hugo-generated output.
            - git clone -b master ssh://git@bitbucket.org/{your-bitbucket-username}/{your-bitbucket-username}.bitbucket.io public

            # Remove the entire contents of the master branch.
            # This ensures that any content that was deleted during the latest commit (hugo branch) is also purged.
            - rm -rf ./public/*

            # Remove the '.git' file under any theme submodules.
            # This is necessary to ensure that submodule files are also pushed to the master branch.
            # Without this step, the Hugo build will fail to generate index.html, and other mandatory files.
            - rm -rf ./themes/*/.git

            # Build the Hugo website in the 'public' directory.
            - hugo version
            - hugo --baseURL "https://{your-bitbucket-username}.bitbucket.io" --destination public --verbose
            - ls -al ./public

            # Push the build output to the master branch.
            # Authentication is achieved through SSH key generation (pipeline settings) and exchange (Bitbucket account settings).
            - git config --global user.email "{your-bitbucket-email-address}"
            - git config --global user.name "{your-bitbucket-username}"
            - cd public && git status && git add . && git commit -m "Generated from $BITBUCKET_COMMIT" && git push

            - echo "Finished!"

Commit and watch the magic happen!

# Change to the root directory of the website repository.
cd ~/Git/0x666f6f.bitbucket.io/

# Add all the files (recursively) to staging for your next Git commit
git add .

# Commit
git commit -a -m "Initial commit"
git push
If all went to plan, your website should now be accessible at https://{your-bitbucket-username}.bitbucket.io!

:)

Troubleshooting

Git: modified content, untracked content

Notes: After my initial commit, I received the following error:

user@d9-workstation:~/Git/0x666f6f.bitbucket.io$ git status
On branch hugo
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)
  (commit or discard the untracked or modified content in submodules)

    modified:   hugo-src/themes/tranquil-peak (modified content, untracked content)

This occurred because upstream changes to the theme resulted in modified/untracked content and associated error messages. Solutions: - Edit the .gitmodules file in the repo source, and under the theme submodule section, add ignore = dirty. - Or, see this useful guide.

Pipeline build fails

Pipeline execution details are accessible via the web interface of the Bitbucket repository. You can select each pipeline to view the output from each build.

NB! Be aware that, if there’s no pages at all under ./hugo-src/content/ (and/or subfolders), the Pipeline build will fail, even though the local Docker-based hugo server testing won’t. Hopefully you won’t get caught out like I was!

References and Credits

Thanks to the following publishers and resources, without whose articles I wouldn’t have been able to complete my own setup nor this guide.

Docker

Hugo

Git

Tutorials