Presentation of my publishing method.

For my blog I chose the Hugo1 static website generator. Unlike the dynamic site like Wordpress the static site generators are very easy to use: there is no administration interface, no database to manage and no need to use a specialized editor because I write my articles in Markdown.

The principle is the following, I work locally on my computer and I broadcast the articles on my self-hosted server. To follow the evolution of my blog, I use the Git version manager and to keep a remote copy I use the Gitlab service on framagit.org.

After writing my article, how my blog is updated automatically on my self-hosted server?

My first solution was to use Ansible for deploying articles on my blog. The configuration files allowed me to do a good job but I am not fully satisfied with that. Indeed, to update the blog you must install Ansible, which is not always possible, especially on my smartphone.

My current solution is based on Gitlab’s CI service : we create a file called gitlab-ci.yml, located at the root of the Git repository, which defines a set of actions (or deployment pipeline) to be performed in chronological order.

The deployment pipeline gitlab-ci.yml :

stages:
  - build
  - deploy
build:
  stage: build
  image: alpine:latest
  tags:
    - private
  only:
    - master
  only:
    variables:
      - $CI_COMMIT_MESSAGE =~ /deploy/
  script:
  - apk update && apk add hugo git
  - git submodule update --init --recursive
  - hugo -d public
  artifacts:
    paths:
    - public
    expire_in: 10 mins
deploy:
  stage: deploy
  image: alpine:latest
  tags:
    - private
  only:
    - master
  only:
    variables:
      - $CI_COMMIT_MESSAGE =~ /deploy/
  before_script:
    - apk update && apk add openssh-client bash rsync
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null
    - mkdir -p ~/.ssh
    - chmod 700 ~/.ssh
    - echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
    - chmod 644 ~/.ssh/known_hosts
  script:
    - rsync -rz --delete public/ $DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH

The deployment pipeline
It consists of two stages:

  1. build : allows to build the public folder which contains the structure and the translated blog files from Markdown to html/css. The public folder is then cached to be used by the next step.
  2. deploy : allows to copy with Rsync this public folder on my self-hosted server.

This pipeline is containerized in Gitlab.

The choice of the docker image
If possible I prefer the Alpine image, it’s light which allows me to reduce the execution time of the pipeline. I compared with a Debian image, I went from 5min34s to 2min11s with the Alpine image.

  image: alpine:latest

The conditions to be respected
The pipeline is executed if and only if the branch is master and the commit message contains the keyword deploy.

  only:
    - master
  only:
    variables:
      - $CI_COMMIT_MESSAGE =~ /deploy/

Blog theme management
The theme I use for the blog is called Nederburg2, the source code is available on Gitub.
With the Hugo static site generator there is a directory named theme. It is in this directory that I import the theme Nederburg with the command git submodule :

git submodule add https://github.com/appernetic/hugo-nederburg-theme.git

The theme is a submodule, it’s just the URL address that is defined. That’s why I use the following command in the script gitlab-ci.yml to copy the theme code.

  script:
    git submodule update --init --recursive

Generation of SSH keys
In order for the Gitlab CI service to securely deploy the public folder to my self-hosted server, creation of an SSH key pair is required.
I create an SSH key pair ed25519 which is an implementation of the twisted Edwards Curve. It offers the same level of security as RSA while consuming less CPU resources.
Keys are generated without passphrase.

ssh-keygen -f ~/.ssh/gitlabci -t ed25519  
cat ~/.ssh/gitlabci.pub >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

For security, the private key is not written in the deployment file gitlab-ci.yml. I’m using an environment variable, which means the private key is written dynamically, when the deployment pipeline is running.

I define my environment variables in the options of my Framagit account.

See image below :
ssh-variables

  • SSH_KNOWN_HOSTS : The host key. Used to verify that the client is connected to the correct host. I get it with the following command : ssh-keyscan mydomainname.
  • SSH_PRIVATE_KEY: The private key without passphrase.

Setting up a GitLab Runner instance

  tags:
    - private

Framagit provides a GitLab Runnner to run a deployment pipeline. For security reasons, I run GitLab Runner from my self-hosted server.

The command below generates the configuration file : /srv/gitlab-runner/config/config.toml with the URL address of the framagit server, authentication token and other options.

docker run --rm -v /home/gitlab-runner/config:/etc/gitlab-runner gitlab/gitlab-runner register \
  --non-interactive \
  --executor "docker" \
  --docker-image alpine:latest \
  --url "https://*******" \
  --registration-token "******" \
  --description "private-runner" \
  --tag-list "private" \
  --run-untagged="false" \
  --locked="true" \
  --access-level="ref_protected"

Then I start the GitLab Runner instance :

docker run -d --name gitlab-runner --restart always -v /home/gitlab-runner/config:/etc/gitlab-runner -v /var/run/docker.sock:/var/run/docker.sock  gitlab/gitlab-runner:latest


private-gitlab-runner

Web server setup
To serve the static pages of the blog I use the Nginx web server. The service listens on port 80, but I have several other web services with port 80 open, that’s why I use the Traefix reverse-proxy. In addition Traefix automatically generates TLS certificates.

Below is the file docker-compose.yml

---
version: '3.7'
services:
  traefik:
    container_name: traefik
    restart: always
    image: traefik:v1.7.16
    ports:
      - 80:80
      - 443:443
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /home/traefik/traefik.toml:/traefik.toml
      - /home/traefik/acme.json:/acme.json
      - /home/traefik/log:/var/log/traefik
    labels:
      - "traefik.docker.network=traefiknet"
      - "traefik.enable=true"
    networks:
      - traefiknet

  blog_web:
    container_name: blog_web
    image: nginx:stable-alpine
    restart: always
    volumes:
      - /home/blog/output:/usr/share/nginx/html:ro
    labels:
      - "traefik.docker.network=traefiknet"
      - "traefik.frontend.rule=Host:blog.la-forge.ml"
      - "traefik.enable=true"
      - "traefik.port=80"
    networks:
      - internal
      - traefiknet
      
networks:
  traefiknet:
    external: true
  internal:
    external: false

Practical case of publication
The procedure is simple (that is indeed the goal!).

  1. I write the article in Markdown.
  2. The writing of the article finished, I do a git commit and I add in the commit message the keyword deploy.
  3. I do a git push. The blog publication pipeline is automatically triggered.
  4. I wait two minutes, the time that the pipeline ends

And here it is, the new article is visible on the blog*

* You will probably need to bypass your cache.

pipeline-de-deploiement