History of yet another blog
Table of Contents
Here we are, never too late too init my first blog π. So as a clichΓ©, this first post must have to talk about his blog…
Why Hugo ? #
I first exit obviously standard all-in-one CMS as Wordpress
. As a markdown lover, Markdown-based headless CMS as Strapi
seemed more suited, but I clearly prefer to avoid any heavy BO+DB storing combo and use Git power for proper native source revisions. Writing posts directly via VS Code with proper Markdown extensions stays an invaluable experience for me π
Besides, I wanted to have minimal work to do on frontend side π with all basic blog features (pagination, tags and so on…) π, just some configurations, without lose any more advanced customization if needed π οΈ.
So flat-file based π static generator was the obvious choice and Hugo
was by far the easiest to use in order to make minimally featured proper blog. Hugo site documentation is clear and proposes many modern and artistic themes.
The theme #
Congo was the perfect choice for me with Dark Mode
and Tailwind
as a bonus π. It provides additional shortcodes as alert, badge, button, icons, katex, lead, as well as complete charts and diagrams system.
The comments system #
I found utterances as the perfect Disqus alternative choice between tracking-free, quick and easiest install, open source and not too much vendor locking by integrate comments directly to actual blog Github repo issues.
You obviously must have a Github account, if you prefer self-hosted solution and multi social login choices, it seems that Remark42 is the perfect choice. Besides, it has official ready to go Docker image.
The repo #
As you can see the repo of this blog, thanks to Hugo modules we easily successfully manage to have only what maters versioned, i.e. only hugo and theme related config files and obviously the contents and layouts overload for customization.
Automatic build #
As any static site generators, the process steps are :
- Generate all static assets files from the content.
- Serving these outputted assets through simple web server as Nginx.
Hugo supports many deployments methods, with Github Actions as one of the simplest. In my case I prefer more self-hosted approach with my favorite CI/CD tool aka Drone CI. Here is the simplest way to build an image and pushing into a custom private docker image registry.
kind: pipeline
type: docker
name: default
steps:
- name: build
image: peaceiris/hugo:latest-mod
commands:
- hugo --minify
- name: image
image: plugins/docker
settings:
registry: registry.okami101.io
repo: registry.okami101.io/adr1enbe4udou1n/blog
tags: latest
username:
from_secret: registry_username
password:
from_secret: registry_password
latest-mod
image tag in my case because I use Congo theme as Go modules dependency, which is the cleanest way to do it as I do not need to include it on my repo. The latest
image tag has only Hugo binary without Go dependency.This Drone pipeline consists of simple 2 steps as we’re talking above :
- The first build step use minimal Go based image container which includes Hugo binary. All we have to do is launching
hugo --minify
command which will firstly download Congo theme dependency and then generate all assets intopublic
subfolder. Note as Drone will automatically clone the repo withdepth=1
and mount it into Hugo container. - Then we use official Docker plugin in order to build our final docker image and push into custom private registry.
In order to use public docker registry simply change image step as following :
...
- name: image
image: plugins/docker
settings:
repo: foo/bar
tags: latest
username:
from_secret: docker_username
password:
from_secret: docker_password
...
The Drone Docker plugin needs a local Dockerfile which will describe how to build the image. In our case, all we need is to choose a light web server (Nginx
will be perfect) and copy previously built public
subfolder from the 1st step into our docker image. It’s only 2 lines !
FROM nginx:alpine
COPY public /usr/share/nginx/html
/usr/share/nginx/html
as default public directory.Note as Drone will automatically mount the current volume state on each step so the public
folder will be directly available on all subsequent step.
It’s that it. We now have ready-to-deploy production image that will be auto updated on each push.
Hosting & Deployment #
In my case, I use custom self-hosted Docker Swarm
for all my projects, and Traefik
as reverse proxy. This proxy allows automatic service discovery and SSL management. All I have to do is define a new blog stack
into my swarm cluster. A stack is just the same as docker-compose file in standalone Docker host, but with additional deploy
statement that allows resource management as scaling strategy, etc.
version: "3"
services:
app:
image: registry.okami101.io/adr1enbe4udou1n/blog
networks:
- traefik-public
deploy:
labels:
- traefik.enable=true
- traefik.http.routers.blog.entrypoints=https
- traefik.http.routers.blog.rule=Host(`blog.okami101.io`)
- traefik.http.services.blog.loadbalancer.server.port=80
networks:
traefik-public:
external: true
If we forget all necessary traefik
related config, the stack can’t be more basic than that. It’s just a matter of pulling our image from our private registry. Don’t forget to save private registry credentials by drone login hub.myregistry.com
command before.
All we have to do is to redirect web traffic from custom domain to our new Docker container instantiated from our final image. It’s really easy with a proper configured Traefik reverse proxy :
- Firstly the service must be connected to the public dedicated Traefik internal private network.
- The
traefik.enable=true
deploy labels allows automatic discovery by Traefik. This is required whenexposedByDefault
is set tofalse
. - The
traefik.http.routers.blog.entrypoints
is used for selecting the proper configured entrypoint. See it as the external port access between end users and your host. - The
traefik.http.routers.blog.rule
allows proper routing from a specific URL request pattern (most of the time the DNS host) to this service. - Finally the
traefik.http.services.blog.loadbalancer.server.port
is mandatory for Docker Swarm in order to proper routing to the internal port of our Docker image. For Nginx based images, it’s80
by default.
Then use the docker stack deploy -c blog.yml blog
command for launching the stack. If successfully started, Traefik will automatically discover the new service and route all traffics from blog.okami101.io
URLs to our custom Nginx container.
Continuous Deployment #
The final task is to configure automatic deploy to production on each push. For that we have to restart the above service on every push. This can be achieved via docker service update blog_app
, note as blog_app
is the default service name that follow <stack_name>_<service_name>
naming convention.
BUT by default it will not use the latest updated image by default. So we need to add additional --image
argument as following : docker service update --image registry.okami101.io/adr1enbe4udou1n/blog:latest blog_app --with-registry-auth
. The --with-registry-auth
argument is mandatory for private registries. This command must be launch on a manager Swarm cluster.
All we have to do now is to use our Drone pipeline with a new final deploy step that will consist on simple SSH command π with this above one-line script. This can be achieved easily thanks to Drone SSH plugin.
...
- name: deploy
image: appleboy/drone-ssh
settings:
host: front.okami101.io
port: 2222
username: okami
key:
from_secret: swarm_ssh_key
script:
- docker service update --image registry.okami101.io/adr1enbe4udou1n/blog:latest blog_app --with-registry-auth
...
swarm_ssh_key
is the secret private SSH key that will allow proper SSH connection to the swarm manager cluster.We have now full CI/CD π. Pretty heavy for just a fancy blog, but it would be not cool without a little overkill complexity. Moreover, it will be impossible to write my first post ! A blog without a single post will be so sad π.
I just regret to not have tried with Kubernetes or either tried to develop my own fancy HTTP framework as Hubert do for more ππ₯…