February 18, 2020

MkDocs with Docker - Part 2: Azure Pipelines and ACR

In Part 1, we created a Docker image with MkDocs, and used the container to execute MkDocs. In this post, we will:

  • Improve the Docker image with additional MkDocs features.
  • Publish the image to Azure Container Registry.
  • Use the container in Azure Pipelines.
  • Use Azure Pipelines to publish the compiled documentation to Azure App Service.

Making Improvements

I'm not fond of MkDoc's default theme, and after trying out various themes, I've settled on Material for MkDocs. It seems to be the most feature-complete, with 3K GitHub stars and lots of extension support. (I also like Read-the-Docs theme, but it seems there are some bugs, such as code blocks rendering in a single line, and even though there's a pull-request for the fix, the developer has not merged it. That project has also been inactive for a couple of years.)

Let's change the Dockerfile to integrate the Material theme, and also to reduce some steps in preparation for Azure Pipelines. Refer to the sample GitHub repo for the project folder structure.

FROM python:3.8.1-alpine3.11

EXPOSE 8000

# Git is required for the git-revision-date plugin.
RUN apk add git

RUN pip install --no-cache-dir mkdocs && \
    pip install --no-cache-dir mkdocs-material && \
    pip install --no-cache-dir mkdocs-git-revision-date-localized-plugin && \
    pip install --no-cache-dir pymdown-extensions

# Note that /mnt/repo should be the git root folder, not the content folder,
# otherwise the git-revision plugin will complain during build.
CMD ["/bin/sh", "-c", "cd /mnt/repo/content && mkdocs build"]

I'm relatively new to Docker and python, so it's probably not the optimal Dockerfile, so will need to explore further, such as using multi-stage builds. Also should look into requirements.txt file for pip.

Note the CMD command in the Dockerfile – it assumes certain conventions for mounting the host directory path, so we'll need to follow it when we want to run the container in Azure Pipelines. If we don't want to follow the convention, it can be overwritten in docker run command.

Just out of curiosity, what shell does Docker use when it's executing the RUN command? Looks like it's /bin/sh.

Publish to Azure Container Registry

We can publish the docker image to Docker Hub, but we need to keep things private, so for this demo, let's set up an Azure Container Registry to host the docker images. Follow the quick start guide to setup an Azure Container Registry to push the image we've built above.

Enable the admin user functionality, as we'll use that later in Azure Pipelines below. Note that enabling the admin user is not recommended, but good enough for this demo. Service principals is used in production.

Azure Pipelines

To setup the pipeline, first, add a secret variable to store the Azure Container Registry's admin user password, as ACR-SECRET. Then add the following to azure-pipelines.yml:

trigger:
- master

pool:
  vmImage: 'ubuntu-latest'

steps:
- script: docker login -u <acr-name> -p $(ACR-SECRET) <acr-name>.azurecr.io
  displayName: 'Login to Azure Container Registry'

- script: docker pull <acr-name>.azurecr.io/tools/mkdocs:0.1
  displayName: 'Pull MkDocs docker image'

- script: docker run --rm --mount type=bind,source=$(pwd),target=/mnt/repo <acr-name>.azurecr.io/tools/mkdocs:0.1
  displayName: Run MkDocs build through the docker container.

The above pipeline will login to our container registry, pull down the image, and spin up a container of that image to execute MkDocs to build the documentation. One thing that may need to be investigated is if there's a way to cache the docker image so we don't have to pull it down every time. We'll need to weigh which will be more cost effective, since pulling from the ACR may incur cost. Also note that the Azure Pipelines Build Agents already have common docker images preinstalled, so we should try to use those as base when creating the Dockerfile. We'll also need to weigh the speed of the build, since loading the cache in the pipeline can also take some time.

Now, of course, you don't have to use the docker container here. You can just install MkDocs and its plugins in the pipeline... However, since all of my team members will be using the same docker container to build and test their documentation, we can be sure that it will work the same way on this build. And yes, there are other ways of enforcing those standards even if we didn't use Docker, but let's leave it at that for now.

Publish to Azure App Service

So the pipeline above does the build but doesn't actually do anything with the output of the build. Let's publish the compiled documentation site to Azure App Service. Since the site is static, I can also do Azure Storage Static Hosting, but it seems it's always public access. With the App Service, I'll put it behind our Active Directory authentication.

Follow the documentation to create the site. The documentation doesn't go into details on how to setup a static HTML site using the Portal, so I ended up just creating a Windows .Net Framework site for this demo.

Add the following to the azure-pipelines.yml from above:

- task: ArchiveFiles@2
  inputs:
    rootFolderOrFile: 'content/site'
    includeRootFolder: false
    archiveType: 'zip'
    archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip'
    replaceExistingArchive: true

