A sea in the french alps - the continues flow of water and information Hugo Continous Integration and Deployment

Having used legacy PHP-based CMS solutions such as Wordpress or Typo3 for a long time, I finally switched back to a static website generator, for various reasons. In this article, I want to outline a setup which makes it possible to treat distributed website development as clean as any other software, including versioning and continous deployment.

Why Hugo?

I retired Wordpress in favor of Hugo for several reasons:

  • Wordpress has a very old codebase with mixed quality and reliability
  • A lot of work was required to make it secure. Actually, my old website was a static copy of a Wordpress backend in order to keep zeroday flaws away
  • I would rather prefer to have a static website generated from markup files to avoid lock-in.
  • Wordpress supports a little versioning and reviewing. Nice. But, I would prefer the power and comfort of git and gitlab, as I am used to it from daily work.

After evaluating Jekyll, Hugo and Gatsby, I chose Hugo among the three, because:

  • Hugo is written in Go. My favourite language.
  • Hugo outperforms the other solutions by far
  • Hugo feels more stable than Gatsby
  • The templating language of Hugo is Golang string templates, which I am pretty familiar with. And, the community templates are a great thing to start, too.
  • Hugo follows the UNIX philosophy: Do one thing, and do it well
  • No npm depencency spree like in Gatsby
  • One binary, very comfortable to setup on a buildserver

Continous Deployment

Having setup a local website, the most important commands are hugo serve for live development and hugo build for building a static website. Having a remote server with (s)FTP/Webdav or related access, it would be perfectly viable to just upload the local distribution build (the content of the ./public folder in Hugo) to the remote server, but this might hit scalability issues very soon, for instance if multiple persons are working on the website, or there are requirements with regards to the workflow.

Assuming a remote server with docker, an interesting setup could be:

  • Docker with docker-compose (in case of a simple rootserver)
  • nginx for serving the stages
  • sftp for file access
  • lftp for filesync
  • Traefik as reverse proxy
  • git for versioning
  • gitlab-ci for automation and Continous Integration/Deployment

Providing a two-stage (consent and prod) website configuration using docker-compose and traefik:

 1ersion: '3'
 2
 3services:
 4  nginx-consent:
 5    image: nginx:mainline-alpine
 6    restart: always
 7    hostname: nginx
 8    labels:
 9      - "traefik.frontend.rule=Host:$DOMAINNAME_CONSENT"
10    volumes:
11      - ./$DOMAINNAME_CONSENT/$DOMAINNAME_CONSENT:/usr/share/nginx/html
12    environment:
13      - NGINX_HOST=$DOMAINNAME_CONSENT
14
15  nginx-prod:
16    image: nginx:mainline-alpine
17    restart: always
18    hostname: nginx
19    labels:
20      - "traefik.frontend.rule=Host:$DOMAINNAME_PRODUCTION"
21    volumes:
22      - ./$DOMAINNAME_PRODUCTION/$DOMAINNAME_PRODUCTION:/usr/share/nginx/html
23    environment:
24      - NGINX_HOST=$DOMAINNAME_PRODUCTIOn
25
26
27  sftp:
28    image: atmoz/sftp
29    labels:
30      - "traefik.enable=false"
31    volumes:
32        - ./$DOMAINNAME_CONSENT/:/home/$DOMAINNAME_CONSENT
33        - ./$DOMAINNAME_PROD/:/home/$DOMAINNAME_PROD
34        - $VAULT/sftp/ssh_host_ed25519_key:/etc/ssh/ssh_host_ed25519_key
35        - $VAULT/sftp/ssh_host_rsa_key:/etc/ssh/ssh_host_rsa_key
36        - $VAULT/sftp/sftp-users.conf:/etc/sftp/users.conf:ro
37    ports:
38        - "22:22"    

When the site is managed by git and a gitlab instance is available, a gitlab-CI script could be:

 1variables:
 2  DOCKER_DRIVER: overlay2
 3  REGISTRY: hub.docker.com
 4  GIT_SUBMODULE_STRATEGY: recursive
 5
 6stages:
 7  - build
 8  - deploy-consent
 9  - deploy-production
10
11build:
12  image: jojomi/hugo:0.58
13  stage: build
14  artifacts:
15    paths:
16      -  public
17  script:
18    - hugo
19    - pwd
20    - ls -alsh
21
22deploy-consent:
23  stage: deploy-consent
24  image: mwienk/docker-lftp:latest
25  script:
26    - echo $SFTP_HOST    
27    - lftp -e "set sftp:auto-confirm true; mirror -eRv --ignore-time $CI_BUILD_XA $DOMAINNAME_CONSENT; exit;" -u $SFTP_USERNAME,$SFTP_PASSWORD $SFTP_HOST
28  when: manual
29
30deploy-production:
31  stage: deploy-production
32  image: mwienk/docker-lftp:latest
33  script:
34    - echo $SFTP_HOST    
35    - lftp -e "set sftp:auto-confirm true; mirror -eRv --ignore-time /$CI_BUILD_XA /$DOMAINNAME_PRODUCTION; exit;" -u $SFTP_USERNAME_PROD,$SFTP_PASSWORD_PROD $SFTP_HOST
36  only:
37    - master
38    - develop
39  when: manual