adelton

Building Django documentation in container

Jan Pazdziora

2016-05-29


Abstract

Just like running tests, building documentation can have dependencies that we might not want to install on our workstation or laptop. Using containers we isolate the software installation to the container image, and we can run processes privilege-separated from our user account.

Building Django documentation

Django project's documentation about building documentation says

Django's documentation uses the Sphinx documentation system, which in turn is based on docutils.

and it goes on to recommend

$ pip install Sphinx

as a way to get Sphinx.

My workstation has only minimal software set installed to get work done and it all gets installed via rpm. I could install the python-sphinx — but I prefer not to have extra software on my root filesystem.

The Django documentation then goes on suggesting

$ make html

so if we are to use container, we have to install the make package there as well.

Minimal container image

A reasonable start will therefore be docs/Dockerfile containing

FROM fedora
RUN dnf install -y make python-sphinx && dnf clean all

We can then build image django-docs with command

$ docker build -t django-docs docs
[ ... ]
  Installing  : python-sphinx-1.2.3-4.fc23.noarch                         43/44 
  Installing  : make-1:4.0-5.1.fc23.x86_64                                44/44 
[ ... ]
Successfully built 5367375bc73a

Giving container access

With the container image build, we can attempt to build the HTML documentation:

$ docker run --rm -ti -u nobody --security-opt=label:disable \
	-v $PWD/docs:/django-docs:ro django-docs make -C /django-docs html
make: Entering directory '/django-docs'
[ ... ]
OSError: [Errno 30] Read-only file system: '/django-docs/_build'
Makefile:49: recipe for target 'html' failed
make: *** [html] Error 1
make: Leaving directory '/django-docs'

We run the container as UID nobody and with SELinux type spc_t to allow the processes to access the content of our Django repository working tree (likely in our home directory) that we mount read-only as /django-docs.

As we can see, the html target make attempts to create _build/ directory to store the output. Since the filesystem is mounted read-only, that operation fails.

Let's create the target directory and make it available to the documentation building processes:

$ mkdir -p docs/_build
$ chmod o+wt,g+s docs/_build

By using the sticky bit with chmod +t, we allow the nobody user in the container to create the output directory there. With the setgid bit, any content that the container will create will have parent's group, making it easy to manipulate (and delete) the directory structure from outside of the container, with our account which created the directory.

Sphinx versions

We can now run the make html in the container, bind-mounting the target location as read-write:

$ docker run --rm -ti -u nobody --security-opt=label:disable \
	-v $PWD/docs:/django-docs:ro -v $PWD/docs/_build:/django-docs/_build \
	django-docs make -C /django-docs html
make: Entering directory '/django-docs'
sphinx-build -b djangohtml -n -d /django-docs/_build/doctrees -D language=   . /django-docs/_build/html
Making output directory...
Running Sphinx v1.2.3
Sphinx version error:
This project needs at least Sphinx v1.3 and therefore cannot be built with this version.
Makefile:49: recipe for target 'html' failed
make: *** [html] Error 1
make: Leaving directory '/django-docs'

The package version is lower than what the documentation-building tools expect. Since Fedora 23 which we use as our base image does not have newer version of Sphinx packaged (yet), using

RUN dnf install -y 'python-sphinx >= 1.3'

would not help us. Let's use the pip install to install the software instead:

RUN dnf install -y make python-pip && dnf clean all
RUN pip install 'Sphinx >= 1.3'

Note that we had to install python-pip via rpm to get pip in the first place.

Re-running the docker run command now shows it's using Sphinx 1.4.1 and it will end with

build succeeded, 2 warnings.
Build finished. The HTML pages are in /django-docs/_build/html.
make: Leaving directory '/django-docs'

Since we've bind-mounted the directory docs/_build/ to the container, that's where we will find subdirectory with the rendered HTML pages, just like we would if we had run make directory on the host machine.

Final Dockerfile

We can make the docker run invocation a little simpler by declaring the UID to use in the Dockerfile, as well as the command to run, which includes umask to make the directories writeable by group — useful to be able to the purge the directory with our user when we no longer need it.

The final docs/Dockerfile could then be

FROM fedora
# The python-sphinx in Fedora 23 is older than what Django master expects
# RUN dnf install -y make python-sphinx && dnf clean all
RUN dnf install -y make python-pip && dnf clean all
RUN pip install sphinx
RUN ( echo '#!/bin/bash' ; echo 'umask 0002 ; make -C /django-docs "$@"' ) > /bin/django-makedocs
RUN chmod a+x /bin/django-makedocs
ENV PYTHONDONTWRITEBYTECODE 1
USER nobody
ENTRYPOINT [ "/bin/django-makedocs" ]

After building the container image with

$ docker build -t django-docs docs

HTML documentation can be generated by running

$ mkdir -p docs/_build
$ chmod g+s,o+wt docs/_build
$ docker run --rm -ti --security-opt=label:disable -v $PWD/docs:/django-docs:ro -v $PWD/docs/_build:/django-docs/_build django-docs html
make: Entering directory '/django-docs'
sphinx-build -b djangohtml -n -d _build/doctrees -D language=   . _build/html
Running Sphinx v1.4.1
making output directory...
WARNING: while setting up extension djangodocs: directive 'versionadded' is already registered, it will be overridden
WARNING: while setting up extension djangodocs: directive 'versionchanged' is already registered, it will be overridden
loading translations []... not available for built-in messages
loading pickled environment... not yet created
loading intersphinx inventory from https://docs.python.org/3/objects.inv...
loading intersphinx inventory from http://initd.org/psycopg/docs/objects.inv...
loading intersphinx inventory from https://django-formtools.readthedocs.io/en/latest/objects.inv...
loading intersphinx inventory from https://pythonhosted.org/six/objects.inv...
loading intersphinx inventory from http://sphinx-doc.org/objects.inv...
building [mo]: targets for 0 po files that are out of date
building [djangohtml]: targets for 363 source files that are out of date
updating environment: 363 added, 0 changed, 0 removed
reading sources... [100%] topics/testing/tools
looking for now-outdated files... none found
pickling environment... done
checking consistency... done
preparing documents... done
writing output... [100%] topics/testing/tools
generating indices... genindex py-modindex
writing additional pages... search
copying images... [100%] intro/_images/admin14t.png
copying downloadable files... [100%] ref/contrib/gis/install/geodjango_setup.bat
copying static files... done
copying extra files... done
dumping search index in English (code: en) ... done
dumping object inventory... done
writing templatebuiltins.js...
build succeeded, 2 warnings.
Build finished. The HTML pages are in _build/html.
make: Leaving directory '/django-docs'

Versions used

The steps above were working on Fedora 23 with docker-1.10.3-16.gita41254f.fc23.x86_64 and Django 1.9.x and master (before 1.10) branches.