- task: AzureRmWebAppDeployment@4
  inputs:
    ConnectionType: 'AzureRM'
    azureSubscription: '<Your Subscription Here>'
    appType: 'webApp'
    WebAppName: '<app-site-name>'
    packageForLinux: '$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip'

There are two additional tasks added – one to zip up the content of MkDocs build output, and the other to publish that zip file to Azure App Service.

We could also use Release Pipelines, but for this demo, it's enough to just let the build pipeline handle the deployment.

Now whenever the master branch is updated, the pipeline will do the build and publish the site.

Misc. Notes

The git-revision-date-localized plugin requires git to be installed in the container, since it uses git when MkDocs builds the site. It takes the date of the commit and adds it to the compiled document, and is supported by the Material theme.

Note that there are two sections for configuring MkDocs – plugins and markdown extensions. The git-revision-date-localized is configured under the plugin section. Refer to the demo GitHub repo.

It's slightly concerning that MkDocs hasn't had a release since September 7th, 2018. There are some issues, such as favicon not working, where the fix has been merged but hasn't been released yet, though a workaround does exist — overwriting it after the site has been built, which I've implemented in the azure-pipelines.yml in the demo repo.

One of the big gripes when working with markdown is handling images — you can't simply paste in a screenshot, which we often need to do when creating technical documentation. Fortunately, Visual Studio Code has an extension, Markdown Paste, that seems to work well.

February 17, 2020

MkDocs with Docker - Part 1: Getting Started

I had a chance to explore some options on what tools my teams can use for documentation. I've considered Confluence, which I've used in the past and it's what my teams use currently for some technical documentation, I've also seen Read-the-Docs in the past. I like the idea of having the document under git version control, and using pull-requests with code reviews to control the edits – a flow that my team members are very familiar with.

One constraint I have is that the documentation must be internal and can't be public, so I looked for static files or local hosting options. After some searching, settled on MkDocs for now. Read-the-Docs seemed promising, but local hosting is not supported officially as they are focusing on their own cloud host offering. Some other tools lacked search functionality.

Creating a Docker Image for MkDocs

I wanted to keep my local dev machine clean, which means not installing MkDocs and its tooling (MkDocs is written in python), so let's use a docker image. This will also make it easier for other developers in my team to run it on their local machines. Here's the Dockerfile:

FROM python:3.8.1-alpine3.11

RUN pip install mkdocs
    

Yes, it's simple. It will be expanded in Part 2, but good enough for now. I'm using a specific tag – 3.8.1-alpine3.11 – for the base because I already have alpine 3.11 image locally, so it wouldn't take up extra space if it was using another base image.

Let's build the image:

C:\data\my-docs\docker-image>docker build -t dusklight/mkdocs:0.1 .
    

Running MkDocs through Docker Container

Let's assume that I have C:\data\my-docs folder which will store the documentation. I want this folder to be accessible from the Docker container that I'll be running from the image I created above. I also want to serve the documentation from the container by MkDocs and access it from the host.

Share the Host Drive for Docker Containers

To make a folder on the host available to containers, in Docker Desktop for Windows, use the Settings to share the drive:

Start the Docker Container
C:\>docker run -it --rm -p 8888:8000 --mount type=bind,source="C:\data\my-docs",target=/mnt/my-docs dusklight/mkdocs:0.1 /bin/sh
    

I won't go into details in this post about the command, but basically it will start the container in interactive mode and expose container's port 8000 to port 8888 on the host, and make the folder available to the container at /mnt/my-docs.

Note that if you haven't shared the C drive first, you may see the following error: docker: Error response from daemon: status code not OK but 500: {"Message":"Unhandled exception: Drive has not been shared"}.

Creating a Sample Project with MkDocs

After the container starts, let's create a sample MkDocs project:

/ # cd /mnt/my-docs
/ # mkdocs new .
INFO    -  Writing config file: ./mkdocs.yml
INFO    -  Writing initial docs: ./docs/index.md
    

The files are now created in C:\data\my-docs.

Serving Documents with MkDocs Dev Server

MkDocs comes with a dev server that has live reloading feature, so we'll use that for now. When we are at a point where the documentation is ready and can be published to others, we'll build and publish with a Azure DevOps, in Part 2 of this series.

By default, MkDocs will bind to 127.0.0.1, so when it's run in the container, it's not accessible to the outside even with the right ports published in the docker command line. Open the mkdocs.yml and add the following so that it binds to all:

dev_addr: '0.0.0.0:8000'

Now let's start the server:

mkdocs serve

From the host machine, browse to htp://localhost:8888/ and MkDocs should come up.

In Part 2, we'll look at how to use the Docker image in a CI/CD process utilizing Azure DevOps and Azure Container Registry.