I have a dream that I’ll have a blog…
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 Modeand 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: pipelinetype: dockername: 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_passwordNote
Note as I use 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 --minifycommand which will firstly download Congo theme dependency and then generate all assets intopublicsubfolder. Note as Drone will automatically clone the repo withdepth=1and 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.
Note
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/htmlNote
By default Nginx use /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: trueIf 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=truedeploy labels allows automatic discovery by Traefik. This is required whenexposedByDefaultis set tofalse. - The
traefik.http.routers.blog.entrypointsis 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.ruleallows 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.portis mandatory for Docker Swarm in order to proper routing to the internal port of our Docker image. For Nginx based images, it’s80by 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...Note
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 📎🔥…
Now, I have a blog…