3 minutes
A poorman CI/CD for documentation - or other things
So you want to host the documentation for your project on a plain webserver. Maybe because it’s to be used only internally, or maybe because you don’t trust “the cloud” in the form of GitHub pages (and similar services).
Similarly to a static website, one could package a mkdocs
or sphinx
website in a container.
But what if we want it to re-build automatically?
So that you simply push to your git
server and everything else would happen magically?
A builder container
So we need a Docker container that 1) would periodically pull a repository, and 2) build it.
In most cases, the repository is private (otherwise, why are you complicating things?), and we would need a public key to pull
this repository.
We can build a simple container with a 1 minute cronjob with the following Dockerfile:
FROM python:3.12-slim
RUN apt update && apt install --yes git ssh cron && apt autoclean && rm -rf /var/cache/apt
RUN pip install --no-cache-dir mkdocs
COPY puller.sh /puller.sh
RUN chmod +x /puller.sh
# A cronjob every minute. Output to stdout so it can be seen with `docker logs`
RUN echo "* * * * * root /puller.sh > /proc/1/fd/1 2>/proc/1/fd/2" >> /etc/cron.d/puller
# We need to "trust" github.com (assuming there is where we have our repository!)
# This of course would fail when keys rotate!
RUN ssh-keyscan -t rsa github.com >> ~/.ssh/known_hosts
CMD ["cron", "-f"]
A usual Docker build script would help (assuming you are using a registry, otherwise customize it):
docker build . -t registry:5000/docs-builder:staging
docker push registry:5000/docs-builder:staging
docker tag registry:5000/docs-builder:staging registry:5000/docs-builder:stable
docker push registry:5000/docs-builder:stable
And the magic bit puller.sh
:
# (C) 2024 Massimo Girondi massimo@girondi.net
echo "Pulling documentation at $(date --iso-8601=second)"
cd /docs
git config --global --add safe.directory /docs
OLD_HEAD=$(git rev-parse HEAD)
GIT_SSH_COMMAND='ssh -i /docs-builder -o IdentitiesOnly=yes' git fetch --all
git reset --hard origin/main
NEW_HEAD=$(git rev-parse HEAD)
# Rebuild only if the HEAD has changed
if [ "$OLD_HEAD" != "$NEW_HEAD" ]; then
echo "Head changed: rebuild"
# Cron typically doesn't include /usr/local/bin in $PATH
/usr/local/bin/mkdocs build
else
echo "No change"
fi
echo "Done at $(date --iso-8601=second)"
Note that we are using a specific ssh key for pulling our repo (/docs-builder
), since we want to authenticate with ssh (and not with HTTPs). We will mount it as volume when running the container. The public key is then added to the repository as a deploy key in GitHub terms.
Your mileage may vary.
Putting everything together
We can encapsulate everything in a docker compose
stack
version: "3"
services:
server:
image: nginx:stable-alpine-slim
volumes:
- ./docs/www/:/usr/share/nginx/html/:ro
restart: unless-stopped
ports:
- 8001:80
deploy:
resources:
limits:
cpus: '1'
memory: 500M
builder:
image: registry:50000/docs-builder:stable
# If you want to build withing docker compose, use the following:
# build: .
volumes:
- ./docs/:/docs
- ./docs-builder:/docs-builder:ro
restart: unless-stopped
deploy:
resources:
limits:
cpus: '.5'
memory: 500M
This would assume that your mkdocs.yaml
(or equivalent) would build inside ./docs/www
, ideally with sources in ./docs/docs
, with a folder structure similar to this:
./docker-compose.yaml
./Dockerfile
./build.sh
./puller.sh
./docs-builder -> the private SSH key
./docs-builder.pub -> the public SSH key
./docs/mkdocs.yaml -> the configuration file for mkdocs
./docs/docs -> the source folder, with .md files
./docs/www -> Where your HTML files would be placed