Skip to content

Walkthrough of Dockerfile for ghpages docker

Eric Vennemeyer edited this page Aug 2, 2022 · 3 revisions

What's in the Dockerfile?

This will be a quick walkthrough of the Dockerfile for ghpages-docker and is intended to give a leg up to anyone who may need to modify this file in the future, but who doesn't have a lot of prior experience with Docker. It is not a thorough explanation of Docker or a How-To on creating a Dockerfile. (For that, see Docker's Getting Started guide and guide to writing a Dockerfile.)

Please note that the Dockerfile itself is already fairly thoroughly commented, but this guide should offer a more detailed supplement to those comments.

Disclaimer: I know very little about Ruby and Linux, and I knew nothing about Dockerfiles when I first began researching this issue, so some of this info may be wrong or incomplete. Future Hack for LA devs should feel free to edit as they see fit.

Specify the base image for Build Stage 1

FROM ruby:2.7.3-alpine3.13 AS build

Most Dockerfiles begin by specifying a previously-created base image to build on, using the FROM command.

This particular Dockerfile also uses a multistage build, in which the first stage is used to install packages that are required only for the image build process, and the second stage is used to build the final image by copying over only the required files from the first stage. This results in a much smaller image, which speeds up download times. Note that both build stages begin with a FROM command, but only this one includes the information AS build. Naming the build in this way will allow us to reference it later when we want to copy files from it into a new build stage.

Set Ruby environment variables

ENV GEM_BIN=/usr/gem/bin
ENV GEM_HOME=/usr/gem

These lines assign values to environment variables that can then be accessed by Ruby when gems are installed, and also referenced later in the Dockerfile.

Install Linux packages

RUN apk --no-cache add \
  zlib-dev \
  libffi-dev \
  build-base \
  libxml2-dev \
  imagemagick-dev \
  readline-dev \
  libxslt-dev \
  libffi-dev \
  yaml-dev \
  zlib-dev \
  vips-dev \
  vips-tools \
  sqlite-dev \
  cmake

RUN apk --no-cache add \
  linux-headers \
  openjdk8-jre \
  less \
  zlib \
  libxml2 \
  readline \
  libxslt \
  libffi \
  git \
  nodejs \
  tzdata \
  shadow \
  npm \
  libressl \
  yarn

The base image we're using is built on Alpine Linux, a super lightweight Linux distro. Because it's so streamlined, there are a lot of utilities it doesn't automatically come with. The lines above install various utilities we'll need later via the Alpine Package Keeper (apk). It's likely that not all are truly required, but Docker won't be able to install the github-pages gem later without some of them.

Update currently installed gems

RUN echo "gem: --no-ri --no-rdoc" > ~/.gemrc
RUN unset GEM_HOME && unset GEM_BIN && \
  yes | gem update --system

When you need to execute a Linux command within a Dockerfile, you can do so using by prefacing it with the RUN command. There may not be any gems to update in our image at this point, but these lines are nevertheless held over from the original Jekyll Docker because, frankly, they don't seem to break anything.

Install github-pages gem

RUN gem install github-pages -- \
    --use-system-libraries

This is the line where the github-pages gem, which bundles all the dependencies required to run a GitHub Pages site locally, is installed. Note that no version number is specified, so every time an image is built from this Dockerfile the latest version will automatically be installed.

Specify the base image for Build Stage 2

FROM ruby:2.7.3-alpine3.13
LABEL maintainer "Jordon Bedwell <[email protected]>"

Same as Build Stage 1, but putting a FROM command here tells Docker that we're starting over with a new build stage. This time we also add a LABEL command to set metadata for the author of the Dockerfile. In this case, because ghpages-docker is based on the Jekyll Docker image by Jordon Bedwell, his name has been left as the maintainer.

Copy shell scripts into new image

COPY copy/all /

This line tells Docker to copy everything in the copy/all directory of the build context, which is the directory in which the Dockerfile is located, into the root of the new image. These shell scripts were written for Jekyll Docker and have been held over because the Docker container breaks without them.

Set Ruby environment variables again

ENV GEM_BIN=/usr/gem/bin
ENV GEM_HOME=/usr/gem

Because it's a new build stage, we have to treat it as if it's an entirely separate image from the one created during the first build stage. Nothing persists from the first stage unless we manually copy it over, and things like variables need to be reinitialized in order to apply in the new build stage. (There are fancier ways of carrying a variable over from a previous build stage, but for the sake of easy comprehension the code has simply been duplicated here.)

Set additional environment variables

ENV JEKYLL_BIN=/usr/jekyll/bin

# Set system ENV variables
ENV LANG=en_US.UTF-8
ENV LANGUAGE=en_US:en
ENV TZ=America/Chicago
ENV PATH="$JEKYLL_BIN:$PATH"
ENV LC_ALL=en_US.UTF-8
ENV LANGUAGE=en_US

The first line specifies a path in the new image where Jekyll can store its binaries. The rest are basic Linux system values.

Install just the utilities required by the shell scripts

RUN apk --no-cache add \
  bash \
  su-exec

The shell scripts we copied over at the beginning of this build stage won't run without bash and su-exec.

Copy the github-pages gem from Build Stage 1

COPY --from=build /usr/gem/ /usr/gem/

This is an example of how to copy over a package that was installed in a previous build stage, using the --from flag and specifying the name of the previous build stage we want to copy from. In the beginning of our first stage, in the line FROM ruby:2.7.3-alpine3.13 AS build, we called that stage build.

So, what we're telling Docker to do here is to copy the github-pages gem, which was installed into a directory called /usr/gem/ in the previous build stage, into an identical directory within the new build stage.

Create a user, make directories, set permissions, do some filesystem cleanup

RUN addgroup -Sg 1000 jekyll
RUN adduser  -Su 1000 -G \
  jekyll jekyll

RUN mkdir -p /var/jekyll
RUN mkdir -p /srv/jekyll
RUN chown -R jekyll:jekyll /srv/jekyll
RUN chown -R jekyll:jekyll /var/jekyll
RUN rm -rf /home/jekyll/.gem
RUN rm -rf /usr/gem/cache
RUN rm -rf /root/.gem

Tell Docker how to set up access to the new container once it's running

CMD ["jekyll", "--help"]
ENTRYPOINT ["/usr/jekyll/bin/entrypoint"]
WORKDIR /srv/jekyll
VOLUME  /srv/jekyll
EXPOSE 35729
EXPOSE 4000

The CMD command specifies what command(s) Docker should run by default when it starts up a container based on this image. ENTRYPOINT is similar.

WORKDIR sets the working directory that other commands within the Dockerfile will refer to, and VOLUME tells Docker to create a mount point at the specified directory.

Finally, EXPOSE tells Docker which ports to open from within the container so that it can be accessed from the host OS. In this case, 4000 is the port at which the server will display the locally-hosted website, and 35729 is required to enable live reload.

We'll ultimately use the information specified in these last lines in our docker-compose.yml file to make sure our local Jekyll server starts correctly with the docker compose up command.