Using VS Code Development Containers with Hugo

I build and maintain this blog using Hugo, a fairly popular static site generator. I do enjoy how easy it can make creating a site, although one thing that can be a little bit tedious is I work with more than one machine and I can have a tendency to forget what versions of which tools I have installed or just how I installed those tools. Consistency regrettably falls victim to my own laziness at these times. However, as I've been exploring the newest offerings in VS Code, I've noticed there have been a lot of shiny new features added around using Docker containers through the new Remove Extension for Containers. What this gives us is the ability to declare as part of our project the runtime parameters needed (via Dockerfile and a configuration file called devcontainer.json). By using development containers, we can assure the necessary tools and extensions are always present, allowing us to have a consistent development experience across different machines. This also gives us the benefit of speeding up onboarding time for newer team members.

It does sound great, but I want to preface it with a small dose of reality. Like anything, these remote containers are by no means a silver bullet that can magically be applied to any workflow, and in fact depending on the workflow containers could make it harder. It really just all depends on your workflow and what you're trying to accomplish. Personally, I think they are very powerful and useful, and that's why I'm sharing my experiences with them here.

What I will focus on in this article are the steps I took to setup this site's repo to use a development container. If you use Hugo to generate your site like I do, perhaps you may find this guide useful.

1. Starting Point

The examples provided in Microsoft's public repo show a wide variety of different technology stacks, anything from Dotnet Core, Node.js, Java, and of course Python just to name a few.

What I did not find was an example of how to setup for a repo that uses Hugo, or any other static site generator tool for that matter. What I ended up doing was taking the Markdown example, since Hugo sites are built on Markdown, and went from there. To start off we want to be sure we copy the .devcontainer folder contents along with including the .gitattributes file to assure line-endings are consistent.

With the .gitattributes file in place, we'll need to reset the line-endings on our local copy of the repo. Contrary to what Microsoft's documentation says at the time of this writing, there is no need to re-clone the repo. Just do the following.

git rm -rf --cache .
git reset --hard HEAD

2. Installing Hugo into the Dev Container

The Dockerfile for the Markdown container doesn't include Hugo, of course, so that meant having to make it available for use within the container. In order to do so, I had to add some things to that Dockerfile. We specify the HUGO_VERSION as an environment variable, install wget into the container, and then follow that up with another RUN command to download the .deb file and corresponding checksum file for verification and installation.

FROM debian:9

# Avoid warnings by switching to noninteractive
ENV DEBIAN_FRONTEND=noninteractive
ENV HUGO_VERSION="0.61.0"

# This Dockerfile adds a non-root 'vscode' user with sudo access. However, for Linux,
# this user's GID/UID must match your local user UID/GID to avoid permission issues
# with bind mounts. Update USER_UID / USER_GID if yours is not 1000. See
# https://aka.ms/vscode-remote/containers/non-root-user for details.
ARG USERNAME=vscode
ARG USER_UID=1000
ARG USER_GID=$USER_UID

