No. I don’t want to have a git repository with a million billion files that are auto generated by hugo, jekyll.
No. I don’t want to use some non-official, homebrew, backwater, docker image made by some random guy that stopped maintaining the image in 2011.
I want my own dockerfile that is based on alpine
or even use an image official to the framework.
No. I definitely don’t want to use a WYSIWYG (What You See Is What You Get) editor - I have my own local markdown editor that works just fine thank you.
All I want i one (1) - i repeat - ONE fucking goddamn configuration file for the entire site (toml, conf, yaml I don’t care) and blog posts should be written in markdown.
If you are like me, read on.
Additionally, there should be community made themes available - but I shouldn’t have to fucking add them as a git submodule, god damn.
The blog should be hostable through a docker image that just takes your markdown and config file, builds the static website, and serves it using some standard server
(e.g. nginx or python’s http.server
I don’t care which, as long as it is somewhat standard - If I am managing a docker container, I will manage the networking in docker)
Ideally, the directory structure should look like this:
blog
├── Dockerfile // dockerfile to build and host the site
├── README.md // info about the repository, not a blogpost
├── hugo.toml // blog-framework configuration file
├── content
│ ├── about.md // the "about" page
│ └── posts // actual blog posts go here
│ └── example.md
└── static // non-markdown files
└── example.png
And then to build the site, simply build the container:
docker build .
Then you should just be able to insert the docker image into some docker-compose or kubernetes stack - or even just docker run -d
if you’d like.
The point of this is that you should really just focus on writing the blog entries - not the blog website.
If you want to use this workflow - this blog is written using this approach, so see my gitea instance or the github mirror for reference.
The Dockerfile
I have settled on goes like this:
FROM alpine
RUN apk add hugo git
WORKDIR /hugo
RUN hugo new site /hugo
RUN git clone https://github.com/yihui/hugo-xmin.git themes/hugo-xmin
ADD hugo.toml /hugo/hugo.toml
ADD content /hugo/content
ADD static /hugo/static
ENV PORT=1313
EXPOSE $PORT
CMD hugo serve --bind 0.0.0.0 --port $PORT
For now, I am just using the built-in server in hugo
, but it should be possible to serve using nginx
.
I mentioned hugo
before, but I was mostly mad that I had to add the autogenerated stuff in git - with this approach… I don’t have to 🎊!
Images
Just put images in the static
directory, and reference to them in your blog posts like you would normally in a hugo
project:
![example](/example.png)
This is not centered, and there’s no built-in shortcode for centering images (why not, hugo? You have a shortcode for figures, but no css class for centering - you cant even add a style
tag? Such an oversight)…
So we have to add one dirty thing to this setup. We have to add a shortcodes
directory, that is then also added to the docker container in /hugo/layouts/shortcodes
:
blog
├── Dockerfile
├── README.md
├── hugo.toml
├── content
│ ├── about.md
│ └── posts
│ ├── example.md
│ └── how-to-blog.md
├── shortcodes
│ └── centered.html // <-- Added
└── static
└── example.png
<!-- centered.html -->
<p align="center">
<img alt="example" src="{{.Get `image`}}" style="max-width: 100%;">
</p>
Then we can use this new shortcode like so:
{{< centered image="/example.png" >}}
Yes! Now we’re cooking with gas! … or atleast cooking with something. Note that this image centering trick does not work in reader mode.
Conclusion
We have made a docker file for automatically downloading, generating and serving a simple hugo
blog site.
Personally, I would’ve liked the static
and shortcodes
directories to not exist, but blog posts need images and it needs to center them, so they are a necessary evil.
Could we make the directory structure better and cleaner? Probably yes.
Will I make it better for this blog in the future? Probably yes.
Will I make another post when I do that? Probably yes!
If you want to just manually build and run docker image on your website server, feel free to stop reading here. The next section concerns about hosting, orchestrating and deploying the site automatically, but it’s totally not required.
Deployment
Being able to build and launch the docker image is nice and can suffice for smaller projects. Yes, this blog is a small project and the manual method should be more than enough, but I also play factorio (highly recommend it!), so I have to automate everything that is tedious. I also have other projects that I host on my VPS (Virtual Private Server) such my portfolio site gtz.dk and a gitea instance amongst other things.
Continuous Integration
I am using my personal gitea instance to host the source code for this blog - which means that I will be using the integrated CI system there, but you can use whichever CI service you’d like. The general concepts of the workflow should be fairly easy to translate to any kind of CI, but this is how my setup looks like:
name: Release CI action
run-name: ${{ gitea.repository }} release
on:
push:
branches:
- main
jobs:
build-and-push-container:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v4
- name: build container
run: docker build -t git.gtz.dk/${{ gitea.repository }}:latest .
- name: login
uses: docker/login-action@v3
with:
# NOTE: See https://gitea.com/gitea/docs/pulls/77/files
registry: git.gtz.dk
username: ${{ gitea.actor }}
password: ${{ secrets.PACKAGE_TOKEN }}
- name: push container
run: docker push git.gtz.dk/${{ gitea.repository }}:latest
This may look a bit overwhelming if you don’t know Github Actions, but it is really quite straight forward.
Every time we push a commit to the main
branch, we run the job named build-and-push-container
.
In the job, we first download our repository with the actions/checkout@v4
action, then we build the container using docker build
(we make sure to accurately tag the image), then we log in to the container registry that we want to host the container at using the docker/login-action@v3
action, and the we simply docker push
the container image.
Make sure to replace the git.gtz.dk
website mentions with your own github hosting service (whether self-hosted, or github.com
) and replace the gitea/GITEA
mentions with github/GITHUB
instead.
Note that the syntax is extremely similar to GitHub Actions - in fact Gitea Actions are trying to be 1 to 1 compatible with GitHub Actions, so it should be relatively straight forward.
This setup also gives us the possibility of performing traditional code-review before releasing by using pull requests. This should empower us to identify and correct issues (e.g. spelling mistakes or whatever) before they are pushed to the official website.
I will probably make a post about how to host a private gitea instance with an actions runner and all that jazz. For now, I will consider that particular rabbit hole out-of-scope.
This was the most difficult thing to do. We are almost there.
Continuous Delivery
With a docker image readily available, we can automatically deploy the blog when we push it!
I personally am a big fan of the simplicity of portainer, as it scales really well when doing personal server stuff.
What we really just need is a simple webhook… Fuck…
Now, don’t get me wrong - portainer is a good piece of software - their business tier is even worth the money. But I am not running a business here. I just want a personal blog. There has to be another way of automatically pulling and redeploying a container based on a new release event.
IN STEPS WATCHTOWER! This amazing piece of software will monitor and automatically update all containers on a system and update them! Now, this only updates once per 24 hours (by default), but that should be plenty fine.
services:
watchtower:
image: containrrr/watchtower
container_name: watchtower
environment:
- DOCKER_CONFIG=/config
volumes:
- watchtower-config:/config/
- /var/run/docker.sock:/var/run/docker.sock
volumes:
watchtower-config:
Only one small thing - if you are hosting the container on a private registry (like I am), you have to do some slight configuration regarding credentials - for this, I will simply refer to the watchtower documentation.
Conclusion
PHEW! I started this blog post using words like “simple” and “easy” - but it got a bit out of hand I must admit.
The workflow is very simple now though. You don’t have to contiously deal with the mess of deploying your stuff, and you don’t have to commit unnecessary autogenerated stuff to git.
Just focus on writing posts and publish them by merging to main
. Nice and automated 😁.
P.S.
You may notice that the CI script on the real blog repository is a bit more complicated than what we’ve went through in this post, but the extra complexity only comes from some stupid technicalities regarding my build server being based on ARM rather than x86 (raspberry pi). The script we made here is plenty good to get you started.