docker build image

This blog that you are looking at is buit using Hugo, a static site generator written in go. You could write and deploy a site as easy as in three steps without completely knowing the internals of the hugo. Hugo allows you to write the content in simple Markdown format and once done, running one command will generate a complete static site with all batteries included that you can serve using any http server like nginx. You can customize hugo either by writing your own templates with all the CSS or use one of the awesome free collections on hugo themes with drop in to your working theme directory. I will write about multiple features and usage in hugo, but for this post we will go over the process of writing a docker image that can be used for testing hugo sites and using it for any CI/CD for easy deployment. Focus will be more on the docker image building. In the coming posts I will show how I leverage this image for my deployment.

Install Docker

Setting up docker is fairly easy on any platform , There is docker for desktop available for both Mac OS and Windows platform that comes with executables. You can download and install them from here. If you are on any Linux platform make sure you provide write permission to docker to run from your own user otherwise you might run into sock issues. Once installed, test the installation with simple hello world docker like this docker run helloworld

Base Image

First thing to understand in building an image is DOCKERFILE, a default named file that contains your instructions to docker on how to build. All the instructions in these files are run sequentially to build the final image. If you don’t pass in a name of a file to docker for building , docker wil pick up the file names DOCKERFILE in the current directory to build. We will look at instruction in a while.

Building docker images can be seen as adding layer upon layer to build a cake. These layers are described as instruction/command in your docker file. Docker image can be built in two ways , one you build the image from scratch which means you start off adding every layer like filesystem, bootstrap on your own. This kind of image is often used to build other images like Linux, ubuntu or any environment based images. Other types of images are based on parent images, these images are built using images like ubuntu, Arch Linux, goland, python or Debian. With the parent image on top, you can add your layer using all the features in your parent image. We will start our image from a parent image called Alpine Linux. Alpine Linux is a very small image without a lot of bells and whistles of traditional linux. This allows to bring down the size of the image drastically which is almost around 2 MB compared to a standard image of any linux distribution >100 MB. Similar to alpine linux, there are other small versions of operating systems like debian and ubuntu. You can find a quick summary of all of them in this stackoverflow

Build Instructions

To start off lets create a file called Dockerfile in a directory and start putting the command in them. The very first thing we need to do is pull the base image to build on top of. That base image will be alpine linux. In the file your first instruction will be FROM alpine:latest

FROM is the standard keyword for docker and alpine:latest is the alpine version of linux with version to be latest. You can specify a particular available version if you need. When docker see’s this instruction , it will go to docker hub- a registry where all the images are store and pull from, pull the latest alpine image.

Next LABEL, Docker file can have additional labels that can help you organise your docker image. Let’s say you want to add a maintainer name and contact details then you can use the LABEL tag to define a key/value. So here i will define it as LABEL MAINTAINER =”sandeep”

At this point you can build this image by issuing ```docker build -t my_image . “ Here you are instructing docker to build the image with name “my_name” and look for the dockerfile in the current directory “.” . This should be successful since there it’s only pulling the alpine image.

Now that we have a base image to start off, we could run any command we usually do in linux like installing a package. So let’s look at the second instruction in the file.

RUN apk add bash \
   &&  apk add git  \
   && apk add hugo  \
   && rm -rf /var/cache/apk/* \
   && chmod 0777 /run.sh

RUN command will run any command you give to it on the base image. In our case we are first installing bash for ease of writing scripts later and for access as well. Since we are using an alpine image , it comes with an alpine shell(ash) and alpine package manager(apk) . which is why we are using ‘apk add bash’ and ‘apk add git’ these are instructing docker to install those packages. If you observe i am installing hugo as well from the apk. There are multiple ways to install hugo which you can find here. I am just going with the easy one. You could download the binary using wget and install as well. The command ```rm -rf /var/cache/apk/*`` will make sure the image size is small by removing all the cache indexes locally. I saw a good answer explaining what this does here

You can have any number of RUN commands in your docker file, but here some limitations around other commands like CMD or ENTRYPOINT, unless you are working on multi-stage builds. Next , When I run the container with this image , I should be able to provide a src path that contains my hugo site and also an output folder that would spit out static sites. Please note that the src and output path that i would be using to run the continater will exist in the host machine (my Mac) . Inorder to mount them to the docker container , we need to use VOLUME. VOLUME will mount your local host machine file system to the docker machine. Here is the instructions for that

# mount src from the run command
VOLUME /src
# final output dir - content of public
VOLUME /output

I want to specifically point out a simple difference between image and container. You run the container using an image. You can run multiple container that are based on single image with different name and options”

The actual path for the src and output provided during the container creation time using the command line options. Until then they are just directories in the image. After that we ask docker to treat /src as our working directory so that any command run after the ```WORKDIR /SRC `` will be executed in the given WORKDIR path. Next we issue ENTRYPOINT , this defines the one command that you container should run when executed. In our case we are pointing to a file called run.sh shell file that contains an additional command that the container should run when executed. Next we import/copy the run.sh from the same directory as dockerfile on the host machine to docker filesystem by using COPY command. This will copy the file to image and we should also give the execute permission to the file to be able to run using ENTRYPOINT.

WORKDIR /src
 
COPY ./run.sh /run.sh
 
RUN chmod 0777 /run.sh
 
ENTRYPOINT ["/run.sh"]
 
EXPOSE 8080

At this point our dockerfile is complete. Now lets focus on the run.sh command which is specific to the hugo build itself. My goal of this image is that when i provide source directory and output directory hugo should compile the content in the source directory and output the static site files in the output directory, for this hugo has following command hugo --destination="/output" . Since we already set the working directory to be src, hugo should be able to pick up the content and generate the static site in output dir. The idea of using run.sh is to be able to expand to do more complex stuff later on . otherwise you could do the same using RUN hugo --destination=/output as well.

FInally this how both file should look like

#Dockerfile
FROM alpine:latest
LABEL MAINTAINER="Sandeep M"
LABEL WEBSITE="sandeepm.dev"
 
# Copy the run.sh that run the hugo and outputs
COPY ./run.sh /run.sh
 
# install bash, git and hugo
RUN apk add bash \
   &&  apk add git  \
   && apk add hugo  \
   && rm -rf /var/cache/apk/* \
   &&
 
# mount src from the run command
VOLUME /src
# final output dir - content of public
VOLUME /output
# Hugo content src
WORKDIR /src
 
COPY ./run.sh /run.sh
 
RUN chmod 0777 /run.sh
 
ENTRYPOINT ["/run.sh"]
#run.sh
#!/bin/sh
 
# output the generated file to folder
hugo --destination="/output"

Second command you see is another hugo command that will startup the server to serve the static content. Not required for the original goal.

Build & Run

To the build image go, make sure your in the drirectory same as your dockerfile and run.sh file and then use follwoing command to build the image

docker build -t my_hugo_image .

-t lets you name and add additional tags to the image as well.

my_hugo_image is the image name.

Once the buld is complete you can check you images using docker build -t my_hugo_image . and check the docker image created by using docker images

docker build image

Now that you have image ready, next is to run a container using this image and optoins. You can use follwoing command to run docker run -v /Users/sandeep/projects/hugo/msandeep.io:/src -v /Users/sandeep/projects/hugo/docker/output:/output -d my_hugo as you see, i have mentioend src and output path for it to generate static files, -d is to instruct docker to run this container as demon in bacground that way it will keep running.

run build image

Once run , you should be able to see the output folder with the static site.

Next

Plan is to use GitHub Action and automate the build and deploy to the server that i am using for sandeepm.dev. I will write about that in the next post.

Thank you, Sandeep