# Configure apt and install packages
RUN apt-get update \
    #
    # Configure apt
    && apt-get -y install --no-install-recommends apt-utils dialog wget 2>&1 \
    #
    # Verify git and needed tools are installed
    && apt-get -y install git iproute2 procps lsb-release \
    #
    # Create a non-root user to use if preferred - see https://aka.ms/vscode-remote/containers/non-root-user.
    && groupadd --gid $USER_GID $USERNAME \
    && useradd -s /bin/bash --uid $USER_UID --gid $USER_GID -m $USERNAME \
    # [Optional] Add sudo support for the non-root user
    && apt-get install -y sudo \
    && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME\
    && chmod 0440 /etc/sudoers.d/$USERNAME \
    #
    # Clean up
    && apt-get autoremove -y \
    && apt-get clean -y \
    && rm -rf /var/lib/apt/lists/*

# Download and install hugo
RUN cd /tmp \
    && wget https://github.com/gohugoio/hugo/releases/download/v"$HUGO_VERSION"/hugo_"$HUGO_VERSION"_Linux-64bit.deb \
    && wget https://github.com/gohugoio/hugo/releases/download/v"$HUGO_VERSION"/hugo_"$HUGO_VERSION"_checksums.txt \
    && grep " hugo_${HUGO_VERSION}_Linux-64bit.deb\$" hugo_${HUGO_VERSION}_checksums.txt | sha256sum -c - \
    && dpkg -i hugo_"$HUGO_VERSION"_Linux-64bit.deb \
    && rm /tmp/hugo*

# Switch back to dialog for any ad-hoc use of apt-get
ENV DEBIAN_FRONTEND=dialog

From the devcontainer.json file, the only thing we need to do is add port 1313 to the appPort array so that when we start up the hugo server it can be accessed from the browser.

The resulting devcontainer.json files should look something like this.

{
 "name": "Markdown Editing",
 "dockerFile": "Dockerfile",

 // Use 'settings' to set *default* container specific settings.json values on container create.
 // You can edit these settings after create using File > Preferences > Settings > Remote.
 "settings": {
  "terminal.integrated.shell.linux": "/bin/bash"
 },

 // Use 'appPort' to create a container with published ports. If the port isn't working, be sure
  // your server accepts connections from all interfaces (0.0.0.0 or '*'), not just localhost.
  "appPort": [1313],

  "containerUser": "vscode",

  // Uncomment the next line to use a non-root user. On Linux, this will prevent
  // new files getting created as root, but you may need to update the USER_UID
  // and USER_GID in .devcontainer/Dockerfile to match your user if not 1000.
  "runArgs": [  ],

  // Add the IDs of extensions you want installed when the container is created in the array below.
  "extensions": [
    "yzhang.markdown-all-in-one",
    "streetsidesoftware.code-spell-checker",
    "DavidAnson.vscode-markdownlint",
    "shd101wyy.markdown-preview-enhanced"
  ]
}

3. Building the dev container

One way to build the dev container is to simply re-open the folder in VS Code. You'll get a nice prompt at the bottom corner of your screen asking if you want to re-open in a container.

When you choose this option, VS Code will close the folder and then re-open itself, communicating with the local Docker instance to create the dev container based on the parameters provided. If all goes well, after a few moments (or longer, depending on how long it takes to build the container), the folder should be open and you should have everything you need. Try it out by typing hugo version to verify you've got the expected version of Hugo installed, or run hugo server --bind 0.0.0.0 -D so you can navigate to localhost:1313 and try out your site locally.

For convenience, I added the command to startup the hugo server as a script in my repo so I don't have to keep typing it over and over again.

4. Notes on sharing GitHub credentials

According to the VS Code documentation, git credentials can be shared with the container a couple of ways. I've typically used SSH for authenticating between my local git client and GitHub and also with GitHub Enterprise at work.

For my personal computer and GitHub credentials, I found that enabling the SSH Agent and adding my keys to the agent did work as expected. However, when I tried this with my work computer and GitHub Enterprise it did not work at all. In this case, I wound up switching to using HTTPS and using a personal access token which I added to Windows Credential Helper and then those credentials did forward to the container as expected.

This all being said, the effort to do all this is only if you prefer to make use of the git integration that is part of VS Code. If you use an external client like GitHub Desktop or GitKraken, then this extra effort isn't necessary at all. This is because the repository's files are shared across a volume mount from the host system into the dev container. This also means that changes you make to files within the container will persist if you rebuild the container.

5. Next steps

One thing I want to do next with my site is make use of Git LFS for my images. If I want to integrate this with the way Netlify does things, I will have some additional setup work to do in my devcontainer. This will require having the Netlify CLI (and subsequently Node.js as a pre-req) along with Git LFS in order to enable this feature. I'll do a follow-up post with further info on how to do this.